// 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)); }