diff options
Diffstat (limited to 'js/seed/src/app.js')
-rw-r--r-- | js/seed/src/app.js | 165 |
1 files changed, 165 insertions, 0 deletions
diff --git a/js/seed/src/app.js b/js/seed/src/app.js new file mode 100644 index 0000000..34b4579 --- /dev/null +++ b/js/seed/src/app.js @@ -0,0 +1,165 @@ +// app.js +// Entrypoint for the app + +import { initialState, cloneState } from './state.js'; +import { update } from './update.js'; +import { view } from './view.js'; +import { fetchPokemon } from './api.js'; +import { initDevMode } from './dev.js'; + +const root = document.getElementById('app'); +let state = cloneState(initialState); +let dev; + +/** + * Entrypoint for the app. + * + * This file implements a minimal Elm-style architecture using only browser APIs and ES modules. + * - All state is immutable and updated by a pure update function. + * - The entire UI is re-rendered as a string on each state change for simplicity and predictability. + * - Event delegation is used to keep wiring minimal and functional. + * - No 3rd party code: everything is browser-native for cozy portability and clarity. + * + * Why this approach? + * - Functional, pure update/view logic is easier for me to reason about and test. + * - Re-rendering the whole UI avoids bugs from manual DOM updates and keeps state/UI in sync. + * - Minimal code and clear data flow make it easy to extend or adapt for new projects. + */ + +// Enable devMode if ?dev=1 is in the URL +/** + * devMode enables logging of all actions and state transitions for debugging. + * + * Why? This makes the app's state flow transparent, helping you understand and debug the app without extra tooling. + */ +const devMode = window.location.search.includes('dev=1'); + +/** + * Generalized render function for Elm-style apps. + * + * @param {Object} config - Render configuration + * @param {HTMLElement} config.root - Root DOM element + * @param {any} config.state - Current app state + * @param {Function} config.view - View function (state => HTML string) + * @param {Array} [config.events] - Array of { selector, event, handler } + * @param {Function} [config.postRender] - Optional function({ root, state }) for post-render logic + */ +function render({ root, state, view, events = [], postRender }) { + root.innerHTML = view(state); + events.forEach(({ selector, event, handler }) => { + const el = root.querySelector(selector); + if (el) el.addEventListener(event, handler); + }); + if (typeof postRender === 'function') { + postRender({ root, state }); + } +} + +// --- App-specific config for render --- +function postRender({ root, state }) { + const input = root.querySelector('#pokemon-query'); + const error = root.querySelector('.error'); + if (error) { + error.focus(); + } else if (input) { + input.focus(); + input.value = state.query; + input.setSelectionRange(input.value.length, input.value.length); + } +} + +function doRender() { + render({ + root, + state, + view, + events: [ + { selector: '#search-form', event: 'submit', handler: handleSubmit }, + { selector: '#pokemon-query', event: 'input', handler: handleInput }, + ], + postRender, + }); +} + +/** + * Dispatches an action to update state and re-render. + * + * Why centralize dispatch? This enforces a single source of truth for state changes, making the app predictable and easy to debug. + * + * Why log actions/state in devMode? This provides a transparent, time-travel-like view of app logic without needing any extra tooling. + */ +function dispatch(action) { + const prevState = state; + state = update(state, action); + if (devMode) { + dev.pushState(state); + console.groupCollapsed(`Action: ${action.type}`); + console.log('Payload:', action.payload); + console.log('Prev state:', prevState); + console.log('Next state:', state); + console.groupEnd(); + } + doRender(); +} + +/** + * Handles input events by dispatching an update action. + * + * Why not update state directly? All state changes go through dispatch/update for consistency and traceability. + */ +function handleInput(e) { + dispatch({ type: 'UPDATE_QUERY', payload: e.target.value }); +} + +/** + * Handles form submission, triggers async fetch, and dispatches state updates. + * + * Why handle async here? Keeps update/view pure and centralizes side-effect. + */ +async function handleSubmit(e) { + e.preventDefault(); + if (!state.query.trim()) return; + dispatch({ type: 'FETCH_START' }); + try { + const data = await fetchPokemon(state.query.trim()); + dispatch({ type: 'FETCH_SUCCESS', payload: data }); + } catch (err) { + dispatch({ type: 'FETCH_ERROR', payload: err.message }); + } +} + +// Initial render +doRender(); + +// After devMode is set +if (devMode) { + dev = initDevMode({ + getState: () => state, + setState: s => { state = s; }, + render: doRender + }); +} + +function updateHistoryInfo() { + if (!devMode || !dev) return; + dev.update(); +} + +function setHistoryPointer(idx) { + const info = dev.getHistoryInfo(); + if (idx < 1 || idx > info.length) return; + const newState = dev.setPointer(idx - 1); + if (newState) { + state = newState; + doRender(); + updateHistoryInfo(); + } +} + +function handleSliderChange(e) { + setHistoryPointer(Number(e.target.value)); +} + +function handleStepperChange(e) { + setHistoryPointer(Number(e.target.value)); +} \ No newline at end of file |