Skip to content

Commit

Permalink
docs: todomvc
Browse files Browse the repository at this point in the history
  • Loading branch information
acrazing committed Nov 1, 2024
1 parent 0e62683 commit f2bdf0e
Show file tree
Hide file tree
Showing 24 changed files with 308 additions and 180 deletions.
9 changes: 7 additions & 2 deletions examples/TodoMVC/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* @author junbao <[email protected]>
*/

import { useSelector } from 'amos/react';
import { hydrate } from 'amos';
import { useQuery, useSelector } from 'amos/react';
import { memo } from 'react';
import { Filter } from './components/Filter';
import { Header } from './components/Header';
Expand All @@ -13,11 +14,15 @@ import { currentUserIdBox } from './store/user.boxes';

export const App = memo(() => {
const userId = useSelector(currentUserIdBox);
const [, gate] = useQuery(hydrate([currentUserIdBox]));
if (gate.isPending()) {
return <div>Loading...</div>;
}
if (!userId) {
return <SignIn />;
}
return (
<div>
<div className="app">
<Header />
<Filter />
<TodoList />
Expand Down
42 changes: 30 additions & 12 deletions examples/TodoMVC/src/components/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,43 @@ export const Filter = memo(() => {
const dispatch = useDispatch();
const status = useSelector(todoStatusFilterBox);
const [input, setInput] = useState('');
const handleAdd = () => {
const title = input.trim();
if (!title) {
return;
}
dispatch(addTodo(title));
};
return (
<div>
<div>
<input placeholder="What to do?" value={input} onChange={(e) => setInput(e.target.value)} />{' '}
<button disabled={!input.trim()} onClick={() => dispatch(addTodo(input.trim()))}>
<div className="filter">
<div className="filter-input">
<input
placeholder="What to do?"
value={input}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleAdd();
}
}}
onChange={(e) => setInput(e.target.value)}
/>{' '}
<button disabled={!input.trim()} onClick={handleAdd}>
Add
</button>
</div>
<div>
<span>Filter: </span>
{[TodoStatusFilter.All, TodoStatusFilter.New, TodoStatusFilter.Completed].map((s) => (
<input
type="radio"
name="todo_status"
value={s}
checked={s === status}
onChange={() => dispatch(todoStatusFilterBox.setState(s))}
key={s}
/>
<label key={s}>
<input
type="radio"
name="todo_status"
value={s}
checked={s === status}
onChange={() => dispatch(todoStatusFilterBox.setState(s))}
/>
<span>{s}</span>
</label>
))}
</div>
</div>
Expand Down
19 changes: 11 additions & 8 deletions examples/TodoMVC/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,20 @@ import { selectCurrentUser } from '../store/user.selectors';
export const Header = memo(() => {
const dispatch = useDispatch();
const user = useSelector(selectCurrentUser());
const handleSignOut = () => {
const keepData = confirm('You will be signed out, do you want to keep your data?');
dispatch(signOut(keepData));
};
if (!user.isInitial()) {
if (user.isInitial()) {
return null;
}
return (
<div>
<span>Welcome, {user.name}. </span>
<button onClick={handleSignOut}>Sign out</button>
<div className="header flex">
<span className="expand" />
<span>Welcome,</span>
&nbsp;
<strong>{user.name}</strong>
<span>.</span>
&nbsp;
<button onClick={() => dispatch(signOut(false))}>Sign out</button>
&nbsp;
<button onClick={() => dispatch(signOut(true))}>Switch user</button>
</div>
);
});
5 changes: 3 additions & 2 deletions examples/TodoMVC/src/components/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ export const SignIn = memo(() => {
const [input, setInput] = useState('');
const dispatch = useDispatch();
return (
<div>
<input placeholder="Your name" value={input} onChange={(e) => setInput(e.target.value)} />{' '}
<div className="sign-in">
<input placeholder="Your name" value={input} onChange={(e) => setInput(e.target.value)} />
&nbsp;
<button disabled={!input.trim()} onClick={() => dispatch(signIn(input))}>
Sign In
</button>
Expand Down
7 changes: 5 additions & 2 deletions examples/TodoMVC/src/components/TodoItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ export const TodoItem = memo(({ id }: { id: number }) => {
const dispatch = useDispatch();
const todo = useSelector(todoMapBox.getItem(id));
return (
<div>
<span>{todo.title}</span>
<div className="todo flex">
<strong>{todo.title}</strong>
&nbsp;
<span className="expand" />
{todo.completed ? null : (
<button onClick={() => dispatch(completeTodo(id))}>Mark as completed</button>
)}
&nbsp;
<button onClick={() => dispatch(deleteTodo(id))}>Delete</button>
</div>
);
Expand Down
6 changes: 3 additions & 3 deletions examples/TodoMVC/src/components/TodoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
* @author junbao <[email protected]>
*/

import { useQuery } from 'amos/react';
import { useSelector } from 'amos/react';
import { memo } from 'react';
import { getTodoList } from '../store/todo.actions';
import { selectVisibleUserTodoList } from '../store/todo.selectors';
import { TodoItem } from './TodoItem';

export const TodoList = memo(() => {
const [todoList] = useQuery(getTodoList());
const todoList = useSelector(selectVisibleUserTodoList());
return (
<div>
{todoList.map((id) => (
Expand Down
61 changes: 55 additions & 6 deletions examples/TodoMVC/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
line-height: 1.5;
font-weight: 400;

--black: #242424;
--white: rgba(255, 255, 255, 0.87);
--back: #000000;
--front: #ffffff;

color-scheme: light dark;
color: var(--white);
background-color: var(--black);
color: var(--front);
background-color: var(--back);

font-synthesis: none;
text-rendering: optimizeLegibility;
Expand All @@ -18,11 +18,60 @@

@media (prefers-color-scheme: light) {
:root {
--white: #ffffff;
--black: #213547;
--back: #ffffff;
--front: #000000;
}
}

body {
margin: 0;
}

#root {
display: flex;
align-items: center;
justify-content: center;
width: 100dvw;
height: 100dvh;
}

.app {
width: 100%;
height: 100%;
max-width: 640px;
max-height: 800px;
border: 1px solid var(--front);
box-sizing: border-box;
padding: 12px;
display: flex;
flex-direction: column;
align-items: stretch;
}

.flex {
display: flex;
}

.expand {
flex: 1;
}

.filter {
align-self: center;
padding: 20px 0;
}

.filter-input {
display: flex;
justify-content: space-around;
padding-bottom: 12px;
}

button {
cursor: pointer;
}

.todo {
padding: 8px 12px;
border-bottom: 1px solid var(--front);
}
19 changes: 17 additions & 2 deletions examples/TodoMVC/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,25 @@ import { createRoot } from 'react-dom/client';
import { App } from './App';
import './index.css';
import { hashCode } from './store/todo.boxes';
import { currentUserIdBox, userMapBox } from './store/user.boxes';

const userId = hashCode('Amos');

const store = createStore(
{ preloadedState: { 'users.currentUserId': hashCode('Amos') } },
withPersist({ storage: new IDBStorage('todo', 'todo') }),
{
name: 'Amos - TodoMVC',
devtools: true,
preloadedState: {
[currentUserIdBox.key]: userId,
[userMapBox.key]: {
[userId]: { id: userId, name: 'Amos' },
},
},
},
withPersist({
storage: new IDBStorage('todo', 'todo'),
includes: () => true,
}),
);

createRoot(document.getElementById('root')!).render(
Expand Down
4 changes: 2 additions & 2 deletions examples/TodoMVC/src/store/todo.selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @author junbao <[email protected]>
*/

import { isSameList, selector } from 'amos';
import { selector } from 'amos';
import { todoMapBox, TodoStatusFilter, todoStatusFilterBox, userTodoListBox } from './todo.boxes';
import { currentUserIdBox } from './user.boxes';

Expand All @@ -24,6 +24,6 @@ export const selectVisibleUserTodoList = selector(
});
},
{
equal: isSameList,
cache: true,
},
);
2 changes: 1 addition & 1 deletion examples/TodoMVC/src/store/user.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const signOut = action(async (dispatch, select, keepData: boolean) => {
// do real sign out
await doAsync(void 0);
const userId = select(currentUserIdBox);
dispatch(currentUserIdBox.setState());
dispatch(currentUserIdBox.setState(0));
// notify boxes to clean data
dispatch(signOutSignal({ userId, keepData }));
});
2 changes: 1 addition & 1 deletion packages/amos-boxes/src/listMapBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const ListMapBox = MapBox.extends<ListMapBox<any>>({
export function listMapBox<K, E>(
key: string,
inferKey: K,
defaultElement: E,
inferElement: E,
): ListMapBox<ListMap<IDOf<K>, List<E>>> {
return new ListMapBox(
key,
Expand Down
4 changes: 2 additions & 2 deletions packages/amos-core/src/enhancers/withCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Box } from '../box';
import { Selector } from '../selector';
import { StoreEnhancer } from '../store';
import { Selectable } from '../types';
import { resolveCacheKey } from '../utils';
import { computeCacheKey } from '../utils';

export type SelectEntry<R = any> = Entry<Selectable<R>, R>;

Expand Down Expand Up @@ -44,7 +44,7 @@ export const withCache: () => StoreEnhancer = () => {
}
}
// should check the cache now
const key = resolveCacheKey(store.select, s, void 0);
const key = computeCacheKey(store.select, s, void 0);
let cache = cacheMap.get(key);
if (cache && cache[0] !== s.id) {
cacheMap.delete(key);
Expand Down
4 changes: 2 additions & 2 deletions packages/amos-core/src/enhancers/withConcurrent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { isAmosObject, override } from 'amos-utils';
import { Action } from '../action';
import { StoreEnhancer } from '../store';
import { resolveCacheKey } from '../utils';
import { computeCacheKey } from '../utils';

export function withConcurrent(): StoreEnhancer {
return (next) => (options) => {
Expand All @@ -17,7 +17,7 @@ export function withConcurrent(): StoreEnhancer {
if (!isAmosObject<Action>(d, 'action') || d.conflictPolicy !== 'leading') {
return dispatch(d);
}
const key = resolveCacheKey(store.select, d, d.conflictKey);
const key = computeCacheKey(store.select, d, d.conflictKey);
if (pending.has(key)) {
return pending.get(key)!;
}
Expand Down
14 changes: 7 additions & 7 deletions packages/amos-core/src/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { countBox, Rick, selectDouble, selectDoubleCount, UserRecord } from 'amos-testing';
import { createStore } from './store';
import { Select } from './types';
import { resolveCacheKey, stringify } from './utils';
import { computeCacheKey, stringify } from './utils';

describe('core utils', () => {
it('should stringify params', () => {
Expand All @@ -32,12 +32,12 @@ describe('core utils', () => {
const store = createStore();
store.dispatch(countBox.setState(1));
expect([
resolveCacheKey(store.select, selectDouble(2), void 0),
resolveCacheKey(store.select, selectDouble(2), countBox),
resolveCacheKey(store.select, selectDouble(2), selectDoubleCount()),
resolveCacheKey(store.select, selectDouble(2), [countBox, selectDoubleCount()]),
resolveCacheKey(store.select, selectDouble(2), (select: Select) => select(countBox)),
resolveCacheKey(store.select, selectDouble(2), (select: Select) => [
computeCacheKey(store.select, selectDouble(2), void 0),
computeCacheKey(store.select, selectDouble(2), countBox),
computeCacheKey(store.select, selectDouble(2), selectDoubleCount()),
computeCacheKey(store.select, selectDouble(2), [countBox, selectDoubleCount()]),
computeCacheKey(store.select, selectDouble(2), (select: Select) => select(countBox)),
computeCacheKey(store.select, selectDouble(2), (select: Select) => [
select(countBox),
select(selectDoubleCount()),
]),
Expand Down
2 changes: 1 addition & 1 deletion packages/amos-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function stringify(data: any): string {
return data.toString();
}

export function resolveCacheKey(
export function computeCacheKey(
select: Select,
v: Action | Selector,
key: CacheOptions<any> | undefined,
Expand Down
3 changes: 2 additions & 1 deletion packages/amos-persist/src/enhancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { Box, type Selectable, type Selector, StoreEnhancer } from 'amos-core';
import { append, isAmosObject, override, PartialRequired } from 'amos-utils';
import { append, isAmosObject, once, override, PartialRequired } from 'amos-utils';
import { createHydrate } from './hydrate';
import { createPersist } from './persist';
import { persistBox, type PersistState } from './state';
Expand All @@ -28,6 +28,7 @@ export function withPersist(options: PartialRequired<PersistOptions, 'storage'>)
const state: PersistState = {
...finalOptions,
selecting: false,
init: once(async () => options.storage.init?.()),
snapshot: new Map(),
getInitial: (box) => {
if (initial.has(box.key)) {
Expand Down
1 change: 1 addition & 0 deletions packages/amos-persist/src/hydrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { fromKey, toKey, toPersistOptions } from './utils';
export const createHydrate = (store: Store, finalOptions: PersistOptions) => {
return nextSerialTicker<PersistKey<any>, void>(async (items) => {
const state = store.select(persistBox)!;
await state.init();

function migrate(box: Box, v: number, id: string, d: any) {
const opts = toPersistOptions(box);
Expand Down
Loading

0 comments on commit f2bdf0e

Please sign in to comment.