Todo App v2 (indexes)
In this demo, we build a more complex 'Todo' app. In addition to what we built in Todo App v1 (the basics), we let people specify a type for each todo, such as 'Home', 'Work' or 'Archived'.
We also index those types with an Indexes
object so that people can see their todos filtered by each type.
We're making changes to the Todo App v1 (the basics) demo.
Additional Initialization
We'll be creating an Indexes
object in this demo, so we'll need an additional import, the useCreateIndexes
hook. We'll also use a SliceView
component to display the index, instead of the simple TableView
component that we used before. We'll be using a Value
for the view state, so we'll also import the useSetValueCallback
hook and useValue
hook.
-const {createStore} = TinyBase;
+const {createIndexes, createStore} = TinyBase;
const {
CellView,
Provider,
- TableView,
+ SliceView,
useAddRowCallback,
useCell,
+ useCreateIndexes,
useCreateStore,
useSetCellCallback,
+ useSetValueCallback,
+ useValue,
} = TinyBaseUiReact;
We're defining a list of the types a todo can have, and giving our default todos each a different initial type:
+const TYPES = ['Home', 'Work', 'Archived'];
const INITIAL_TODOS = {
todos: {
- 0: {text: 'Clean the floor'},
+ 0: {text: 'Clean the floor', type: 'Home'},
- 1: {text: 'Install TinyBase'},
+ 1: {text: 'Install TinyBase', type: 'Work'},
- 2: {text: 'Book holiday'},
+ 2: {text: 'Book holiday', type: 'Archived'},
},
};
Adding Additional Stores And Indexes
In this demo we let people select a todo type and see a filtered list. The current type being displayed will need to be known by components across the app. We could make this a part of the top level component's state and pass it around with props.
But instead, we will create and memoize a second Store
object called viewStore
to store the current type being viewed, in a Value
called type
.
We also want to index the todos by type, so we create and memoize an Indexes
object, and define an index called types
on the todos
Table
, based on the value of the type
Cell
:
const App = () => {
const store = useCreateStore(() => createStore().setTables(INITIAL_TODOS));
+ const viewStore = useCreateStore(() =>
+ createStore().setValue('type', 'Home'),
+ );
+ const indexes = useCreateIndexes(store, (store) =>
+ createIndexes(store).setIndexDefinition('types', 'todos', 'type'),
+ );
return (
- <Provider store={store}>+ <Provider store={store} storesById={{viewStore}} indexes={indexes}> <Title />
<NewTodo />
+ <Types />
<Todos />
<Inspector />
</Provider>
);
};
Notice that we pass the new viewStore
and indexes
down into the app using the same Provider that we used for the store
in Todo App v1. We need to pass viewStore
in the storesById
prop so we can refer to it explicitly for the components that need it (to disambiguate it from the default Store
object that we provided in the store
prop).
We also added a new component to the app called Types
. This is a side-bar that lists the types so people can pick one and view the filtered Todos
list for it.
The Types
Component
This new component goes on the left-hand side of the demo and lists the available types. When people click a type name, the current type will be set in the viewStore
and the list on the right will be filtered accordingly. (Additionally, a new todo will be set to have this current type when it's added.)
The component literally just enumerates the TYPES
array and creates a Type
component for each one:
const Types = () => (
<ul id="types">
{TYPES.map((type) => (
<Type key={type} type={type} />
))}
</ul>
);
#types {
margin: 0;
}
The Type
Component
In the Types
component, each type appears as a clickable name. The viewStore
provides the currently selected type, and if it matches, this component will have an additional CSS class added to it.
If the component is clicked, the viewStore
's value will be updated with a callback provided by the useSetValueCallback
hook:
const Type = ({type}) => {
const currentType = useValue('type', 'viewStore');
const handleClick = useSetValueCallback(
'type',
() => type,
[type],
'viewStore',
);
const className = 'type' + (type == currentType ? ' current' : '');
return (
<li className={className} onClick={handleClick}>
{type}
</li>
);
};
NB: In this example, we are setting up one listener on the viewStore
for every instance of the Type
component in the side bar. This makes the Type
components completely self-sufficient. An alternative approach would be to use the useCell
hook once in the parent Types
component and pass down the current type as a prop to each item. We would then pass a parameter to the useSetCellCallback
hook to set the value based on the item clicked.
Which of these two approaches is optimal in the general case will depend on the number and complexity of the children components being rendered. For example we will do something similar to this in the Countries demo, which has a longer list of items in the side bar).
The Type
component has a small amount of styling:
#types .type {
cursor: pointer;
margin-bottom: @spacing;
user-select: none;
&.current {
color: @accentColor;
}
}
Upgrading The Todos
Component
Previously we used a TableView
component to list all the todos in the todos
Table
of the store
. But now we want to show only the todos of the current type. We created an Indexes
object (called indexes
) that has an index called types
. Within that is one slice per type, which we can render with a SliceView
component.
The slices in an index are simply sets of Row
Ids
from a Table
grouped according to the index's definition. Often - as here - these are sets of Row
Ids
that share a particular Cell
value.
We simply need to change the Todos
component to fetch the current type from the viewStore
, and then pass the corresponding index slice to the SliceView
component. It still uses the Todo
component to render each Row
itself:
const Todos = () => (
<ul id="todos">- <TableView tableId="todos" rowComponent={Todo} />
+ <SliceView
+ indexId="types"
+ sliceId={useValue('type', 'viewStore')}
+ rowComponent={Todo}
+ />
</ul>
);
Upgrading The Todo
Component
Since the todo has the new type
Cell
, we can display that alongside the text. In fact, we want to let people change the type for each todo too, so we implement a new component called TodoType
that contains as a select
dropdown. It has a callback to update the todo Row
with the value from the select
element if it is changed:
const Todo = (props) => (
<li className="todo">
<TodoText {...props} />+ <TodoType {...props} />
</li>
);
const TodoType = ({tableId, rowId}) => {
const type = useCell(tableId, rowId, 'type');
const handleChange = useSetCellCallback(
tableId,
rowId,
'type',
({target: {value}}) => value,
[],
);
return (
<select className="type" onChange={handleChange} value={type}>
{TYPES.map((type) => (
<option>{type}</option>
))}
</select>
);
};
We can style the select
element so that it appears on the right of the Todo
component:
#todos .todo .type {
border: none;
color: #777;
font: inherit;
font-size: 0.8rem;
margin-top: 0.1rem;
}
Upgrading the NewTodo
Component
Our final step is to make sure that when someone adds a new todo it defaults to the current type from the viewStore
- if only so that the newly-created todo appears in the current IndexView
component:
const NewTodo = () => {
const [text, setText] = useState('');
+ const type = useValue('type', 'viewStore');
const handleChange = useCallback(({target: {value}}) => setText(value), []);
const handleKeyDown = useAddRowCallback(
'todos',
({which, target: {value: text}}) =>
- which == 13 && text != '' ? {text} : null,
+ which == 13 && text != '' ? {text, type} : null,
- [],
+ [type],
undefined,
() => setText(''),
[setText],
);
Note how the current type is now listed as a dependency for the handler function so that that function is correctly memoized.
Summary
And again, that's it: a fairly small set of changes to make the app a little more useful. But there's more!
Next, we will build a yet more complex 'Todo' app, complete with persistence, a schema, and metrics. Please continue to the Todo App v3 (persistence) demo.