Todo demo annotated source
Back to indexThe todo sample project shows how the view and model layers of Torus interact in a simple case.
3
Bootstrap the required globals from Torus, since we're not bundling
5for (const exportedName in Torus) {6 window[exportedName] = Torus[exportedName];
7}
8
Task is our model for a single todo item. We just extend
Record since we don't need it to have any special functionality.
11class Task extends Record {}12
TaskStore represents a collection of todos, or our to-do list.
Our todo list component will be bound to this store.
15class TaskStore extends StoreOf(Task) {Let's sort this collection by the task description
17 get comparator() {18 return task => task.get('description').toLowerCase();19 }
20}
21
We create an instance of the task collection for our list, and initialize it with two items.
24const tasks = new TaskStore([
25 new Task(1, {description: 'Do this', completed: false}),26 new Task(2, {description: 'Do that', completed: false}),27]);
28
Component that represents a single todo item
30class TaskItem extends StyledComponent {31
32 init(source, removeCallback) {33 this.removeCallback = removeCallback;
34 this.onCheck = this.onCheck.bind(this);
35 this.deleteClick = this.deleteClick.bind(this);
We want the component to re-render when the todo item's properties change, so we bind the record's events to re-renders.
38 this.bind(source, data => this.render(data));
39 }
40
41 styles(data) {42 return {When the task is completed, we'll fade out that item.
44 'opacity': data.completed ? 0.4 : 1,
45 'height': '50px',
46 'width': '100%',
47 'background': '#eee',
48 'list-style': 'none',
49 'display': 'flex',
50 'flex-direction': 'row',
51 'align-items': 'center',
52 'justify-content': 'space-between',
53 'margin-bottom': '1px',
54 'cursor': 'pointer',
55 'padding': '0 12px',
56 '.description': {57 'flex-grow': '1',
58 'height': '50px',
59 'line-height': '50px',
60 'padding-left': '6px',
61 },
62 }
63 }
64
When we check the item off the list from the UI, we toggle the 'completed' property on the record.
67 onCheck() {68 this.record.update({69 completed: !this.record.get('completed'),70 });
71 }
72
73 deleteClick() {74 this.removeCallback();
75 }
76
77 compose(data) {78 return jdom`<li>
79 <input type="checkbox" checked="${data.completed}" onclick="${this.onCheck}"/>80 <div class="description" onclick="${this.onCheck}">${data.description}</div>81 <button class="removeButton" onclick="${this.deleteClick}">X</button>82 </li>`;
83 }
84
85}
86
Subclass of the default list view that represents a list of tasks.
We use the ListOf(TaskItem) syntax to tell TaskList to render
new items that appear in the collection as TaskItem components.
90class TaskList extends ListOf(TaskItem) {91
92 compose() {93 return jdom`<ul style="padding:0">${this.nodes}</ul>`;94 }
95
96}
97
Input field with a submit button that creates new items in the list of tasks.
100class TaskInput extends StyledComponent {101
102 init() {103 this.value = '';
104 this.boundOnKeyPress = this.onKeyPress.bind(this);
105 this.boundOnAddClick = this.onAddClick.bind(this);
106 this.boundSetValue = this.setValue.bind(this);
107 }
108
If an enter key is pressed, try to add a task
110 onKeyPress(evt) {111 if (evt.key === 'Enter') {112 this._addTask();
113 }
114 }
115
If the add button is clicked, try to add a task
117 onAddClick() {118 this._addTask();
119 }
120
We want the input component to be a controlled
component
so every time there's an input, we update the component's value property accordingly.
124 setValue(evt) {125 this.value = evt.target.value;
126 this.render();
127 }
128
What happens when we try to add a task? Create a new task in the task list with the current input value, in an uncompleted state.
132 _addTask() {133 if (this.value) {134 tasks.create({135 description: this.value,
136 completed: false,
137 });
138
139 this.value = '';
140 this.render();
141 }
142 }
143
144 styles() {145 return {146 'width': '100%',
147 'display': 'flex',
148 'flex-direction': 'row',
149 'input': {150 'flex-grow': 1,
151 },
152 }
153 }
154
155 compose() {156 return jdom`<div>
157 <input
158 value="${this.value}"159 oninput="${this.boundSetValue}"160 onkeypress="${this.boundOnKeyPress}"/>161 <button onclick="${this.boundOnAddClick}">Add</button>162 </div>`;
163 }
164
165}
166
A component to represent the entire app, bringing together the input component and the todo list component.
169class App extends StyledComponent {170
171 init() {We create instances of both input and list views here, to reuse in repeated renders.
174 this.input = new TaskInput();
175 this.list = new TaskList(tasks);
176 }
177
178 styles() {179 return {180 'font-family': "system-ui, 'Helvetica', 'Ubuntu', sans-serif",
181 'width': '100%',
182 'max-width': '500px',
183 'margin': '0 auto',
184 }
185 }
186
187 compose() {The app is really just both components' nodes wrapped in a single div.
190 return jdom`<div>
191 ${[this.input.node, this.list.node]}192 </div>`;
193 }
194
195}
196
Create an instance of the app, and append to the DOM.
198const app = new App();
199document.body.appendChild(app.node);
200