DEV Community

Cover image for The FDD for Vanilla JS: The 20-Min Doc That Saves 2 Days of Rework
brixtonmavu
brixtonmavu

Posted on

The FDD for Vanilla JS: The 20-Min Doc That Saves 2 Days of Rework

Functional Design Docs aren’t corporate overhead. They are a best practice that keeps code lean.

We avoid documentation because "vanilla JS" sounds pure and self-documenting. Then we ship three versions of the same bulk-delete feature and call it "agile."

A Functional Design Document (FDD) explains how a feature gap will be implemented. It converts high-level user needs into a structured functional solution. You need this when your project has more than one developer—or when it takes more than one week of work.


1. Why This Exists

✅ With FDD ❌ Without FDD
An FDD explains HOW a feature gap will be implemented. It converts user stories into a structured functional solution everyone agrees on before coding starts. Lack of structured design leads to confusion, incorrect assumptions, late-stage rework, and "wait, I thought we decided..." debates during code reviews.

Vanilla JS translation: Before you touch app.js, write down what the DOM can’t do yet versus what the user actually needs.


2. The Flow That Prevents Rework

Business Need ➔ Fit-Gap Analysis ➔ GAP Identified ➔ FDD Creation
                                                          │
Final Solution ◀─── Testing ◀─── Technical Development ◀──┘
Enter fullscreen mode Exit fullscreen mode

Example: Bulk Delete Feature

  1. Business Need: "Admins need to bulk delete items."
  2. Fit-Gap: We only have single-item deletion. There are no checkboxes or mass-selection elements in the UI. (GAP Identified)
  3. FDD Creation: Write the contract first. It takes 20 minutes.
  4. Technical Development: Write the Vanilla JS (implemented below).
  5. Testing: Run validation checks against the FDD specs.
  6. Final Solution: Ships once. No surprise pull request (PR) comments.

3. The 4 Design Docs You Actually Need

Doc Type Focus Example
FDD (Functional Design) What it does functionally. "Clicking 'Delete Selected' removes checked rows and shows a success toast."
TDD (Technical Design) How the code achieves it. Event delegation, DOM manipulation patterns, memory leak prevention.
IDD (Integration Design) API contracts & payloads. DELETE /api/items expects { ids: number[] }
ADD (Analytics Design) What metrics we track. Fire bulk_delete_completed event with metadata on success.

Best Practice: Product managers own the FDD. Developers own the TDD. Both parties sign off on both documents. That is the pattern.


4. FDD Structure + The Actual Vanilla JS

Here is an FDD for a bulk delete feature, paired with the exact Vanilla JS code that satisfies its requirements.

FDD Specification

  • GAP: No way to delete multiple table rows simultaneously. The current UI restricts deletion to individual items.
  • Solution Overview: Add a checkbox column to the admin table. The header checkbox toggles all selectable rows. A sticky actions bar shows the current selection count and the delete action.
  • Business Logic:
    • Skip rows with data-status="locked".
    • Display a confirmation prompt before executing deletion.
    • Show a success toast notification and fire an analytics event on successful deletion.

The Implementation

// admin-table.js - No frameworks, just a clear contract
class BulkDeleteController {
  constructor(tableSelector) {
    this.table = document.querySelector(tableSelector);
    this.selectedIds = new Set();
    this.deleteBtn = document.querySelector('#bulk-delete-btn');
    this.countEl = document.querySelector('#selected-count');

    if (!this.table) return;
    this.bindEvents();
    this.render();
  }

  bindEvents() {
    // Event delegation: 1 event listener instead of hundreds
    this.table.addEventListener('change', (e) => {
      if (e.target.matches('.row-checkbox')) {
        this.toggleRow(e.target.dataset.id, e.target.checked);
      }
      if (e.target.matches('#select-all')) {
        this.toggleAll(e.target.checked);
      }
    });

    this.deleteBtn?.addEventListener('click', () => this.handleDelete());
  }

  toggleRow(id, isChecked) {
    isChecked ? this.selectedIds.add(id) : this.selectedIds.delete(id);
    this.render();
  }

  toggleAll(isChecked) {
    const boxes = this.table.querySelectorAll('.row-checkbox:not([disabled])');
    boxes.forEach(box => {
      box.checked = isChecked;
      this.toggleRow(box.dataset.id, isChecked);
    });
  }

  render() {
    const count = this.selectedIds.size;
    this.countEl.textContent = count;
    this.deleteBtn.disabled = count === 0;

    // Update 'select-all' checkbox indeterminate state
    const allBoxes = this.table.querySelectorAll('.row-checkbox:not([disabled])');
    const selectAll = this.table.querySelector('#select-all');
    if (selectAll) {
      selectAll.checked = count > 0 && count === allBoxes.length;
      selectAll.indeterminate = count > 0 && count < allBoxes.length;
    }
  }

