3.1 KiB
JS Module Split Design
Goal
Split public/app.js (1025 lines, single file) into ES modules under public/js/ — no build step, no npm. Modernize var to const/let in the process.
Module Structure
public/js/
state.js shared mutable state object
i18n.js translations, locale logic
api.js fetch wrapper, loadGoals(), saveGoal()
goals.js pure goal calculations (calc, tOff, dcls, …)
ui.js tpl(), showToast(), overlay helpers, updateHeader()
auth.js login/register/password screens
sheets.js openNew(), openData(), openAdmin()
render.js buildCard(), render(), wire()
app.js entry point: init, URL params, stopwatch, midnight timer
State Management
Single exported object in state.js. All modules import and mutate it directly:
// state.js
export const state = {
goals: [],
userName: '',
isAdmin: false,
prefs: {},
selDay: {},
addAmt: {},
renamingId: null,
renameVal: '',
collapsed: {},
TODAY: new Date(),
};
Replaces all global var declarations at the top of app.js. Mutations like goals = data become state.goals = data.
Dependency Graph
state.js ← all modules
i18n.js ← goals.js, ui.js, auth.js, sheets.js, render.js, app.js
api.js ← auth.js, sheets.js, app.js
goals.js ← render.js, sheets.js
ui.js ← auth.js, sheets.js, render.js
auth.js ← sheets.js, app.js
render.js ← app.js
Circular Import Solution
api.js previously called showLogin() on 401 — creating a circular dependency with auth.js. Resolved via a Custom Event:
// api.js — on 401:
document.dispatchEvent(new CustomEvent('session-expired'));
// app.js — on startup:
document.addEventListener('session-expired', () => showLogin());
Events are used only for this cross-cutting concern. All other communication is via direct imports.
const/let Conversion
- ES modules run in strict mode automatically
var→constwhere the binding is never reassignedvar→letfor loop counters and mutated locals- State re-assignments (
goals = newArray) →state.goals = newArray TODAYmoves intostate.TODAY, updated inscheduleMidnight()andvisibilitychange
HTML Change
templates/app.html.twig:
<!-- before -->
<script src="{{ asset('app.js') }}"></script>
<!-- after -->
<script type="module" src="{{ asset('js/app.js') }}"></script>
public/app.js is deleted after the migration is complete.
File Size Estimates
| File | Est. lines |
|---|---|
| state.js | ~15 |
| i18n.js | ~185 |
| api.js | ~35 |
| goals.js | ~60 |
| ui.js | ~35 |
| auth.js | ~130 |
| sheets.js | ~135 |
| render.js | ~185 |
| app.js | ~60 |
| Total | ~840 |
(Reduction due to removed comments and tighter const/let style.)
Migration Strategy
One module at a time, keeping the app functional throughout:
- Create
public/js/directory - Write modules in dependency order: state → i18n → api → goals → ui → auth → sheets → render → app
- Switch HTML to
<script type="module">when all modules are ready - Delete old
public/app.js