[JavaScript Series 20] Capstone: A Mini Web App Covering Accessibility, Patterns, and Performance

한국어 버전

Welcome to the final lesson. We combine everything from lessons 01–19 to complete Task Hub, a mini task-management app. Focus on “feature and technical requirements + hash router + tests” as the core. Performance tuning and deployment pipelines are optional extensions once the baseline ships.

Key terms

  1. Feature requirements: The actions users expect—add, filter, stats, etc.
  2. Technical requirements: Constraints about tools or structure (state, tests, deployment).
  3. Workflow: The step-by-step order (design → build → test → deploy) that keeps the team in sync.
  4. Task Hub: The name of the capstone mini app; every example assumes this project.

Core ideas

  • Feature requirements (user-facing behavior): Task CRUD, filters, stats, activity log, routing, and accessibility.
  • Technical requirements (implementation rules): State management, pattern renderers, a11y attributes, lazy loading, tests, Vite build, automated deployment.
  • Workflow (order of work): Design → implement → test → performance check → deploy → review.

Planning and design workflow

Gather requirementsDesign folders/routerImplement featuresTests + BrowserMockPerformance & a11y checkDeployVerify/retro Next iteration

Read it as “document the plan → design folder and router structure → implement features → test → audit performance → deploy → review.” We follow the entire loop to expand the blocks we assembled earlier in the series.

Code examples

Start by locking in a folder structure for components, router, and utilities. Think of this as the seat belt that keeps files from piling up in one folder.

src/
  components/
    pattern.js
    task-form.js
    task-list.js
    stats-panel.js
    activity-log.js
  router/
    hash-router.js
  lib/
    storage.js
    bus.js
    performance.js
  tests/
    task-form.test.js
    router.test.js
  index.js
  styles.css

Next, build the hash router that orchestrates screen changes.

// router/hash-router.js
export function createHashRouter(routes, outlet) {
  function renderRoute() {
    const hash = location.hash.replace("#", "") || "home";
    const view = routes[hash] ?? routes["404"];
    outlet.innerHTML = view();
  }
  window.addEventListener("hashchange", renderRoute);
  renderRoute();
}

In one line: “Read the current hash, default to home, pick the view function, and render it into the outlet.”

The form component handles accessibility attributes and draft persistence.

// components/task-form.js

export function initTaskForm(root) {
  const form = root.querySelector("form");
  const feedback = root.querySelector(".form-feedback");
  const persistDraft = debounce(() => {
    const data = Object.fromEntries(new FormData(form));
    sessionStorage.setItem("task-draft", JSON.stringify(data));
  }, 300);

  form?.addEventListener("input", (event) => {
    const target = event.target;
    if (!(target instanceof HTMLInputElement)) return;
    target.setAttribute("aria-invalid", String(!target.value.trim()));
    persistDraft();
  });

  form?.addEventListener("submit", (event) => {
    event.preventDefault();
    const data = Object.fromEntries(new FormData(form));
    if (!data.title?.trim()) {
      if (feedback) feedback.textContent = "Please enter a title.";
      return;
    }
    emit("task:add", { task: { ...data, id: crypto.randomUUID(), done: false, createdAt: Date.now() } });
    form.reset();
    sessionStorage.removeItem("task-draft");
    if (feedback) feedback.textContent = "Task added.";
  });
}

Summed up: “Watch form input, and when ready, emit a task:add event.” Keep that promise throughout the capstone.

Verify the router with tests.

// tests/router.test.js

describe("createHashRouter", () => {
  it("renders the correct view when the hash changes", () => {
    const dom = new JSDOM(`<!doctype html><div id="app"></div>`);
    const outlet = dom.window.document.getElementById("app");
    createHashRouter({ home: () => "<p>Home</p>", stats: () => "<p>Stats</p>" }, outlet);
    dom.window.location.hash = "#stats";
    dom.window.dispatchEvent(new dom.window.HashChangeEvent("hashchange"));
    expect(outlet.innerHTML).toContain("Stats");
  });
});

The test only asks “does changing the hash swap the outlet?” but it safeguards the entire capstone.

Bundle tests and builds inside CI to ensure stability.

npm run test
npm run build

Continuous Integration should run tests first, then build.

Why It Matters

  • Connecting features, state, and routing in one screen helps you uncover bottlenecks quickly.
  • Shipping accessibility, performance, and deployment together mirrors real product constraints.
  • Completing the capstone gives you portfolio-ready evidence.

Practice

  • Follow along: Duplicate the folder structure and implement Task Hub with matching functionality.
  • Extend: Refine router, pattern renderer, and storage modules to meet your own requirements.
  • Debug: Run Lighthouse, chase performance and accessibility scores to 90+.
  • Done: Tests passing, build succeeding, deployment link live, and Lighthouse metrics over 90 signal completion.

Wrap-Up

Congratulations! If you finished all 20 lessons, you now have the skills to design and deploy UIs with plain JavaScript. Connect the app to any framework or backend you like and scale up your next project.

💬 댓글

이 글에 대한 의견을 남겨주세요