Todo App v1 (the basics)
In this demo, we build a minimum viable 'Todo' app. It uses React and a simple Store
to let people add new todos and then mark them as done.
Initialization
First, we pull in React, ReactDOM, and TinyBase:
<script src="/umd/react.production.min.js"></script>
<script src="/umd/react-dom.production.min.js"></script>
<script src="/umd/tinybase/index.js"></script>
<script src="/umd/tinybase/ui-react/index.js"></script>
<script src="/umd/tinybase/ui-react-inspector/index.js"></script>
We're using the Inspector
component for the purposes of seeing how the data is structured.
We import the functions and components we need:
const {createStore} = TinyBase;
const {
CellView,
Provider,
TableView,
useAddRowCallback,
useCell,
useCreateStore,
useSetCellCallback,
} = TinyBaseUiReact;
const {useCallback, useState} = React;
const {Inspector} = TinyBaseUiReactInspector;
In this demo, we'll start with some sample data. We'll have a single Table
, called todos
, that has three Row
entries, each representing one todo:
const INITIAL_TODOS = {
todos: {
0: {text: 'Clean the floor'},
1: {text: 'Install TinyBase'},
2: {text: 'Book holiday'},
},
};
The Top-Level App
Component
We have a top-level App
component, in which we create our Store
object with the sample data, and render the parts of the app. We use the useCreateStore
hook, since it provides memoization in case the component is rendered more than once.
We could drill the Store
object as a React prop down to all the components, but for clarity we wrap our app with the Provider
component, which then makes it available throughout our app as the default Store
object:
const App = () => {
const store = useCreateStore(() => createStore().setTables(INITIAL_TODOS));
return (
<Provider store={store}>
<Title />
<NewTodo />
<Todos />
<Inspector />
</Provider>
);
};
We also added the Inspector
component at the end there so you can inspect what is going on with the data during this demo. Simply click the TinyBase logo in the corner.
The app only has three components: Title
, NewTodo
(a form to enter a new todo), and Todos
(a list of all of the todos).
We use LESS to create a grid layout and some defaults for the app's styling:
@accentColor: #d81b60;
@spacing: 0.5rem;
@border: 1px solid #ccc;
@font-face {
font-family: Inter;
src: url(https://tinybase.org/fonts/inter.woff2) format('woff2');
}
body {
display: grid;
grid-template-columns: 35% minmax(0, 1fr);
grid-template-rows: auto 1fr;
box-sizing: border-box;
font-family: Inter, sans-serif;
letter-spacing: -0.04rem;
grid-gap: @spacing * 2 @spacing;
margin: 0;
min-height: 100vh;
padding: @spacing * 2;
* {
box-sizing: border-box;
outline-color: @accentColor;
}
}
When the window loads, we render the App
component to start the app:
window.addEventListener('load', () =>
ReactDOM.createRoot(document.body).render(<App />),
);
Let's look at each of three components that make up the app.
The Title
Component
There's not much to say about this component for now. It's just the title for the app. We'll do more with this later!
const Title = () => 'Todos';
The NewTodo
Component
This component lets the user add a new todo. It's a standard managed input
element that keeps the text
of the input box in the component's state, and also listens to key presses:
const NewTodo = () => {
const [text, setText] = useState('');
const handleChange = useCallback(({target: {value}}) => setText(value), []);
const handleKeyDown = useAddRowCallback(
'todos',
({which, target: {value: text}}) =>
which == 13 && text != '' ? {text} : null,
[],
undefined,
() => setText(''),
[setText],
);
return (
<input
id="newTodo"
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="New Todo"
value={text}
/>
);
};
The useAddRowCallback
hook creates us a callback that is run whenever the user presses a key. This lets us check if the key pressed was the Return key, and if there's some text in the input box. If so, it adds a new todo Row
to the default Store
object and then resets the form.
As an aside, it may seem like the useAddRowCallback
hook has a lot of parameters, since we are providing two functions, each with their own lists of dependencies (which are used to memoize them). The first parameter is the Id
of the table to which we are adding a row. The second is the handler that takes the event and produces the new row to be added and the third parameter is the list of dependencies for that function (of which there are none). The fourth parameter would allow us to explicitly specify which Store
object to use, but undefined
gives us the app-wide default. The fifth parameter is a 'then' callback which will run after the row has been added (which is where we can reset the input field) and the sixth and final parameter is a list of dependencies for that (of which there is only one, the function used to do that setting of the input field text).
The input box also needs styling:
#newTodo {
border: @border;
display: block;
font: inherit;
letter-spacing: inherit;
padding: @spacing;
width: 100%;
}
The Todos
Component
To get the list of todos, we build a very simple component, comprising the TableView
component right out of the box. We pass props to that component to indicate that we will be rendering each Row
of the todos
Table
. Since we are not providing a store
prop to the TableView, it will also use the default one from the Provider
component that we wrapped the whole app in:
const Todos = () => (
<ul id="todos">
<TableView tableId="todos" rowComponent={Todo} />
</ul>
);
The final rowComponent
prop for the TableView
component allows us to specify how each Row
is rendered. We will have our own Todo
component (described below) to do this.
Finally, we style this part of the app and position it in the grid:
#todos {
grid-column: 2;
margin: 0;
padding: 0;
}
The Todo
Component
This simple Todo
component renders a simple div
containing the todo's text
that toggles the done
flag when clicked. Each Row
has two fields: the todo's text
and an optional boolean indicating whether it is done
that we get using the useCell
hook.
We also create a simple handleClick
callback that negates the done
flag when the text is clicked:
const Todo = (props) => (
<li className="todo">
<TodoText {...props} />
</li>
);
const TodoText = ({tableId, rowId}) => {
const done = useCell(tableId, rowId, 'done');
const className = 'text' + (done ? ' done' : '');
const handleClick = useSetCellCallback(tableId, rowId, 'done', () => !done, [
done,
]);
return (
<span className={className} onClick={handleClick}>
<CellView tableId={tableId} rowId={rowId} cellId="text" />
</span>
);
};
NB: The parent TableView
component passes its Store
object as a prop to each of its rowComponent
children (such as this TodoText
). We could have used that prop here, but instead we're using the useSetCellCallback
hook that will automatically get it from the Provider
component.
Finally, we style each of these todos, and use the done
CSS class to strike through those that are completed:
#todos .todo {
background: #fff;
border: @border;
display: flex;
margin-bottom: @spacing;
padding: @spacing;
.text {
cursor: pointer;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
white-space: nowrap;
&::before {
content: '\1F7E0';
padding: 0 0.5rem 0 0.25rem;
}
&.done {
color: #ccc;
&::before {
content: '\2705';
}
}
}
}
Summary
That's it: a simple Todo app made from a handful of tiny components, and less than 100 lines of generously-formatted code.
Next, we will build a more complex viable 'Todo' app. Please continue to the Todo App v2 (indexes) demo.