  async handleDelete() {
    const ids = [...this.selectedIds].map(Number);
    if (!ids.length) return;

    const confirmed = confirm(`Delete ${ids.length} items? This action cannot be undone.`);
    if (!confirmed) return;

    this.deleteBtn.disabled = true;
    this.deleteBtn.textContent = 'Deleting...';

    try {
      const res = await fetch('/api/items', {
        method: 'DELETE',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ids })
      });

      if (!res.ok) throw new Error('Delete operation failed');

      // FDD Requirement: Show toast on success
      this.showToast(`Successfully deleted ${ids.length} items`);

      // FDD Requirement: Cleanly remove rows from the DOM
      ids.forEach(id => {
        this.table.querySelector(`tr[data-id="${id}"]`)?.remove();
      });

      // FDD Requirement: Trigger analytics
      window.analytics?.track('bulk_delete_count', { count: ids.length });

      this.selectedIds.clear();
    } catch (err) {
      this.showToast('Failed to delete items. Please try again.', 'error');
    } finally {
      this.render();
      this.deleteBtn.textContent = 'Delete Selected';
    }
  }

  showToast(msg, type = 'success') {
    // Standardized toast implementation defined in our system architecture
    console.log(`[${type.toUpperCase()}] ${msg}`);
  }
}

// Initialize when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', () => {
  new BulkDeleteController('#admin-table');
});
Enter fullscreen mode Exit fullscreen mode

The HTML Structure

This markup matches the exact interface promised by our FDD:

<table id="admin-table">
  <thead>
    <tr>
      <th><input type="checkbox" id="select-all"></th>
      <th>Name</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr data-id="1">
      <td><input type="checkbox" class="row-checkbox" data-id="1"></td>
      <td>Item 1</td>
      <td>active</td>
    </tr>
    <tr data-id="2" data-status="locked">
      <td><input type="checkbox" class="row-checkbox" data-id="2" disabled></td>
      <td>Item 2</td>
      <td>locked</td>
    </tr>
  </tbody>
</table>

<div class="bulk-actions">
  <span><span id="selected-count">0</span> selected</span>
  <button id="bulk-delete-btn" disabled>Delete Selected</button>
</div>
Enter fullscreen mode Exit fullscreen mode

5. Validation Checks as Code

An FDD ensures that code remains testable and predictable. The functional requirements map directly to our code logic:

Check User Action Expected Behavior Code Proof
Multi-Select Flow Check 3 boxes, click Delete Confirm modal shows accurate count. confirm(\Delete ${ids.length} items?)
Select-All Logic Click header checkbox All un-locked rows get checked. querySelectorAll('.row-checkbox:not([disabled])')
Role Guard Load page as a 'viewer' role No select elements render. if (!canBulkDelete) return;
Empty State Deselect all active items Delete button returns to disabled state. this.deleteBtn.disabled = count === 0;

Rule of Thumb: If your functional design cannot be directly mapped to a validation check in code like the table above, the design is incomplete.


6. Business Benefits = Engineering Benefits

  1. Accurate Development: The written code does precisely what the FDD laid out. No guesswork required.
  2. Faster Delivery Times: You don't have to pause development mid-sprint to debate browser confirm() dialogue versus building a custom modal component. It was decided and documented beforehand.
  3. Streamlined Onboarding: A new engineer can read the FDD, inspect the Vanilla JS class, and immediately understand the relationship between feature expectations and technical implementation.
  4. Minimal Scope Creep: Hard edge cases like "what happens to locked items?" are addressed and validated before staging a single commit.

Conclusion

Code is simply our agreed-upon process, automated.

Writing Vanilla JS doesn't mean skipping best practices. It means choosing lightweight processes that keep you moving. 60 lines of vanilla JS backed by a 1-page FDD is faster, more performant, and more stable than 600 lines of framework boilerplate built without a plan.

Quick FDD Template

Copy and drop this outline into /docs/fdd/ on your next project:

# FDD: [Feature Name]

## 1. GAP Analysis
* What is missing or broken in the current UI/UX?

## 2. Solution Overview
* High-level summary of what we are building.

## 3. UI/UX Changes
* DOM modifications, states (empty, loading, disabled), and user pathways.

## 4. Business Logic & Edge Cases
* Roles/permissions, constraints, and conditional behavior.

## 5. Data & API Contract
* Payload requirements and endpoint changes.

## 6. Validation Checks
* How we prove the feature works as intended.

## 7. Impact Analysis
* What existing features could this change touch or break?
Enter fullscreen mode Exit fullscreen mode

Make this simple documentation change, and your next pull request will focus on code quality, not architectural debate.

Top comments (0)