Quellcode durchsuchen

Progress on todo app

Collin Cahill vor 3 Jahren
Ursprung
Commit
f2c012e606
6 geänderte Dateien mit 347 neuen und 30 gelöschten Zeilen
  1. 39 13
      src/components/todo-input.ts
  2. 108 0
      src/components/todo-item.ts
  3. 56 0
      src/components/todo-list.ts
  4. 29 0
      src/models/todo-model.ts
  5. 105 17
      src/todo-lit.ts
  6. 10 0
      src/util/util.ts

+ 39 - 13
src/components/todo-input.ts

@@ -1,8 +1,10 @@
 import {LitElement, html, css} from 'lit-element';
-import {customElement, query} from 'lit/decorators.js';
+import {customElement, property, query} from 'lit/decorators.js';
 
-// const ENTER_KEY = 13;
-// const ESC_KEY = 27
+const KeyCodes = {
+  ENTER: 'Enter',
+  NUMPAD_ENTER: 'NumpadEnter'
+};
 
 const TodoInputEventType = {
   SUBMIT_TODO: 'submit-todo'
@@ -14,34 +16,58 @@ export class TodoInputElement extends LitElement {
     :host {
       display: block;
     }
+    input[type="text"] {
+      width: 100%;
+      padding: 16px;
+      border: none;
+      background: rgba(0, 0, 0, 0.003);
+      box-shadow: inset 0 -2px 1px rgb(0 0 0 / 3%);
+      font-size: 14px;
+    }
   `;
 
   @query('input', true)
   _input!: HTMLInputElement;
 
+  @property({type: String})
+  description = '';
+
+  @property({type: String})
+  placeholder = '';
+
+  @property({type: Boolean})
+  newTodo = false
+
   render() {
     return html`
       <input type="text"
-        @keyup="${this._onkeyup}"
-        @input="${this._oninput}">
+        value="${this.description || ''}"
+        placeholder="${this.placeholder || ''}"
+        @keyup="${this._onkeyup}">
     `;
   }
 
   private _onkeyup(event: KeyboardEvent) {
-    if (event.code === 'Enter' || event.code === 'NumpadEnter') {
-      this.dispatchEvent(new CustomEvent(TodoInputEventType.SUBMIT_TODO, {
-        bubbles: true,
-        composed: true,
-        detail: this._input.value
-      }));
+    if (event.code === KeyCodes.ENTER || event.code === KeyCodes.NUMPAD_ENTER) {
+      this._submitTodo(this._input.value);
     }
   }
 
-  private _oninput(event: Event) {
-    console.log('oninput:', event);
+  private _submitTodo(description: string) {
+    this.dispatchEvent(new CustomEvent(TodoInputEventType.SUBMIT_TODO, {
+      bubbles: true,
+      composed: true,
+      detail: description
+    }));
+    this._input.value = '';
+  }
+
+  focus() {
+    this._input?.focus();
   }
 }
 
+
 declare global {
   interface HTMLElementTagNameMap {
     'todo-input': TodoInputElement;

+ 108 - 0
src/components/todo-item.ts

@@ -0,0 +1,108 @@
+import {LitElement, html, css} from 'lit-element';
+import {customElement, property} from 'lit/decorators.js';
+
+import TodoModel from '../models/todo-model';
+import './todo-input';
+
+@customElement('todo-item')
+export class TodoItemElement extends LitElement {
+  static styles = css`
+    :host {
+      display: block;
+      margin: 7px 0;
+      font-family: sans-serif;
+    }
+    .todo-item {
+      display: flex;
+      flex-direction: row;
+    }
+    button.remove-button {
+      visibility: hidden;
+    }
+    .todo-item:hover > button.remove-button,
+    .todo-item input:focus ~ button.remove-button,
+    .todo-item button.remove-button:focus {
+      visibility: visible;
+    }
+    .description {
+      display: flex;
+      flex-direction: column;
+      font-size: 14px;
+      font-weight: 400;
+    }
+    .description > span {
+      padding: 16px;
+    }
+    .created {
+      font-size: 10px;
+    }
+
+  `;
+
+  @property({type: Object})
+  todo: TodoModel = new TodoModel('');
+
+  @property({type: Boolean})
+  completed = false;
+
+  @property({type: Boolean})
+  editing = false;
+
+  render() {
+    return html`
+      <div class="todo-item">
+        <input type="checkbox"
+          .checked="${this.completed}"
+          name="Completed"
+          @change="${this._checkedChanged}">
+        <div class="description">
+          ${this.editing ? html`
+            <todo-input
+              .description="${this.todo.description}"
+              @submit-todo="${this._updateTodo}"></todo-input>
+          ` : html`
+            <span @click="${this._toggleEditing}">${this.todo.description}</span>
+          `}
+          <span class="created">Created: ${new Date(this.todo.dateCreated).toLocaleTimeString()}</span>        
+        </div>
+        <button type="button" @click="${this._removeClicked}" class="remove-button">X</button>
+      </div>
+    `;
+  }
+
+  private _removeClicked(_event: Event) {
+    this.dispatchEvent(new CustomEvent('remove-todo', {
+      bubbles: true,
+      composed: true,
+      detail: this.todo
+    }));
+  }
+
+  private _checkedChanged(_event: Event) {
+    this.dispatchEvent(new CustomEvent('toggle-completed', {
+      bubbles: true,
+      composed: true,
+      detail: this.todo
+    }));
+  }
+
+  private _toggleEditing(_event: Event) {
+    this.editing = !this.editing;
+  }
+
+  private _updateTodo(event: CustomEvent) {
+    this.todo.description = event.detail;
+    this.dispatchEvent(new CustomEvent('update-todo', {
+      bubbles: true,
+      composed: true,
+      detail: this.todo
+    }));
+    this.editing = false;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'todo-item': TodoItemElement;
+  }
+}

+ 56 - 0
src/components/todo-list.ts

@@ -0,0 +1,56 @@
+import {LitElement, html, css} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
+
+import TodoModel from '../models/todo-model';
+import './todo-item.js';
+
+@customElement('todo-list')
+export class TodoListElement extends LitElement {
+  static styles = css`
+    :host {
+      display: block;
+    }
+    ul {
+      padding: 0;
+    }
+  `;
+
+  @property({type: Array})
+  // TodoModel requires a string in the constructor. This will immediately be replaced with a real TodoModel.
+  todos: TodoModel[] = [new TodoModel('')];
+
+  render() {
+    return html`
+      <ul>
+        ${this.todos.map((todo) => html`
+          <todo-item
+            .todo="${todo}"
+            .completed="${todo.completed}"
+          >
+          </todo-item>`)}
+      </ul>
+      <div>
+        ${this.todos.length ?
+          html`${this._remainingItemsText()}` :
+          ''
+        }
+        <button type="button" @click="${this._removeCompleted}">Remove completed</button>
+      </div>
+    `;
+  }
+
+  private _remainingItemsText() {
+    const remainingLength = this.todos.filter((todo) => !todo.completed).length;
+    return html`<p>${remainingLength === 1 ? '1 item' : `${remainingLength} items`} left</p>`;
+  }
+  
+  private _removeCompleted() {
+    this.dispatchEvent(new Event('remove-completed'));
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'todo-list': TodoListElement;
+  }
+}

+ 29 - 0
src/models/todo-model.ts

@@ -0,0 +1,29 @@
+export default class TodoModel {
+  id: string;
+  description: string;
+  dateCreated: string;
+  completed: boolean;
+
+  constructor(description: string) {
+    this.id = this._generateUUID();
+    this.description = description;
+    this.dateCreated = new Date().toISOString();
+    this.completed = false;
+  }
+
+  private _generateUUID() {
+    let i, random;
+    let uuid = '';
+
+    for (i = 0; i < 32; i++) {
+      random = Math.random() * 16 | 0;
+      if (i === 8 || i === 12 || i === 16 || i === 20) {
+        uuid += '-';
+      }
+      uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random))
+        .toString(16);
+    }
+
+    return uuid;
+  }
+}

+ 105 - 17
src/todo-lit.ts

@@ -5,10 +5,17 @@
  */
 
 import {LitElement, html, css} from 'lit';
-import {customElement} from 'lit/decorators.js';
+import {customElement, property} from 'lit/decorators.js';
+import TodoModel from './models/todo-model';
+import Util from './util/util';
 
-import './components/click-counter.js';
-import './components/todo-input.js';
+import './components/click-counter';
+import './components/todo-input';
+import './components/todo-list';
+
+const DISPLAY_STRINGS = {
+  PLACEHOLDER: 'What needs to be done?',
+}
 
 /**
  * An example element.
@@ -18,31 +25,112 @@ import './components/todo-input.js';
  */
 @customElement('todo-lit')
 export class TodoLit extends LitElement {
-  constructor() {
-    super();
-    this.addEventListener('submit-todo', (event: Event) => {
-      console.log('submit-todo:', event);
-    });
-  }
-
   static styles = css`
     :host {
-      display: block;
+      display: grid;
+      grid-template-columns: auto [main-content] 600px auto;
       padding: 16px;
     }
+    h1, .todo-app {
+      grid-column: main-content;
+    }
+    .todo-app {
+      border: 1px solid rgba(0,0,0,0.3);
+      padding: 20px;
+    }
   `;
 
+  @property({type: Array})
+  todos: TodoModel[] = [];
+
+  @property({attribute: false})
+  filter: Function = () => {}
+
+  connectedCallback() {
+    super.connectedCallback();
+    this.todos = Util.store('todos');
+  }
+
   render() {
     return html`
-      <h1>Hello, ${this.foo()}!</h1>
-      <click-counter></click-counter>
-      <todo-input></todo-input>
-      <slot></slot>
+      <h1>todos</h1>
+      <section class="todo-app">
+        <div class="controls">
+          <button type="button" @click="${this._toggleAllCompleted}">Mark all completed</button>
+          <!-- <button type="button" @click="${this._removeAllTodos}">Remove all</button> -->
+          <todo-input
+            .placeholder="${DISPLAY_STRINGS.PLACEHOLDER}"
+            @submit-todo="${this._addTodo}"></todo-input>
+        </div>
+        <todo-list
+          .todos="${this.todos}"
+          @remove-todo="${this._removeTodo}"
+          @toggle-completed="${this._toggleCompleted}"
+          @update-todo="${this._updateTodo}"
+          @remove-completed="${this._removeCompletedTodos}"></todo-list>
+      </section>
     `;
   }
 
-  foo(): string {
-    return 'foo';
+  private _getIndexById(id: string) {
+    return this.todos.findIndex((todo) => todo.id === id);
+  }
+
+  private _addTodo(event: CustomEvent) {
+    const newTodo = new TodoModel(event.detail);
+    this.todos = [
+      ...this.todos,
+      newTodo
+    ];
+    Util.store('todos', this.todos);
+  }
+
+  private _removeTodo(event: CustomEvent) {
+    const indexToDelete = this._getIndexById(event.detail.id);
+    const newArray = [...this.todos];
+    newArray.splice(indexToDelete, 1);
+    this.todos = newArray;
+    Util.store('todos', this.todos);
+  }
+
+  private _toggleCompleted(event: CustomEvent) {
+    const indexToChange = this._getIndexById(event.detail.id);
+    const newArray = [...this.todos];
+    newArray[indexToChange].completed = !newArray[indexToChange].completed;
+    this.todos = newArray;
+    Util.store('todos', this.todos);
+  }
+
+  private _updateTodo(event: CustomEvent) {
+    const indexToChange = this._getIndexById(event.detail.id);
+    const newArray = [...this.todos];
+    newArray[indexToChange].description = event.detail.description;
+    this.todos = newArray;
+    Util.store('todos', this.todos);
+  }
+
+  private _toggleAllCompleted() {
+    let completedBool = true
+    if (this.todos.every((todo) => todo.completed)) {
+      completedBool = false
+    } 
+    const newArray = this.todos.map((todo) => {
+      todo.completed = completedBool;
+      return todo;
+    });
+    this.todos = newArray;
+    Util.store('todos', this.todos);
+  }
+
+  private _removeAllTodos() {
+    this.todos = [];
+    Util.store('todos', this.todos);
+  }
+
+  private _removeCompletedTodos() {
+    const newArray = this.todos.filter((todo) => !todo.completed);
+    this.todos = newArray;
+    Util.store('todos', this.todos);
   }
 }
 

+ 10 - 0
src/util/util.ts

@@ -0,0 +1,10 @@
+export default class Util {
+  static store(namespace: string, data?: unknown) {
+    if (data) {
+      return localStorage.setItem(namespace, JSON.stringify(data));
+    }
+
+    const store = localStorage.getItem(namespace);
+    return (store && JSON.parse(store)) || [];
+  }
+}