diff options
Diffstat (limited to 'js/seed/src/app.js')
-rw-r--r-- | js/seed/src/app.js | 114 |
1 files changed, 86 insertions, 28 deletions
diff --git a/js/seed/src/app.js b/js/seed/src/app.js index c345ef7..34b4579 100644 --- a/js/seed/src/app.js +++ b/js/seed/src/app.js @@ -1,80 +1,105 @@ // app.js -// Entrypoint for the PokéAPI FRP/TEA app +// 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 PokéAPI FRP/TEA app. + * 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 via a pure update function. + * - 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 maximal portability and clarity. + * - No 3rd party code: everything is browser-native for cozy portability and clarity. * * Why this approach? - * - Functional, pure update/view logic is easy to reason about and test. + * - 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 prototypes. + * - 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 developers understand and debug the app without extra tooling. + * 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'); /** - * Renders the app UI and wires up event handlers. - * - * Why re-render the whole UI? This ensures the DOM always matches the state, avoiding subtle bugs from incremental updates. + * Generalized render function for Elm-style apps. * - * Why focus management? Accessibility: focusing error messages ensures screen readers announce them, and focusing the input improves keyboard UX. + * @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() { +function render({ root, state, view, events = [], postRender }) { root.innerHTML = view(state); - const form = root.querySelector('#search-form'); + 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 (form) { - form.addEventListener('submit', handleSubmit); - input.addEventListener('input', handleInput); - // Focus management for accessibility - if (error) { - error.focus(); - } else { - input.focus(); - input.value = state.query; - input.setSelectionRange(input.value.length, input.value.length); - } + 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 for free. + * 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(); } - render(); + doRender(); } /** @@ -89,7 +114,7 @@ function handleInput(e) { /** * Handles form submission, triggers async fetch, and dispatches state updates. * - * Why handle async here? Keeps update/view pure and side-effect free, following functional programming best practices. + * Why handle async here? Keeps update/view pure and centralizes side-effect. */ async function handleSubmit(e) { e.preventDefault(); @@ -104,4 +129,37 @@ async function handleSubmit(e) { } // Initial render -render(); \ No newline at end of file +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 |