From 6d046963cf1d7c2f1e34fecb742f40be33fedc78 Mon Sep 17 00:00:00 2001 From: Aaron Reisman Date: Thu, 26 Dec 2024 01:14:12 -0800 Subject: [PATCH] add prettier --- .prettierignore | 4 + .prettierrc | 4 + README.md | 4 + package.json | 7 +- pnpm-lock.yaml | 12 ++- src/app_store.test.ts | 14 +-- src/app_store.ts | 26 ++--- src/app_view.test.ts | 96 ++++++++----------- src/app_view.ts | 60 ++++++------ src/app_view/add_button_view.test.ts | 2 +- src/app_view/add_button_view.ts | 40 ++++---- src/app_view/input_view.test.ts | 2 +- src/app_view/input_view.ts | 38 ++++---- src/main.ts | 12 +-- src/todo.ts | 2 +- src/todo_item_view.test.ts | 12 ++- src/todo_item_view.ts | 38 ++++---- src/todo_item_view/delete_button_view.test.ts | 2 +- src/todo_item_view/delete_button_view.ts | 54 +++++------ 19 files changed, 221 insertions(+), 208 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..dd6c136 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +dist +coverage +node_modules +pnpm-lock.yaml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a20502b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/README.md b/README.md index a500d33..496407d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A simple, functional and immutable todo application built with TypeScript demons The project follows a modular architecture with clear separation of concerns: ### `/src` + - `todo.ts`: Core domain model defining the `Todo` interface - `store.ts`: Generic store interface for state management - `app_store.ts`: Todo-specific store implementation with state management and actions @@ -25,16 +26,19 @@ The project follows a modular architecture with clear separation of concerns: The application follows these key architectural principles: 1. **Immutable State Management** + - All state updates create new state objects rather than mutating existing ones - State changes are handled through a centralized store - Typed actions for predictable state mutations 2. **Event-Based Architecture** + - Uses a publish/subscribe pattern for state updates - Components subscribe to state changes and update accordingly - Strong typing ensures type-safe event handling 3. **Functional Components** + - UI components are created using pure functions - Side effects are isolated and managed through callbacks - Clear separation between view logic and state management diff --git a/package.json b/package.json index f6c6c85..4f4a8a0 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,15 @@ "build": "tsc && vite build", "preview": "vite preview", "test": "vitest", - "coverage": "vitest run --coverage" + "coverage": "vitest run --coverage", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "devDependencies": { "@vitest/coverage-v8": "2.1.8", "jsdom": "25.0.1", - "typescript": "~5.7.2", + "prettier": "3.4.2", + "typescript": "5.7.2", "vite": "6.0.6", "vitest": "2.1.8" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29be6f6..5168c72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,8 +14,11 @@ importers: jsdom: specifier: 25.0.1 version: 25.0.1 + prettier: + specifier: 3.4.2 + version: 3.4.2 typescript: - specifier: ~5.7.2 + specifier: 5.7.2 version: 5.7.2 vite: specifier: 6.0.6 @@ -774,6 +777,11 @@ packages: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + engines: {node: '>=14'} + hasBin: true + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1670,6 +1678,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prettier@3.4.2: {} + punycode@2.3.1: {} rollup@4.29.1: diff --git a/src/app_store.test.ts b/src/app_store.test.ts index 697f1dc..7c1d836 100644 --- a/src/app_store.test.ts +++ b/src/app_store.test.ts @@ -37,7 +37,7 @@ describe('AppStore', () => { store.addTodo('First todo'); store.addTodo('Second todo'); const [todo1] = store.getState().todos; - + store.toggleTodo(todo1.id); const state = store.getState(); expect(state.todos[0].completed).toBe(true); // First todo toggled @@ -56,7 +56,7 @@ describe('AppStore', () => { const store = AppStore.create({ todos: [] }); store.addTodo('Test todo'); const todo = store.getState().todos[0]; - + store.removeTodo(todo.id); expect(store.getState().todos).toHaveLength(0); }); @@ -71,7 +71,7 @@ describe('AppStore', () => { it('should notify subscribers of state changes', () => { const store = AppStore.create({ todos: [] }); const actions: AppStore.Action[] = []; - + store.subscribe((action) => { actions.push(action); }); @@ -142,23 +142,23 @@ describe('AppStore', () => { const store = AppStore.create({ todos: [] }); const subscriber1 = (_action: AppStore.Action) => {}; const subscriber2 = (_action: AppStore.Action) => {}; - + // Subscribe both subscribers const unsubscribe1 = store.subscribe(subscriber1); const unsubscribe2 = store.subscribe(subscriber2); // Unsubscribe the first one unsubscribe1(); - + // Add a todo to verify subscriber2 still gets notifications store.addTodo('Test todo'); // Try to unsubscribe subscriber1 again (should handle -1 index) expect(() => unsubscribe1()).not.toThrow(); - + // Unsubscribe subscriber2 unsubscribe2(); - + // Try to unsubscribe both again (should handle empty subscribers array) expect(() => { unsubscribe1(); diff --git a/src/app_store.ts b/src/app_store.ts index 6620bc9..161752e 100644 --- a/src/app_store.ts +++ b/src/app_store.ts @@ -1,23 +1,23 @@ -import { Store } from "./store"; -import { Todo } from "./todo"; +import { Store } from './store'; +import { Todo } from './todo'; /** Application state type */ export type State = { todos: Todo[] }; /** Action types for state mutations */ export type Action = - | { type: "todoAdded"; todo: Todo } - | { type: "todoUpdated"; todo: Todo } - | { type: "todoRemoved"; id: Todo["id"] }; + | { type: 'todoAdded'; todo: Todo } + | { type: 'todoUpdated'; todo: Todo } + | { type: 'todoRemoved'; id: Todo['id'] }; /** Store type with todo-specific actions */ export type t = Store & { /** Adds a new todo item */ addTodo: (text: string) => void; /** Removes a todo item by id */ - removeTodo: (id: Todo["id"]) => void; + removeTodo: (id: Todo['id']) => void; /** Toggles the completed state of a todo item */ - toggleTodo: (id: Todo["id"]) => void; + toggleTodo: (id: Todo['id']) => void; }; /** @@ -40,7 +40,7 @@ export function create(initialState: State): t { publish(action); }; - const findTodo = (id: Todo["id"]) => state.todos.find((t) => t.id === id); + const findTodo = (id: Todo['id']) => state.todos.find((t) => t.id === id); return { getState: (): Readonly => state, @@ -61,10 +61,10 @@ export function create(initialState: State): t { }; update( { ...state, todos: [...state.todos, newTodo] }, - { type: "todoAdded", todo: newTodo } + { type: 'todoAdded', todo: newTodo }, ); }, - toggleTodo: (id: Todo["id"]) => { + toggleTodo: (id: Todo['id']) => { const todo = findTodo(id); if (!todo) return; @@ -74,14 +74,14 @@ export function create(initialState: State): t { ...state, todos: state.todos.map((t) => (t.id === id ? updatedTodo : t)), }, - { type: "todoUpdated", todo: updatedTodo } + { type: 'todoUpdated', todo: updatedTodo }, ); }, - removeTodo: (id: Todo["id"]) => { + removeTodo: (id: Todo['id']) => { if (!findTodo(id)) return; update( { ...state, todos: state.todos.filter((t) => t.id !== id) }, - { type: "todoRemoved", id } + { type: 'todoRemoved', id }, ); }, }; diff --git a/src/app_view.test.ts b/src/app_view.test.ts index be4c8a7..5e85e41 100644 --- a/src/app_view.test.ts +++ b/src/app_view.test.ts @@ -1,19 +1,23 @@ import { describe, it, expect, vi } from 'vitest'; import * as AppView from './app_view'; import type { Todo } from './todo'; -import type { Action } from './app_store'; +import type * as AppStore from './app_store'; describe('AppView', () => { - it('should create app view with all elements', () => { - const mockStore = { - getState: () => ({ todos: [] }), - subscribe: (_dispatch: (action: Action) => void) => () => {}, + function createMockStore(initialTodos: Todo[] = []) { + return { + getState: () => ({ todos: initialTodos }), + subscribe: vi.fn( + (_dispatch: (action: AppStore.Action) => void) => () => {}, + ), addTodo: vi.fn(), removeTodo: vi.fn(), toggleTodo: vi.fn(), }; + } - const view = AppView.create(mockStore); + it('should create app view with all elements', () => { + const view = AppView.create(createMockStore()); expect(view.container).toBeInstanceOf(HTMLDivElement); expect(view.heading).toBeInstanceOf(HTMLHeadingElement); @@ -25,15 +29,9 @@ describe('AppView', () => { }); it('should handle form submission', () => { - const mockStore = { - getState: () => ({ todos: [] }), - subscribe: (_dispatch: (action: Action) => void) => () => {}, - addTodo: vi.fn(), - removeTodo: vi.fn(), - toggleTodo: vi.fn(), - }; - + const mockStore = createMockStore(); const view = AppView.create(mockStore); + view.input.value = 'Test todo'; view.form.dispatchEvent(new Event('submit')); @@ -42,15 +40,9 @@ describe('AppView', () => { }); it('should not add empty todos', () => { - const mockStore = { - getState: () => ({ todos: [] }), - subscribe: (_dispatch: (action: Action) => void) => () => {}, - addTodo: vi.fn(), - removeTodo: vi.fn(), - toggleTodo: vi.fn(), - }; - + const mockStore = createMockStore(); const view = AppView.create(mockStore); + view.input.value = ' '; view.form.dispatchEvent(new Event('submit')); @@ -58,15 +50,9 @@ describe('AppView', () => { }); it('should handle todo actions', () => { - const mockStore = { - getState: () => ({ todos: [] }), - subscribe: vi.fn(), - addTodo: vi.fn(), - removeTodo: vi.fn(), - toggleTodo: vi.fn(), - }; - + const mockStore = createMockStore(); AppView.create(mockStore); + const subscriber = mockStore.subscribe.mock.calls[0][0]; // Test todoAdded action @@ -76,7 +62,9 @@ describe('AppView', () => { // Test todoUpdated action subscriber({ type: 'todoUpdated', todo: { ...todo, completed: true } }); - const checkbox = document.querySelector('input[type="checkbox"]'); + const checkbox = document.querySelector( + 'input[type="checkbox"]', + ); expect(checkbox?.checked).toBe(true); // Test todoRemoved action @@ -85,15 +73,9 @@ describe('AppView', () => { }); it('should handle unknown action', () => { - const mockStore = { - getState: () => ({ todos: [] }), - subscribe: vi.fn(), - addTodo: vi.fn(), - removeTodo: vi.fn(), - toggleTodo: vi.fn(), - }; - + const mockStore = createMockStore(); AppView.create(mockStore); + const subscriber = mockStore.subscribe.mock.calls[0][0]; expect(() => { @@ -102,28 +84,18 @@ describe('AppView', () => { }); it('should initialize with existing todos', () => { - const todo: Todo = { id: Symbol(), text: 'Existing todo', completed: false }; - const mockStore = { - getState: () => ({ todos: [todo] }), - subscribe: (_dispatch: (action: Action) => void) => () => {}, - addTodo: vi.fn(), - removeTodo: vi.fn(), - toggleTodo: vi.fn(), + const todo: Todo = { + id: Symbol(), + text: 'Existing todo', + completed: false, }; + AppView.create(createMockStore([todo])); - AppView.create(mockStore); expect(document.querySelectorAll('li').length).toBe(1); }); it('should handle non-existent todo item views', () => { - const mockStore = { - getState: () => ({ todos: [] }), - subscribe: vi.fn(), - addTodo: vi.fn(), - removeTodo: vi.fn(), - toggleTodo: vi.fn(), - }; - + const mockStore = createMockStore(); const view = AppView.create(mockStore); const subscriber = mockStore.subscribe.mock.calls[0][0]; @@ -132,14 +104,22 @@ describe('AppView', () => { subscriber({ type: 'todoAdded', todo }); // Test todoUpdated action with non-existent todo - const nonExistentTodo: Todo = { id: Symbol(), text: 'Non-existent', completed: false }; + const nonExistentTodo: Todo = { + id: Symbol(), + text: 'Non-existent', + completed: false, + }; subscriber({ type: 'todoUpdated', todo: nonExistentTodo }); expect(view.ul.children.length).toBe(1); - expect(view.ul.children[0].querySelector('label')?.textContent?.trim()).toBe('Test todo'); + expect( + view.ul.children[0].querySelector('label')?.textContent?.trim(), + ).toBe('Test todo'); // Test todoRemoved action with non-existent todo subscriber({ type: 'todoRemoved', id: nonExistentTodo.id }); expect(view.ul.children.length).toBe(1); - expect(view.ul.children[0].querySelector('label')?.textContent?.trim()).toBe('Test todo'); + expect( + view.ul.children[0].querySelector('label')?.textContent?.trim(), + ).toBe('Test todo'); }); }); diff --git a/src/app_view.ts b/src/app_view.ts index cdc0560..a08218d 100644 --- a/src/app_view.ts +++ b/src/app_view.ts @@ -1,9 +1,9 @@ /** Main application view module */ -import type * as AppStore from "./app_store"; -import { Todo } from "./todo"; -import * as TodoItemView from "./todo_item_view"; -import * as AddButtonView from "./app_view/add_button_view"; -import * as InputView from "./app_view/input_view"; +import type * as AppStore from './app_store'; +import { Todo } from './todo'; +import * as TodoItemView from './todo_item_view'; +import * as AddButtonView from './app_view/add_button_view'; +import * as InputView from './app_view/input_view'; /** Main application view structure */ export type t = { @@ -27,26 +27,26 @@ export type t = { * @returns The main application view structure */ export function create(appStore: AppStore.t): t { - const container = document.createElement("div"); - container.style.maxWidth = "1280px"; - container.style.margin = "0 auto"; - container.style.padding = "2rem"; - container.style.textAlign = "center"; + const container = document.createElement('div'); + container.style.maxWidth = '1280px'; + container.style.margin = '0 auto'; + container.style.padding = '2rem'; + container.style.textAlign = 'center'; container.style.fontFamily = - "Inter, system-ui, Avenir, Helvetica, Arial, sans-serif"; + 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif'; document.body.appendChild(container); - const heading = document.createElement("h1"); - heading.textContent = "Functional & Immutable Todo"; - heading.style.fontSize = "3.2em"; - heading.style.lineHeight = "1.1"; - heading.style.textAlign = "center"; + const heading = document.createElement('h1'); + heading.textContent = 'Functional & Immutable Todo'; + heading.style.fontSize = '3.2em'; + heading.style.lineHeight = '1.1'; + heading.style.textAlign = 'center'; container.appendChild(heading); - const form = document.createElement("form"); - form.style.display = "flex"; - form.style.marginBottom = "1rem"; + const form = document.createElement('form'); + form.style.display = 'flex'; + form.style.marginBottom = '1rem'; const { input } = InputView.create(); @@ -56,42 +56,42 @@ export function create(appStore: AppStore.t): t { form.appendChild(addButton); container.appendChild(form); - const ul = document.createElement("ul"); - ul.style.listStyle = "none"; - ul.style.paddingLeft = "0"; + const ul = document.createElement('ul'); + ul.style.listStyle = 'none'; + ul.style.paddingLeft = '0'; container.appendChild(ul); - const todoItemViews: Map = new Map(); + const todoItemViews: Map = new Map(); - form.addEventListener("submit", (e) => { + form.addEventListener('submit', (e) => { e.preventDefault(); if (input.value.trim()) { appStore.addTodo(input.value); - input.value = ""; + input.value = ''; } }); appStore.subscribe((action) => { switch (action.type) { - case "todoAdded": { + case 'todoAdded': { const todoItemView = TodoItemView.create( action.todo, appStore.toggleTodo, - appStore.removeTodo + appStore.removeTodo, ); ul.appendChild(todoItemView.li); todoItemViews.set(action.todo.id, todoItemView); break; } - case "todoUpdated": { + case 'todoUpdated': { const todoItemView = todoItemViews.get(action.todo.id); if (!todoItemView) return; TodoItemView.update(todoItemView, action.todo); break; } - case "todoRemoved": { + case 'todoRemoved': { const todoItemView = todoItemViews.get(action.id); if (!todoItemView) return; TodoItemView.remove(todoItemView); @@ -109,7 +109,7 @@ export function create(appStore: AppStore.t): t { const todoItemView = TodoItemView.create( todo, appStore.toggleTodo, - appStore.removeTodo + appStore.removeTodo, ); ul.appendChild(todoItemView.li); todoItemViews.set(todo.id, todoItemView); diff --git a/src/app_view/add_button_view.test.ts b/src/app_view/add_button_view.test.ts index f8f0987..40af9be 100644 --- a/src/app_view/add_button_view.test.ts +++ b/src/app_view/add_button_view.test.ts @@ -13,7 +13,7 @@ describe('AddButtonView', () => { it('should handle hover and focus events', () => { const view = AddButtonView.create(); - + // Initial state expect(view.addButton.style.backgroundColor).toBe('rgb(241, 241, 241)'); expect(view.addButton.style.borderColor).toBe('transparent'); diff --git a/src/app_view/add_button_view.ts b/src/app_view/add_button_view.ts index de150ab..3482102 100644 --- a/src/app_view/add_button_view.ts +++ b/src/app_view/add_button_view.ts @@ -6,23 +6,23 @@ export type t = { /** Creates and returns a new add button view component */ export function create(): t { - const addButton = document.createElement("button"); - addButton.type = "submit"; - addButton.textContent = "Add"; - addButton.style.borderRadius = "8px"; - addButton.style.border = "1px solid transparent"; - addButton.style.padding = "0.6em 1.2em"; - addButton.style.fontSize = "1em"; - addButton.style.fontWeight = "500"; - addButton.style.fontFamily = "inherit"; - addButton.style.backgroundColor = "#f1f1f1"; - addButton.style.cursor = "pointer"; - addButton.style.transition = "border-color 0.25s"; + const addButton = document.createElement('button'); + addButton.type = 'submit'; + addButton.textContent = 'Add'; + addButton.style.borderRadius = '8px'; + addButton.style.border = '1px solid transparent'; + addButton.style.padding = '0.6em 1.2em'; + addButton.style.fontSize = '1em'; + addButton.style.fontWeight = '500'; + addButton.style.fontFamily = 'inherit'; + addButton.style.backgroundColor = '#f1f1f1'; + addButton.style.cursor = 'pointer'; + addButton.style.transition = 'border-color 0.25s'; - addButton.addEventListener("focus", hover); - addButton.addEventListener("mouseover", hover); - addButton.addEventListener("blur", initial); - addButton.addEventListener("mouseout", initial); + addButton.addEventListener('focus', hover); + addButton.addEventListener('mouseover', hover); + addButton.addEventListener('blur', initial); + addButton.addEventListener('mouseout', initial); return { addButton, @@ -33,14 +33,14 @@ export function create(): t { * @param this - The HTML button element */ function hover(this: HTMLButtonElement) { - this.style.backgroundColor = "#e6e6e6"; - this.style.borderColor = "#8a8a8a"; + this.style.backgroundColor = '#e6e6e6'; + this.style.borderColor = '#8a8a8a'; } /** Handles initial/default state styling for the button * @param this - The HTML button element */ function initial(this: HTMLButtonElement) { - this.style.backgroundColor = "#f1f1f1"; - this.style.borderColor = "transparent"; + this.style.backgroundColor = '#f1f1f1'; + this.style.borderColor = 'transparent'; } diff --git a/src/app_view/input_view.test.ts b/src/app_view/input_view.test.ts index c97464c..fadfa32 100644 --- a/src/app_view/input_view.test.ts +++ b/src/app_view/input_view.test.ts @@ -13,7 +13,7 @@ describe('InputView', () => { it('should handle hover and focus events', () => { const view = InputView.create(); - + // Initial state expect(view.input.style.backgroundColor).toBe('rgb(241, 241, 241)'); expect(view.input.style.borderColor).toBe('transparent'); diff --git a/src/app_view/input_view.ts b/src/app_view/input_view.ts index dbef516..6ce1ad9 100644 --- a/src/app_view/input_view.ts +++ b/src/app_view/input_view.ts @@ -6,22 +6,22 @@ export type t = { /** Creates and returns a new input view component */ export function create(): t { - const input = document.createElement("input"); - input.type = "text"; - input.placeholder = "What needs to be done?"; - input.style.flex = "1"; - input.style.padding = "8px"; - input.style.marginRight = "8px"; - input.style.borderRadius = "8px"; - input.style.border = "1px solid transparent"; - input.style.fontSize = "1em"; - input.style.fontFamily = "inherit"; - input.style.backgroundColor = "#f1f1f1"; + const input = document.createElement('input'); + input.type = 'text'; + input.placeholder = 'What needs to be done?'; + input.style.flex = '1'; + input.style.padding = '8px'; + input.style.marginRight = '8px'; + input.style.borderRadius = '8px'; + input.style.border = '1px solid transparent'; + input.style.fontSize = '1em'; + input.style.fontFamily = 'inherit'; + input.style.backgroundColor = '#f1f1f1'; - input.addEventListener("focus", hover); - input.addEventListener("mouseover", hover); - input.addEventListener("blur", initial); - input.addEventListener("mouseout", initial); + input.addEventListener('focus', hover); + input.addEventListener('mouseover', hover); + input.addEventListener('blur', initial); + input.addEventListener('mouseout', initial); return { input, @@ -32,14 +32,14 @@ export function create(): t { * @param this - The HTML input element */ function initial(this: HTMLInputElement) { - this.style.backgroundColor = "#f1f1f1"; - this.style.borderColor = "transparent"; + this.style.backgroundColor = '#f1f1f1'; + this.style.borderColor = 'transparent'; } /** Handles hover and focus state styling for the input * @param this - The HTML input element */ function hover(this: HTMLInputElement) { - this.style.backgroundColor = "#e6e6e6"; - this.style.borderColor = "#8a8a8a"; + this.style.backgroundColor = '#e6e6e6'; + this.style.borderColor = '#8a8a8a'; } diff --git a/src/main.ts b/src/main.ts index 6539fad..54de14b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,14 @@ -import * as AppStore from "./app_store.ts"; -import * as AppView from "./app_view.ts"; +import * as AppStore from './app_store.ts'; +import * as AppView from './app_view.ts'; const appStore = AppStore.create({ todos: [] }); const appView = AppView.create(appStore); -appStore.addTodo("Learn TypeScript"); -appStore.addTodo("Build a functional example app"); +appStore.addTodo('Learn TypeScript'); +appStore.addTodo('Build a functional example app'); setTimeout(() => { - appStore.addTodo("Profit"); + appStore.addTodo('Profit'); }, 2000); -document.getElementById("app")?.appendChild(appView.container); +document.getElementById('app')?.appendChild(appView.container); diff --git a/src/todo.ts b/src/todo.ts index 525f597..2a5c5f5 100644 --- a/src/todo.ts +++ b/src/todo.ts @@ -1,4 +1,4 @@ -/** +/** * Represents a todo item in the application * @interface Todo */ diff --git a/src/todo_item_view.test.ts b/src/todo_item_view.test.ts index 785de55..76408ee 100644 --- a/src/todo_item_view.test.ts +++ b/src/todo_item_view.test.ts @@ -58,14 +58,22 @@ describe('TodoItemView', () => { const onRemove = vi.fn(); const view = TodoItemView.create(todo, onToggle, onRemove); - TodoItemView.update(view, { ...todo, completed: true, text: 'Updated todo' }); + TodoItemView.update(view, { + ...todo, + completed: true, + text: 'Updated todo', + }); expect(view.checkbox.checked).toBe(true); expect(view.label.textContent).toBe('Updated todo'); expect(view.label.style.textDecoration).toBe('line-through'); // Test updating back to uncompleted - TodoItemView.update(view, { ...todo, completed: false, text: 'Updated again' }); + TodoItemView.update(view, { + ...todo, + completed: false, + text: 'Updated again', + }); expect(view.checkbox.checked).toBe(false); expect(view.label.style.textDecoration).toBe('none'); }); diff --git a/src/todo_item_view.ts b/src/todo_item_view.ts index a8b6057..05e7b82 100644 --- a/src/todo_item_view.ts +++ b/src/todo_item_view.ts @@ -1,6 +1,6 @@ /** View module for individual todo items */ -import type { Todo } from "./todo"; -import * as DeleteButtonView from "./todo_item_view/delete_button_view"; +import type { Todo } from './todo'; +import * as DeleteButtonView from './todo_item_view/delete_button_view'; /** Todo item view component structure */ export type t = { @@ -25,34 +25,34 @@ export type t = { */ export function create( todo: Todo, - onToggle: (id: Todo["id"]) => void, - onRemove: (id: Todo["id"]) => void + onToggle: (id: Todo['id']) => void, + onRemove: (id: Todo['id']) => void, ): t { - const li = document.createElement("li"); - li.style.display = "flex"; - li.style.alignItems = "center"; - li.style.marginBottom = "0.5rem"; + const li = document.createElement('li'); + li.style.display = 'flex'; + li.style.alignItems = 'center'; + li.style.marginBottom = '0.5rem'; - const label = document.createElement("label"); - label.style.flex = "1"; - label.style.cursor = "pointer"; + const label = document.createElement('label'); + label.style.flex = '1'; + label.style.cursor = 'pointer'; label.style.fontFamily = - "Inter, system-ui, Avenir, Helvetica, Arial, sans-serif"; + 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif'; - const checkbox = document.createElement("input"); - checkbox.type = "checkbox"; - checkbox.style.marginRight = "8px"; + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.style.marginRight = '8px'; checkbox.checked = todo.completed; - checkbox.style.cursor = "pointer"; + checkbox.style.cursor = 'pointer'; - checkbox.addEventListener("change", () => onToggle(todo.id)); + checkbox.addEventListener('change', () => onToggle(todo.id)); const textNode = document.createTextNode(todo.text); label.appendChild(checkbox); label.appendChild(textNode); - label.style.textDecoration = todo.completed ? "line-through" : "none"; + label.style.textDecoration = todo.completed ? 'line-through' : 'none'; const { deleteButton } = DeleteButtonView.create(todo, onRemove); @@ -76,7 +76,7 @@ export function create( export function update(view: t, todo: Todo) { view.textNode.nodeValue = todo.text; view.checkbox.checked = todo.completed; - view.label.style.textDecoration = todo.completed ? "line-through" : "none"; + view.label.style.textDecoration = todo.completed ? 'line-through' : 'none'; } /** diff --git a/src/todo_item_view/delete_button_view.test.ts b/src/todo_item_view/delete_button_view.test.ts index 6a7879e..2909563 100644 --- a/src/todo_item_view/delete_button_view.test.ts +++ b/src/todo_item_view/delete_button_view.test.ts @@ -17,7 +17,7 @@ describe('DeleteButtonView', () => { const todo: Todo = { id: Symbol(), text: 'Test todo', completed: false }; const onRemove = vi.fn(); const view = DeleteButtonView.create(todo, onRemove); - + // Initial state expect(view.deleteButton.style.backgroundColor).toBe('rgb(255, 68, 68)'); expect(view.deleteButton.style.borderColor).toBe('transparent'); diff --git a/src/todo_item_view/delete_button_view.ts b/src/todo_item_view/delete_button_view.ts index 9a26a79..1581d1f 100644 --- a/src/todo_item_view/delete_button_view.ts +++ b/src/todo_item_view/delete_button_view.ts @@ -1,36 +1,36 @@ -import { Todo } from "../todo"; +import { Todo } from '../todo'; /** Interface representing the delete button view component */ export type t = { - /** The HTML button element for deleting todos */ - deleteButton: HTMLButtonElement; + /** The HTML button element for deleting todos */ + deleteButton: HTMLButtonElement; }; /** Creates and returns a new delete button view component * @param todo - The todo item associated with this delete button * @param onRemove - Callback function to handle todo removal */ -export function create(todo: Todo, onRemove: (id: Todo["id"]) => void): t { - const deleteButton = document.createElement("button"); - deleteButton.textContent = "x"; - deleteButton.style.marginLeft = "8px"; - deleteButton.style.borderRadius = "8px"; - deleteButton.style.border = "1px solid transparent"; - deleteButton.style.padding = "0.4em 0.8em"; - deleteButton.style.fontSize = "1em"; - deleteButton.style.fontWeight = "500"; - deleteButton.style.fontFamily = "inherit"; - deleteButton.style.backgroundColor = "#ff4444"; - deleteButton.style.color = "white"; - deleteButton.style.cursor = "pointer"; - deleteButton.style.transition = "border-color 0.25s, background-color 0.25s"; +export function create(todo: Todo, onRemove: (id: Todo['id']) => void): t { + const deleteButton = document.createElement('button'); + deleteButton.textContent = 'x'; + deleteButton.style.marginLeft = '8px'; + deleteButton.style.borderRadius = '8px'; + deleteButton.style.border = '1px solid transparent'; + deleteButton.style.padding = '0.4em 0.8em'; + deleteButton.style.fontSize = '1em'; + deleteButton.style.fontWeight = '500'; + deleteButton.style.fontFamily = 'inherit'; + deleteButton.style.backgroundColor = '#ff4444'; + deleteButton.style.color = 'white'; + deleteButton.style.cursor = 'pointer'; + deleteButton.style.transition = 'border-color 0.25s, background-color 0.25s'; - deleteButton.addEventListener("focus", hover); - deleteButton.addEventListener("mouseover", hover); - deleteButton.addEventListener("blur", initial); - deleteButton.addEventListener("mouseout", initial); + deleteButton.addEventListener('focus', hover); + deleteButton.addEventListener('mouseover', hover); + deleteButton.addEventListener('blur', initial); + deleteButton.addEventListener('mouseout', initial); - deleteButton.addEventListener("click", () => onRemove(todo.id)); + deleteButton.addEventListener('click', () => onRemove(todo.id)); return { deleteButton, @@ -41,14 +41,14 @@ export function create(todo: Todo, onRemove: (id: Todo["id"]) => void): t { * @param this - The HTML button element */ function hover(this: HTMLButtonElement) { - this.style.backgroundColor = "#ff6666"; - this.style.borderColor = "#ff8888"; + this.style.backgroundColor = '#ff6666'; + this.style.borderColor = '#ff8888'; } /** Handles initial/default state styling for the delete button * @param this - The HTML button element */ function initial(this: HTMLButtonElement) { - this.style.backgroundColor = "#ff4444"; - this.style.borderColor = "transparent"; -} \ No newline at end of file + this.style.backgroundColor = '#ff4444'; + this.style.borderColor = 'transparent'; +}