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