Skip to content

Commit 3b47237

Browse files
committed
Add tiny utility to control forms with MobX
1 parent 8e62e0b commit 3b47237

13 files changed

+191
-146
lines changed

.prettierrc.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
singleQuote: true,
3+
printWidth: 100
4+
};

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This project is a boilerplate for a quickstart development React applications. [
77
- [MobX](https://github.com/mobxjs/mobx) - Simple, scalable state management
88
- [mobx-log](https://github.com/kubk/mobx-log) - Logger for MobX, works only in dev mode
99
- [React Router](https://github.com/ReactTraining/react-router) - The most popular routing library for React
10+
- A [tiny utility](src/store/mobx-form.ts) to control forms with MobX
1011
- Prettier
1112

1213
The development process is exactly the same as you will do with Create React App.

package-lock.json

+20-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"classnames": "^2.3.1",
1616
"gh-pages": "^3.2.3",
1717
"mobx": "^6.3.3",
18-
"mobx-log": "^0.4.0",
18+
"mobx-log": "^0.8.6",
1919
"mobx-react-lite": "^3.2.1",
2020
"mobx-utils": "^6.0.4",
2121
"nanoid": "^3.1.25",
@@ -34,7 +34,6 @@
3434
"typescript": "^4.4.2"
3535
},
3636
"scripts": {
37-
"cs": "./node_modules/.bin/prettier src/**/*.{ts,tsx,js,jsx,scss} --write --single-quote --jsx-single-quote",
3837
"predeploy": "npm run build",
3938
"deploy": "gh-pages -d build",
4039
"start": "react-scripts start",

src/components/assignee-selector/assignee-selector.tsx

+13-21
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,19 @@ import { observer } from 'mobx-react-lite';
44

55
type Props = {
66
value: string | number;
7-
onSelect: (userId: string | null) => void;
7+
onChange: (value: { currentTarget: { value: string } }) => void;
88
users: User[];
99
};
1010

11-
export const AssigneeSelector = observer(
12-
({ onSelect, users, value }: Props) => {
13-
return (
14-
<select
15-
value={value}
16-
onChange={(e) => {
17-
const userId = e.target.value || null;
18-
onSelect(userId);
19-
}}
20-
>
21-
<option value=''>No assignee</option>
22-
{users.map((user, i) => (
23-
<option key={i} value={user.id}>
24-
{user.name}
25-
</option>
26-
))}
27-
</select>
28-
);
29-
}
30-
);
11+
export const AssigneeSelector = observer(({ onChange, users, value }: Props) => {
12+
return (
13+
<select value={value} onChange={onChange}>
14+
<option value="">No assignee</option>
15+
{users.map((user, i) => (
16+
<option key={i} value={user.id}>
17+
{user.name}
18+
</option>
19+
))}
20+
</select>
21+
);
22+
});

src/components/tasks/tasks.tsx

+45-41
Original file line numberDiff line numberDiff line change
@@ -43,50 +43,54 @@ export const Tasks = observer(() => {
4343

4444
{!taskStore.usersLoading && (
4545
<TransitionGroup className={styles.tasks}>
46-
{taskStore.tasks.map((task, i) => (
47-
<CSSTransition key={task.id} timeout={300} classNames={'item'}>
48-
<div
49-
key={i}
50-
className={cn(styles.task, { [styles.done]: task.isDone })}
51-
>
52-
<span className={styles.taskInfo}>
53-
<CheckCircle
54-
onClick={() => taskStore.toggleDone(task.id)}
55-
className={styles.icon}
56-
fill={task.isDone ? 'var(--c-teal)' : 'var(--c-black)'}
57-
/>
46+
{taskStore.tasks.map((task, i) => {
47+
const { form } = task;
48+
return (
49+
<CSSTransition key={task.id} timeout={300} classNames={'item'}>
50+
<div
51+
key={i}
52+
className={cn(styles.task, {
53+
[styles.done]: form.isDone.checked,
54+
})}
55+
>
56+
<span className={styles.taskInfo}>
57+
<CheckCircle
58+
onClick={form.isDone.toggle}
59+
className={styles.icon}
60+
fill={form.isDone.checked ? 'var(--c-teal)' : 'var(--c-black)'}
61+
/>
5862

59-
<input
60-
className={styles.input}
61-
value={task.title}
62-
ref={(input) => {
63-
if (i === 0) {
64-
rememberElement(input);
65-
}
66-
}}
67-
placeholder='Type in the title of the task!'
68-
readOnly={task.isDone}
69-
onChange={(e) => {
70-
taskStore.editTask(task.id, 'title', e.target.value);
71-
}}
72-
/>
73-
</span>
63+
<input
64+
className={styles.input}
65+
ref={(input) => {
66+
if (i === 0) {
67+
rememberElement(input);
68+
}
69+
}}
70+
placeholder="Type in the title of the task!"
71+
readOnly={form.isDone.checked}
72+
{...form.title.toInput}
73+
/>
74+
</span>
7475

75-
<AssigneeSelector
76-
value={task.userId || ''}
77-
onSelect={(userId) => taskStore.assign(task.id, userId)}
78-
users={taskStore.users}
79-
/>
76+
<AssigneeSelector
77+
users={taskStore.users.map((user) => ({
78+
id: user.id,
79+
name: user.form.name.value,
80+
}))}
81+
{...form.userId.toInput}
82+
/>
8083

81-
<img
82-
src={trash}
83-
alt={'remove task'}
84-
className={styles.trash}
85-
onClick={() => taskStore.removeTask(task.id)}
86-
/>
87-
</div>
88-
</CSSTransition>
89-
))}
84+
<img
85+
src={trash}
86+
alt={'remove task'}
87+
className={styles.trash}
88+
onClick={() => taskStore.removeTask(task.id)}
89+
/>
90+
</div>
91+
</CSSTransition>
92+
);
93+
})}
9094
</TransitionGroup>
9195
)}
9296
</div>

src/components/users/users.tsx

+3-8
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const Users = observer(() => {
4242
{!taskStore.usersLoading && (
4343
<TransitionGroup className={styles.users}>
4444
{taskStore.usersWithTasks.map((user, i) => (
45-
<CSSTransition key={user.id} timeout={300} classNames='item'>
45+
<CSSTransition key={user.id} timeout={300} classNames="item">
4646
<div className={styles.user} key={i}>
4747
<input
4848
ref={(input) => {
@@ -51,16 +51,11 @@ export const Users = observer(() => {
5151
}
5252
}}
5353
className={styles.input}
54-
value={user.name}
54+
{...user.form.name.toInput}
5555
placeholder="Type in user's name!"
56-
onChange={(event) => {
57-
taskStore.editUser(user.id, 'name', event.target.value);
58-
}}
5956
/>
6057

61-
<p className={styles.taskCompleted}>
62-
Completed {user.taskCompleted}
63-
</p>
58+
<p className={styles.taskCompleted}>Completed {user.taskCompleted}</p>
6459
<p className={styles.taskCount}>Total {user.taskTotal}</p>
6560

6661
<img

src/css/index.css

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
--shadow-depth-3: 0 6px 24px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.08);
1717
}
1818

19-
2019
body {
2120
margin: 0;
2221
padding: 0;

src/icons/check-circle.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import React, { SVGAttributes } from 'react';
22

33
export const CheckCircle = (props: SVGAttributes<SVGElement>) => {
44
return (
5-
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' {...props}>
6-
<path d='M11.48,23.5h0a11.5,11.5,0,0,1,0-23h0a11.38,11.38,0,0,1,4.67,1,1,1,0,0,1-.85,1.91,9.31,9.31,0,0,0-3.82-.81h0a9.41,9.41,0,0,0,0,18.82h0A9.4,9.4,0,0,0,20.87,12V11A1,1,0,1,1,23,11v1A11.49,11.49,0,0,1,11.48,23.5Z' />
7-
<path d='M11.48,15.14a1,1,0,0,1-.74-.31L7.61,11.7a1,1,0,0,1,1.48-1.48l2.39,2.4L22.22,1.86a1,1,0,0,1,1.48,1.48L12.22,14.84A1,1,0,0,1,11.48,15.14Z' />
5+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
6+
<path d="M11.48,23.5h0a11.5,11.5,0,0,1,0-23h0a11.38,11.38,0,0,1,4.67,1,1,1,0,0,1-.85,1.91,9.31,9.31,0,0,0-3.82-.81h0a9.41,9.41,0,0,0,0,18.82h0A9.4,9.4,0,0,0,20.87,12V11A1,1,0,1,1,23,11v1A11.49,11.49,0,0,1,11.48,23.5Z" />
7+
<path d="M11.48,15.14a1,1,0,0,1-.74-.31L7.61,11.7a1,1,0,0,1,1.48-1.48l2.39,2.4L22.22,1.86a1,1,0,0,1,1.48,1.48L12.22,14.84A1,1,0,0,1,11.48,15.14Z" />
88
</svg>
99
);
1010
};

src/store/mobx-form.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { makeAutoObservable } from 'mobx';
2+
3+
export class TextInput {
4+
constructor(public value = '') {
5+
makeAutoObservable(this);
6+
}
7+
8+
private onChange = (e: { currentTarget: { value: string } }) => {
9+
this.value = e.currentTarget.value;
10+
};
11+
12+
setValue = (value: string) => {
13+
this.value = value;
14+
};
15+
16+
get toInput() {
17+
return { value: this.value, onChange: this.onChange };
18+
}
19+
}
20+
21+
export class CheckboxInput {
22+
constructor(public checked: boolean) {
23+
makeAutoObservable(this);
24+
}
25+
26+
toggle = () => {
27+
this.checked = !this.checked;
28+
};
29+
30+
setValue = (value: boolean) => {
31+
this.checked = value;
32+
};
33+
34+
onChange = (event: { currentTarget: { checked: boolean } }) => {
35+
this.checked = event.currentTarget.checked;
36+
};
37+
38+
get toInput() {
39+
return { checked: this.checked, onChange: this.onChange };
40+
}
41+
}

src/store/stores.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { configureMakeLoggable } from 'mobx-log';
44

55
configureMakeLoggable({
66
storeConsoleAccess: true,
7-
})
7+
});
88

99
export const stores = {
1010
taskStore: new TaskStore(new TaskApi()),

src/store/task-store.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,23 @@ describe('TaskStore', () => {
4040

4141
await when(() => !taskStore.usersLoading && !taskStore.tasksLoading);
4242

43-
expect(taskStore.usersWithTasks[0].name).toBe('John Doe');
43+
expect(taskStore.usersWithTasks[0].form.name.value).toBe('John Doe');
4444
expect(taskStore.usersWithTasks[0].taskTotal).toBe(2);
4545
expect(taskStore.usersWithTasks[0].taskCompleted).toBe(1);
4646

47-
expect(taskStore.usersWithTasks[1].name).toBe('Jane Snow');
47+
expect(taskStore.usersWithTasks[1].form.name.value).toBe('Jane Snow');
4848
expect(taskStore.usersWithTasks[1].taskTotal).toBe(1);
4949
expect(taskStore.usersWithTasks[1].taskCompleted).toBe(0);
5050

51-
taskStore.assign('1', '2');
51+
taskStore.tasks.find((task) => task.id === '1')?.form.userId.setValue('2');
5252
// Completed task 1 removed from user 1
5353
expect(taskStore.usersWithTasks[0].taskTotal).toBe(1);
5454
expect(taskStore.usersWithTasks[0].taskCompleted).toBe(0);
5555
// Task 1 added to added to user 2 as completed
5656
expect(taskStore.usersWithTasks[1].taskTotal).toBe(2);
5757
expect(taskStore.usersWithTasks[1].taskCompleted).toBe(1);
5858

59-
taskStore.toggleDone('1');
59+
taskStore.tasks.find((task) => task.id === '1')?.form.isDone.toggle();
6060
// User 2 now has no completed tasks
6161
expect(taskStore.usersWithTasks[1].taskCompleted).toBe(0);
6262
});

0 commit comments

Comments
 (0)