Skip to main content

Todo List Example

Code

store.ts

examples/todoList/store.ts
import { nodeType, TNode } from "mobx-bonsai"

const todoType = "todoSample/Todo"

export type Todo = TNode<
typeof todoType,
{
id: string
text: string
done: boolean
}
>

export const TTodo = nodeType<Todo>(todoType)
.withKey("id")
.defaults({
done: () => false,
})
.settersFor("done", "text")

const todoListType = "todoSample/TodoList"

export type TodoList = TNode<typeof todoListType, { todos: Todo[] }>

export const TTodoList = nodeType<TodoList>(todoListType)
.defaults({
todos: () => [],
})
.getters({
getPending() {
return this.todos.filter((t) => !t.done)
},
getDone() {
return this.todos.filter((t) => t.done)
},
})
.actions({
add(todo: Todo) {
this.todos.push(todo)
},

remove(todo: Todo) {
const index = this.todos.indexOf(todo)
if (index >= 0) {
this.todos.splice(index, 1)
}
},
})

export function createDefaultTodoList(): TodoList {
// the parameter is the initial data for the model
return TTodoList({
todos: [
// we could just use the objects here directly, but then we'd need to
// generate the ids and add the [nodeType] property ourselves
TTodo({ text: "make mobx-bonsai awesome!" }),
TTodo({ text: "spread the word" }),
TTodo({ text: "buy some milk", done: true }),
],
})
}

export function createRootStore(): TodoList {
const rootStore = createDefaultTodoList()

// we can also connect the store to the redux dev tools
// const remotedev = require("remotedev")
// const connection = remotedev.connectViaExtension({
// name: "Todo List Example",
// })

// connectReduxDevTools(remotedev, connection, rootStore)

return rootStore
}

app.tsx

examples/todoList/app.tsx
import { observer } from "mobx-react"
import { useState } from "react"
import { LogsView } from "./logs"
import { createRootStore, Todo, TodoList, TTodo, TTodoList } from "./store"

// we use mobx-react to connect to the data, as it is usual in mobx
// this library is framework agnostic, so it can work anywhere mobx can work
// (even outside of a UI)

export const App = observer(() => {
const [rootStore] = useState(() => createRootStore())

return (
<>
<TodoListView list={rootStore} />
<br />
<LogsView rootStore={rootStore} />
</>
)
})

export const TodoListView = observer(({ list }: { list: TodoList }) => {
const [newTodo, setNewTodo] = useState("")

const renderTodo = (todo: Todo) => (
<TodoView
key={todo.id}
done={todo.done}
text={todo.text}
onClick={() => {
TTodo.setDone(todo, !todo.done)
}}
onRemove={() => {
TTodoList.remove(list, todo)
}}
/>
)

const pendingTodos = TTodoList.getPending(list)
const doneTodos = TTodoList.getDone(list)

return (
<div>
{pendingTodos.length > 0 && (
<>
<h5>TODO</h5>
{pendingTodos.map((t) => renderTodo(t))}
</>
)}

{doneTodos.length > 0 && (
<>
<h5>DONE</h5>
{doneTodos.map((t) => renderTodo(t))}
</>
)}
<br />
<input
value={newTodo}
onChange={(ev) => {
setNewTodo(ev.target.value || "")
}}
placeholder="I will..."
/>
<button
type="button"
onClick={() => {
TTodoList.add(list, TTodo({ text: newTodo, done: false }))
setNewTodo("")
}}
>
Add todo
</button>
</div>
)
})

function TodoView({
done,
text,
onClick,
onRemove,
}: {
done: boolean
text: string
onClick: () => void
onRemove: () => void
}) {
return (
<div style={{ cursor: "pointer" }}>
<span
onClick={onClick}
style={{
textDecoration: done ? "line-through" : "inherit",
}}
>
<span
style={{
display: "inline-block",
width: "1.5rem",
textAlign: "center",
marginRight: 8,
}}
>
{done ? "✔️" : "👀"}
</span>
{text}
{}
</span>
<span onClick={onRemove} style={{ marginLeft: 16 }}>

</span>
</div>
)
}

logs.tsx

examples/todoList/logs.tsx
import { getSnapshot } from "mobx-bonsai"
import { observer } from "mobx-react"
import React from "react"
import { TodoList } from "./store"

export const LogsView = observer((props: { rootStore: TodoList }) => {
// we can convert any model (or part of it) into a plain JS structure
// with it we can:
// - serialize to later deserialize it
// - pass it to non mobx-friendly components
// snapshots respect immutability, so if a subobject is changed
// its refrence will be kept
const rootStoreSnapshot = getSnapshot(props.rootStore)

return (
<>
<PreSection title="Generated immutable snapshot">
{JSON.stringify(rootStoreSnapshot, null, 2)}
</PreSection>
</>
)
})

function PreSection(props: { title: string; children: React.ReactNode }) {
return (
<>
<h5>{props.title}</h5>
<pre style={{ fontSize: 10, whiteSpace: "pre-wrap" }}>{props.children}</pre>
</>
)
}