jac-client 0.2.2__py3-none-any.whl → 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jac_client/examples/all-in-one/assets/workers/worker.py +5 -0
- jac_client/tests/conftest.py +281 -0
- jac_client/tests/test_cli.py +755 -0
- jac_client/tests/test_it.py +347 -67
- {jac_client-0.2.2.dist-info → jac_client-0.2.4.dist-info}/METADATA +30 -24
- jac_client-0.2.4.dist-info/RECORD +10 -0
- {jac_client-0.2.2.dist-info → jac_client-0.2.4.dist-info}/WHEEL +2 -1
- jac_client-0.2.4.dist-info/entry_points.txt +4 -0
- jac_client-0.2.4.dist-info/top_level.txt +1 -0
- jac_client/docs/README.md +0 -689
- jac_client/docs/advanced-state.md +0 -1265
- jac_client/docs/asset-serving/intro.md +0 -209
- jac_client/docs/assets/pipe_line-v2.svg +0 -32
- jac_client/docs/assets/pipe_line.png +0 -0
- jac_client/docs/file-system/app.jac.md +0 -121
- jac_client/docs/file-system/backend-frontend.md +0 -217
- jac_client/docs/file-system/intro.md +0 -72
- jac_client/docs/file-system/nested-imports.md +0 -348
- jac_client/docs/guide-example/intro.md +0 -115
- jac_client/docs/guide-example/step-01-setup.md +0 -270
- jac_client/docs/guide-example/step-02-components.md +0 -416
- jac_client/docs/guide-example/step-03-styling.md +0 -478
- jac_client/docs/guide-example/step-04-todo-ui.md +0 -477
- jac_client/docs/guide-example/step-05-local-state.md +0 -530
- jac_client/docs/guide-example/step-06-events.md +0 -749
- jac_client/docs/guide-example/step-07-effects.md +0 -468
- jac_client/docs/guide-example/step-08-walkers.md +0 -534
- jac_client/docs/guide-example/step-09-authentication.md +0 -586
- jac_client/docs/guide-example/step-10-routing.md +0 -539
- jac_client/docs/guide-example/step-11-final.md +0 -963
- jac_client/docs/imports.md +0 -1141
- jac_client/docs/lifecycle-hooks.md +0 -773
- jac_client/docs/routing.md +0 -659
- jac_client/docs/styling/intro.md +0 -249
- jac_client/docs/styling/js-styling.md +0 -367
- jac_client/docs/styling/material-ui.md +0 -341
- jac_client/docs/styling/pure-css.md +0 -299
- jac_client/docs/styling/sass.md +0 -403
- jac_client/docs/styling/styled-components.md +0 -395
- jac_client/docs/styling/tailwind.md +0 -298
- jac_client/examples/all-in-one/.babelrc +0 -9
- jac_client/examples/all-in-one/README.md +0 -16
- jac_client/examples/all-in-one/app.jac +0 -426
- jac_client/examples/all-in-one/assets/burger.png +0 -0
- jac_client/examples/all-in-one/button.jac +0 -7
- jac_client/examples/all-in-one/components/button.jac +0 -7
- jac_client/examples/all-in-one/package.json +0 -29
- jac_client/examples/all-in-one/styles.css +0 -26
- jac_client/examples/all-in-one/vite.config.js +0 -28
- jac_client/examples/asset-serving/css-with-image/.babelrc +0 -9
- jac_client/examples/asset-serving/css-with-image/README.md +0 -91
- jac_client/examples/asset-serving/css-with-image/app.jac +0 -88
- jac_client/examples/asset-serving/css-with-image/assets/burger.png +0 -0
- jac_client/examples/asset-serving/css-with-image/package.json +0 -28
- jac_client/examples/asset-serving/css-with-image/styles.css +0 -26
- jac_client/examples/asset-serving/css-with-image/vite.config.js +0 -28
- jac_client/examples/asset-serving/image-asset/.babelrc +0 -9
- jac_client/examples/asset-serving/image-asset/README.md +0 -119
- jac_client/examples/asset-serving/image-asset/app.jac +0 -55
- jac_client/examples/asset-serving/image-asset/assets/burger.png +0 -0
- jac_client/examples/asset-serving/image-asset/package.json +0 -28
- jac_client/examples/asset-serving/image-asset/styles.css +0 -26
- jac_client/examples/asset-serving/image-asset/vite.config.js +0 -28
- jac_client/examples/asset-serving/import-alias/.babelrc +0 -9
- jac_client/examples/asset-serving/import-alias/README.md +0 -83
- jac_client/examples/asset-serving/import-alias/app.jac +0 -111
- jac_client/examples/asset-serving/import-alias/assets/burger.png +0 -0
- jac_client/examples/asset-serving/import-alias/package.json +0 -28
- jac_client/examples/asset-serving/import-alias/vite.config.js +0 -28
- jac_client/examples/basic/.babelrc +0 -9
- jac_client/examples/basic/README.md +0 -16
- jac_client/examples/basic/app.jac +0 -21
- jac_client/examples/basic/package.json +0 -27
- jac_client/examples/basic/vite.config.js +0 -27
- jac_client/examples/basic-auth/.babelrc +0 -9
- jac_client/examples/basic-auth/README.md +0 -16
- jac_client/examples/basic-auth/app.jac +0 -308
- jac_client/examples/basic-auth/package.json +0 -27
- jac_client/examples/basic-auth/vite.config.js +0 -27
- jac_client/examples/basic-auth-with-router/.babelrc +0 -9
- jac_client/examples/basic-auth-with-router/README.md +0 -60
- jac_client/examples/basic-auth-with-router/app.jac +0 -464
- jac_client/examples/basic-auth-with-router/package.json +0 -28
- jac_client/examples/basic-auth-with-router/vite.config.js +0 -27
- jac_client/examples/basic-full-stack/.babelrc +0 -9
- jac_client/examples/basic-full-stack/README.md +0 -18
- jac_client/examples/basic-full-stack/app.jac +0 -320
- jac_client/examples/basic-full-stack/package.json +0 -28
- jac_client/examples/basic-full-stack/vite.config.js +0 -27
- jac_client/examples/css-styling/js-styling/.babelrc +0 -9
- jac_client/examples/css-styling/js-styling/README.md +0 -183
- jac_client/examples/css-styling/js-styling/app.jac +0 -84
- jac_client/examples/css-styling/js-styling/package.json +0 -28
- jac_client/examples/css-styling/js-styling/styles.js +0 -100
- jac_client/examples/css-styling/js-styling/vite.config.js +0 -27
- jac_client/examples/css-styling/material-ui/.babelrc +0 -9
- jac_client/examples/css-styling/material-ui/README.md +0 -16
- jac_client/examples/css-styling/material-ui/app.jac +0 -122
- jac_client/examples/css-styling/material-ui/package.json +0 -32
- jac_client/examples/css-styling/material-ui/vite.config.js +0 -27
- jac_client/examples/css-styling/pure-css/.babelrc +0 -9
- jac_client/examples/css-styling/pure-css/README.md +0 -16
- jac_client/examples/css-styling/pure-css/app.jac +0 -64
- jac_client/examples/css-styling/pure-css/package.json +0 -28
- jac_client/examples/css-styling/pure-css/styles.css +0 -111
- jac_client/examples/css-styling/pure-css/vite.config.js +0 -27
- jac_client/examples/css-styling/sass-example/.babelrc +0 -9
- jac_client/examples/css-styling/sass-example/README.md +0 -16
- jac_client/examples/css-styling/sass-example/app.jac +0 -64
- jac_client/examples/css-styling/sass-example/package.json +0 -29
- jac_client/examples/css-styling/sass-example/styles.scss +0 -153
- jac_client/examples/css-styling/sass-example/vite.config.js +0 -27
- jac_client/examples/css-styling/styled-components/.babelrc +0 -9
- jac_client/examples/css-styling/styled-components/README.md +0 -16
- jac_client/examples/css-styling/styled-components/app.jac +0 -71
- jac_client/examples/css-styling/styled-components/package.json +0 -29
- jac_client/examples/css-styling/styled-components/styled.js +0 -90
- jac_client/examples/css-styling/styled-components/vite.config.js +0 -27
- jac_client/examples/css-styling/tailwind-example/.babelrc +0 -9
- jac_client/examples/css-styling/tailwind-example/README.md +0 -16
- jac_client/examples/css-styling/tailwind-example/app.jac +0 -63
- jac_client/examples/css-styling/tailwind-example/global.css +0 -1
- jac_client/examples/css-styling/tailwind-example/package.json +0 -30
- jac_client/examples/css-styling/tailwind-example/vite.config.js +0 -29
- jac_client/examples/full-stack-with-auth/.babelrc +0 -9
- jac_client/examples/full-stack-with-auth/README.md +0 -16
- jac_client/examples/full-stack-with-auth/app.jac +0 -722
- jac_client/examples/full-stack-with-auth/package.json +0 -28
- jac_client/examples/full-stack-with-auth/vite.config.js +0 -29
- jac_client/examples/little-x/app.jac +0 -724
- jac_client/examples/little-x/package.json +0 -23
- jac_client/examples/little-x/submit-button.jac +0 -8
- jac_client/examples/nested-folders/nested-advance/.babelrc +0 -9
- jac_client/examples/nested-folders/nested-advance/ButtonRoot.jac +0 -11
- jac_client/examples/nested-folders/nested-advance/README.md +0 -77
- jac_client/examples/nested-folders/nested-advance/app.jac +0 -35
- jac_client/examples/nested-folders/nested-advance/level1/ButtonSecondL.jac +0 -19
- jac_client/examples/nested-folders/nested-advance/level1/Card.jac +0 -43
- jac_client/examples/nested-folders/nested-advance/level1/level2/ButtonThirdL.jac +0 -25
- jac_client/examples/nested-folders/nested-advance/package.json +0 -29
- jac_client/examples/nested-folders/nested-advance/vite.config.js +0 -28
- jac_client/examples/nested-folders/nested-basic/.babelrc +0 -9
- jac_client/examples/nested-folders/nested-basic/README.md +0 -183
- jac_client/examples/nested-folders/nested-basic/app.jac +0 -13
- jac_client/examples/nested-folders/nested-basic/app.js +0 -7
- jac_client/examples/nested-folders/nested-basic/button.jac +0 -7
- jac_client/examples/nested-folders/nested-basic/components/button.jac +0 -7
- jac_client/examples/nested-folders/nested-basic/package.json +0 -28
- jac_client/examples/nested-folders/nested-basic/vite.config.js +0 -27
- jac_client/examples/with-router/.babelrc +0 -9
- jac_client/examples/with-router/README.md +0 -17
- jac_client/examples/with-router/app.jac +0 -323
- jac_client/examples/with-router/package.json +0 -28
- jac_client/examples/with-router/vite.config.js +0 -27
- jac_client/plugin/cli.py +0 -244
- jac_client/plugin/client.py +0 -152
- jac_client/plugin/client_runtime.jac +0 -234
- jac_client/plugin/vite_client_bundle.py +0 -503
- jac_client/tests/fixtures/basic-app/app.jac +0 -23
- jac_client/tests/fixtures/cl_file/app.cl.jac +0 -48
- jac_client/tests/fixtures/cl_file/app.jac +0 -15
- jac_client/tests/fixtures/client_app_with_antd/app.jac +0 -34
- jac_client/tests/fixtures/js_import/app.jac +0 -34
- jac_client/tests/fixtures/js_import/utils.js +0 -21
- jac_client/tests/fixtures/package-lock.json +0 -329
- jac_client/tests/fixtures/package.json +0 -11
- jac_client/tests/fixtures/relative_import/app.jac +0 -11
- jac_client/tests/fixtures/relative_import/button.jac +0 -7
- jac_client/tests/fixtures/spawn_test/app.jac +0 -129
- jac_client/tests/fixtures/test_fragments_spread/app.jac +0 -67
- jac_client/tests/test_asset_examples.py +0 -322
- jac_client/tests/test_cl.py +0 -530
- jac_client/tests/test_create_jac_app.py +0 -131
- jac_client/tests/test_nested_file.py +0 -374
- jac_client-0.2.2.dist-info/RECORD +0 -171
- jac_client-0.2.2.dist-info/entry_points.txt +0 -4
|
@@ -1,749 +0,0 @@
|
|
|
1
|
-
# Step 6: Event Handlers
|
|
2
|
-
|
|
3
|
-
> ** Quick Tip:** Each step has two parts. **Part 1** shows you what to build. **Part 2** explains why it works. Want to just build? Skip all Part 2 sections!
|
|
4
|
-
|
|
5
|
-
In this step, you'll learn how to handle user interactions like clicks, typing, and key presses to make your app fully interactive!
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Part 1: Building the App
|
|
10
|
-
|
|
11
|
-
### Step 6.1: Handle Input Changes (onChange)
|
|
12
|
-
|
|
13
|
-
Let's make the input field track what you type:
|
|
14
|
-
|
|
15
|
-
```jac
|
|
16
|
-
cl import from react {useState}
|
|
17
|
-
|
|
18
|
-
cl {
|
|
19
|
-
def TodoInput(props: any) -> any {
|
|
20
|
-
return <div style={{
|
|
21
|
-
"display": "flex",
|
|
22
|
-
"gap": "8px",
|
|
23
|
-
"marginBottom": "16px"
|
|
24
|
-
}}>
|
|
25
|
-
<input
|
|
26
|
-
type="text"
|
|
27
|
-
value={props.input}
|
|
28
|
-
onChange={lambda e: any -> None {
|
|
29
|
-
props.setInput(e.target.value);
|
|
30
|
-
}}
|
|
31
|
-
placeholder="What needs to be done?"
|
|
32
|
-
style={{
|
|
33
|
-
"flex": "1",
|
|
34
|
-
"padding": "8px",
|
|
35
|
-
"border": "1px solid #ddd",
|
|
36
|
-
"borderRadius": "4px"
|
|
37
|
-
}}
|
|
38
|
-
/>
|
|
39
|
-
<button style={{
|
|
40
|
-
"padding": "8px 16px",
|
|
41
|
-
"background": "#3b82f6",
|
|
42
|
-
"color": "white",
|
|
43
|
-
"border": "none",
|
|
44
|
-
"borderRadius": "4px",
|
|
45
|
-
"cursor": "pointer"
|
|
46
|
-
}}>
|
|
47
|
-
Add
|
|
48
|
-
</button>
|
|
49
|
-
</div>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
def app() -> any {
|
|
53
|
-
let [input, setInput] = useState("");
|
|
54
|
-
|
|
55
|
-
return <div style={{
|
|
56
|
-
"maxWidth": "600px",
|
|
57
|
-
"margin": "20px auto",
|
|
58
|
-
"padding": "20px"
|
|
59
|
-
}}>
|
|
60
|
-
<h1>My Todos</h1>
|
|
61
|
-
<TodoInput input={input} setInput={setInput} />
|
|
62
|
-
<p>You typed: "{input}"</p>
|
|
63
|
-
</div>;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
**Try it!** Type in the input field - you'll see the text appear below!
|
|
69
|
-
|
|
70
|
-
### Step 6.2: Handle Button Clicks (onClick)
|
|
71
|
-
|
|
72
|
-
Now let's make the "Add" button work:
|
|
73
|
-
|
|
74
|
-
```jac
|
|
75
|
-
cl import from react {useState}
|
|
76
|
-
|
|
77
|
-
cl {
|
|
78
|
-
def TodoInput(props: any) -> any {
|
|
79
|
-
return <div style={{
|
|
80
|
-
"display": "flex",
|
|
81
|
-
"gap": "8px",
|
|
82
|
-
"marginBottom": "16px"
|
|
83
|
-
}}>
|
|
84
|
-
<input
|
|
85
|
-
type="text"
|
|
86
|
-
value={props.input}
|
|
87
|
-
onChange={lambda e: any -> None {
|
|
88
|
-
props.setInput(e.target.value);
|
|
89
|
-
}}
|
|
90
|
-
placeholder="What needs to be done?"
|
|
91
|
-
style={{
|
|
92
|
-
"flex": "1",
|
|
93
|
-
"padding": "8px",
|
|
94
|
-
"border": "1px solid #ddd",
|
|
95
|
-
"borderRadius": "4px"
|
|
96
|
-
}}
|
|
97
|
-
/>
|
|
98
|
-
<button
|
|
99
|
-
onClick={lambda e: any -> None {
|
|
100
|
-
props.addTodo();
|
|
101
|
-
}}
|
|
102
|
-
style={{
|
|
103
|
-
"padding": "8px 16px",
|
|
104
|
-
"background": "#3b82f6",
|
|
105
|
-
"color": "white",
|
|
106
|
-
"border": "none",
|
|
107
|
-
"borderRadius": "4px",
|
|
108
|
-
"cursor": "pointer"
|
|
109
|
-
}}
|
|
110
|
-
>
|
|
111
|
-
Add
|
|
112
|
-
</button>
|
|
113
|
-
</div>;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
def app() -> any {
|
|
117
|
-
let [todos, setTodos] = useState([]);
|
|
118
|
-
let [input, setInput] = useState("");
|
|
119
|
-
|
|
120
|
-
# Function to add a new todo
|
|
121
|
-
def addTodo() -> None {
|
|
122
|
-
if not input.trim() {
|
|
123
|
-
return; # Don't add empty todos
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
let newTodo = {
|
|
127
|
-
"text": input.trim(),
|
|
128
|
-
"done": false
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
setTodos(todos.concat([newTodo]));
|
|
132
|
-
setInput(""); # Clear input
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return <div style={{
|
|
136
|
-
"maxWidth": "600px",
|
|
137
|
-
"margin": "20px auto",
|
|
138
|
-
"padding": "20px"
|
|
139
|
-
}}>
|
|
140
|
-
<h1>My Todos</h1>
|
|
141
|
-
<TodoInput
|
|
142
|
-
input={input}
|
|
143
|
-
setInput={setInput}
|
|
144
|
-
addTodo={addTodo}
|
|
145
|
-
/>
|
|
146
|
-
|
|
147
|
-
# Display todos
|
|
148
|
-
<div>
|
|
149
|
-
{todos.map(lambda todo: any -> any {
|
|
150
|
-
return <div style={{"padding": "8px"}}>
|
|
151
|
-
{todo.text}
|
|
152
|
-
</div>;
|
|
153
|
-
})}
|
|
154
|
-
</div>
|
|
155
|
-
</div>;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
**Try it!** Type a todo and click "Add" - it should appear in the list!
|
|
161
|
-
|
|
162
|
-
### Step 6.3: Handle Enter Key (onKeyPress)
|
|
163
|
-
|
|
164
|
-
Let's add the ability to press Enter to add a todo:
|
|
165
|
-
|
|
166
|
-
```jac
|
|
167
|
-
cl import from react {useState}
|
|
168
|
-
|
|
169
|
-
cl {
|
|
170
|
-
def TodoInput(props: any) -> any {
|
|
171
|
-
return <div style={{
|
|
172
|
-
"display": "flex",
|
|
173
|
-
"gap": "8px",
|
|
174
|
-
"marginBottom": "16px"
|
|
175
|
-
}}>
|
|
176
|
-
<input
|
|
177
|
-
type="text"
|
|
178
|
-
value={props.input}
|
|
179
|
-
onChange={lambda e: any -> None {
|
|
180
|
-
props.setInput(e.target.value);
|
|
181
|
-
}}
|
|
182
|
-
onKeyPress={lambda e: any -> None {
|
|
183
|
-
if e.key == "Enter" {
|
|
184
|
-
props.addTodo();
|
|
185
|
-
}
|
|
186
|
-
}}
|
|
187
|
-
placeholder="What needs to be done?"
|
|
188
|
-
style={{
|
|
189
|
-
"flex": "1",
|
|
190
|
-
"padding": "8px",
|
|
191
|
-
"border": "1px solid #ddd",
|
|
192
|
-
"borderRadius": "4px"
|
|
193
|
-
}}
|
|
194
|
-
/>
|
|
195
|
-
<button
|
|
196
|
-
onClick={lambda e: any -> None {
|
|
197
|
-
props.addTodo();
|
|
198
|
-
}}
|
|
199
|
-
style={{
|
|
200
|
-
"padding": "8px 16px",
|
|
201
|
-
"background": "#3b82f6",
|
|
202
|
-
"color": "white",
|
|
203
|
-
"border": "none",
|
|
204
|
-
"borderRadius": "4px",
|
|
205
|
-
"cursor": "pointer"
|
|
206
|
-
}}
|
|
207
|
-
>
|
|
208
|
-
Add
|
|
209
|
-
</button>
|
|
210
|
-
</div>;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
# ... rest of code
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
**Try it!** Now you can press Enter to add todos!
|
|
218
|
-
|
|
219
|
-
### Step 6.4: Toggle and Delete Todos
|
|
220
|
-
|
|
221
|
-
Let's add the complete functionality:
|
|
222
|
-
|
|
223
|
-
```jac
|
|
224
|
-
cl import from react {useState}
|
|
225
|
-
|
|
226
|
-
cl {
|
|
227
|
-
# ... (keep TodoInput and TodoFilters)
|
|
228
|
-
|
|
229
|
-
def TodoItem(props: any) -> any {
|
|
230
|
-
return <div style={{
|
|
231
|
-
"display": "flex",
|
|
232
|
-
"alignItems": "center",
|
|
233
|
-
"gap": "10px",
|
|
234
|
-
"padding": "10px",
|
|
235
|
-
"borderBottom": "1px solid #e5e7eb"
|
|
236
|
-
}}>
|
|
237
|
-
<input
|
|
238
|
-
type="checkbox"
|
|
239
|
-
checked={props.done}
|
|
240
|
-
onChange={lambda e: any -> None {
|
|
241
|
-
props.toggleTodo(props.id);
|
|
242
|
-
}}
|
|
243
|
-
style={{"cursor": "pointer"}}
|
|
244
|
-
/>
|
|
245
|
-
<span style={{
|
|
246
|
-
"flex": "1",
|
|
247
|
-
"textDecoration": ("line-through" if props.done else "none"),
|
|
248
|
-
"color": ("#999" if props.done else "#000")
|
|
249
|
-
}}>
|
|
250
|
-
{props.text}
|
|
251
|
-
</span>
|
|
252
|
-
<button
|
|
253
|
-
onClick={lambda e: any -> None {
|
|
254
|
-
props.deleteTodo(props.id);
|
|
255
|
-
}}
|
|
256
|
-
style={{
|
|
257
|
-
"padding": "4px 8px",
|
|
258
|
-
"background": "#ef4444",
|
|
259
|
-
"color": "white",
|
|
260
|
-
"border": "none",
|
|
261
|
-
"borderRadius": "4px",
|
|
262
|
-
"cursor": "pointer",
|
|
263
|
-
"fontSize": "12px"
|
|
264
|
-
}}
|
|
265
|
-
>
|
|
266
|
-
Delete
|
|
267
|
-
</button>
|
|
268
|
-
</div>;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
def app() -> any {
|
|
272
|
-
let [todos, setTodos] = useState([]);
|
|
273
|
-
let [input, setInput] = useState("");
|
|
274
|
-
|
|
275
|
-
# Add todo
|
|
276
|
-
def addTodo() -> None {
|
|
277
|
-
if not input.trim() {
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
let newTodo = {
|
|
282
|
-
"id": Date.now(), # Use timestamp as unique ID
|
|
283
|
-
"text": input.trim(),
|
|
284
|
-
"done": false
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
setTodos(todos.concat([newTodo]));
|
|
288
|
-
setInput("");
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
# Toggle todo
|
|
292
|
-
def toggleTodo(id: any) -> None {
|
|
293
|
-
setTodos(todos.map(lambda todo: any -> any {
|
|
294
|
-
if todo["id"] == id {
|
|
295
|
-
return {
|
|
296
|
-
"id": todo["id"],
|
|
297
|
-
"text": todo["text"],
|
|
298
|
-
"done": not todo["done"]
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
return todo;
|
|
302
|
-
}));
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
# Delete todo
|
|
306
|
-
def deleteTodo(id: any) -> None {
|
|
307
|
-
setTodos(todos.filter(lambda todo: any -> bool {
|
|
308
|
-
return todo["id"] != id;
|
|
309
|
-
}));
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return <div style={{
|
|
313
|
-
"maxWidth": "600px",
|
|
314
|
-
"margin": "20px auto",
|
|
315
|
-
"padding": "20px"
|
|
316
|
-
}}>
|
|
317
|
-
<h1>My Todos</h1>
|
|
318
|
-
<TodoInput
|
|
319
|
-
input={input}
|
|
320
|
-
setInput={setInput}
|
|
321
|
-
addTodo={addTodo}
|
|
322
|
-
/>
|
|
323
|
-
|
|
324
|
-
<div>
|
|
325
|
-
{todos.map(lambda todo: any -> any {
|
|
326
|
-
return <TodoItem
|
|
327
|
-
key={todo["id"]}
|
|
328
|
-
id={todo["id"]}
|
|
329
|
-
text={todo["text"]}
|
|
330
|
-
done={todo["done"]}
|
|
331
|
-
toggleTodo={toggleTodo}
|
|
332
|
-
deleteTodo={deleteTodo}
|
|
333
|
-
/>;
|
|
334
|
-
})}
|
|
335
|
-
</div>
|
|
336
|
-
</div>;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
**Try it!** You can now:
|
|
342
|
-
- Add todos
|
|
343
|
-
- Check/uncheck them
|
|
344
|
-
- Delete them
|
|
345
|
-
|
|
346
|
-
### Step 6.5: Add Filter Functionality
|
|
347
|
-
|
|
348
|
-
Final step - make the filter buttons work:
|
|
349
|
-
|
|
350
|
-
```jac
|
|
351
|
-
cl import from react {useState}
|
|
352
|
-
|
|
353
|
-
cl {
|
|
354
|
-
def TodoFilters(props: any) -> any {
|
|
355
|
-
return <div style={{
|
|
356
|
-
"display": "flex",
|
|
357
|
-
"gap": "8px",
|
|
358
|
-
"marginBottom": "16px"
|
|
359
|
-
}}>
|
|
360
|
-
<button
|
|
361
|
-
onClick={lambda e: any -> None {
|
|
362
|
-
props.setFilter("all");
|
|
363
|
-
}}
|
|
364
|
-
style={{
|
|
365
|
-
"padding": "6px 12px",
|
|
366
|
-
"background": ("#3b82f6" if props.filter == "all" else "#e5e7eb"),
|
|
367
|
-
"color": ("#ffffff" if props.filter == "all" else "#000000"),
|
|
368
|
-
"border": "none",
|
|
369
|
-
"borderRadius": "4px",
|
|
370
|
-
"cursor": "pointer"
|
|
371
|
-
}}
|
|
372
|
-
>
|
|
373
|
-
All
|
|
374
|
-
</button>
|
|
375
|
-
<button
|
|
376
|
-
onClick={lambda e: any -> None {
|
|
377
|
-
props.setFilter("active");
|
|
378
|
-
}}
|
|
379
|
-
style={{
|
|
380
|
-
"padding": "6px 12px",
|
|
381
|
-
"background": ("#3b82f6" if props.filter == "active" else "#e5e7eb"),
|
|
382
|
-
"color": ("#ffffff" if props.filter == "active" else "#000000"),
|
|
383
|
-
"border": "none",
|
|
384
|
-
"borderRadius": "4px",
|
|
385
|
-
"cursor": "pointer"
|
|
386
|
-
}}
|
|
387
|
-
>
|
|
388
|
-
Active
|
|
389
|
-
</button>
|
|
390
|
-
<button
|
|
391
|
-
onClick={lambda e: any -> None {
|
|
392
|
-
props.setFilter("completed");
|
|
393
|
-
}}
|
|
394
|
-
style={{
|
|
395
|
-
"padding": "6px 12px",
|
|
396
|
-
"background": ("#3b82f6" if props.filter == "completed" else "#e5e7eb"),
|
|
397
|
-
"color": ("#ffffff" if props.filter == "completed" else "#000000"),
|
|
398
|
-
"border": "none",
|
|
399
|
-
"borderRadius": "4px",
|
|
400
|
-
"cursor": "pointer"
|
|
401
|
-
}}
|
|
402
|
-
>
|
|
403
|
-
Completed
|
|
404
|
-
</button>
|
|
405
|
-
</div>;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
def app() -> any {
|
|
409
|
-
let [todos, setTodos] = useState([]);
|
|
410
|
-
let [input, setInput] = useState("");
|
|
411
|
-
let [filter, setFilter] = useState("all");
|
|
412
|
-
|
|
413
|
-
# ... (keep addTodo, toggleTodo, deleteTodo functions)
|
|
414
|
-
|
|
415
|
-
# Filter todos based on current filter
|
|
416
|
-
def getFilteredTodos() -> list {
|
|
417
|
-
if filter == "active" {
|
|
418
|
-
return todos.filter(lambda todo: any -> bool {
|
|
419
|
-
return not todo["done"];
|
|
420
|
-
});
|
|
421
|
-
} elif filter == "completed" {
|
|
422
|
-
return todos.filter(lambda todo: any -> bool {
|
|
423
|
-
return todo["done"];
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
return todos;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
filteredTodos = getFilteredTodos();
|
|
430
|
-
|
|
431
|
-
return <div style={{
|
|
432
|
-
"maxWidth": "600px",
|
|
433
|
-
"margin": "20px auto",
|
|
434
|
-
"padding": "20px"
|
|
435
|
-
}}>
|
|
436
|
-
<h1>My Todos</h1>
|
|
437
|
-
<TodoInput input={input} setInput={setInput} addTodo={addTodo} />
|
|
438
|
-
<TodoFilters filter={filter} setFilter={setFilter} />
|
|
439
|
-
|
|
440
|
-
<div>
|
|
441
|
-
{filteredTodos.map(lambda todo: any -> any {
|
|
442
|
-
return <TodoItem
|
|
443
|
-
key={todo["id"]}
|
|
444
|
-
id={todo["id"]}
|
|
445
|
-
text={todo["text"]}
|
|
446
|
-
done={todo["done"]}
|
|
447
|
-
toggleTodo={toggleTodo}
|
|
448
|
-
deleteTodo={deleteTodo}
|
|
449
|
-
/>;
|
|
450
|
-
})}
|
|
451
|
-
</div>
|
|
452
|
-
</div>;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
**Try it!** Now you have a fully functional todo app!
|
|
458
|
-
|
|
459
|
-
---
|
|
460
|
-
|
|
461
|
-
**⏭ Want to skip the theory?** Jump to [Step 7: Effects](./step-07-effects.md)
|
|
462
|
-
|
|
463
|
-
---
|
|
464
|
-
|
|
465
|
-
## Part 2: Understanding the Concepts
|
|
466
|
-
|
|
467
|
-
### What are Event Handlers?
|
|
468
|
-
|
|
469
|
-
Event handlers are functions that run when something happens (user clicks, types, etc.).
|
|
470
|
-
|
|
471
|
-
**Common events:**
|
|
472
|
-
- `onClick` - User clicks an element
|
|
473
|
-
- `onChange` - Input value changes
|
|
474
|
-
- `onKeyPress` - User presses a key
|
|
475
|
-
- `onSubmit` - Form is submitted
|
|
476
|
-
- `onFocus` - Element gains focus
|
|
477
|
-
- `onBlur` - Element loses focus
|
|
478
|
-
|
|
479
|
-
### Event Handler Syntax
|
|
480
|
-
|
|
481
|
-
```jac
|
|
482
|
-
<button onClick={lambda e: any -> None {
|
|
483
|
-
# Code runs when button is clicked
|
|
484
|
-
console.log("Clicked!");
|
|
485
|
-
}}>
|
|
486
|
-
Click me
|
|
487
|
-
</button>
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
**Breakdown:**
|
|
491
|
-
- `onClick={}` - The event attribute
|
|
492
|
-
- `lambda e: any -> None { }` - Anonymous function
|
|
493
|
-
- `e` - Event object (contains info about the event)
|
|
494
|
-
|
|
495
|
-
### The Event Object (`e`)
|
|
496
|
-
|
|
497
|
-
```jac
|
|
498
|
-
onChange={lambda e: any -> None {
|
|
499
|
-
console.log(e.target); # The element that triggered the event
|
|
500
|
-
console.log(e.target.value); # For inputs: the current value
|
|
501
|
-
console.log(e.key); # For key events: which key was pressed
|
|
502
|
-
}}
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
**Common properties:**
|
|
506
|
-
- `e.target` - The element that triggered the event
|
|
507
|
-
- `e.target.value` - Current value (for inputs)
|
|
508
|
-
- `e.key` - Which key was pressed
|
|
509
|
-
- `e.preventDefault()` - Prevent default behavior
|
|
510
|
-
|
|
511
|
-
### Passing Functions as Props
|
|
512
|
-
|
|
513
|
-
You can pass functions down to child components:
|
|
514
|
-
|
|
515
|
-
```jac
|
|
516
|
-
def Parent() -> any {
|
|
517
|
-
def handleClick() -> None {
|
|
518
|
-
console.log("Clicked!");
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
# Pass function to child
|
|
522
|
-
return <Child onClick={handleClick} />;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
def Child(props: any) -> any {
|
|
526
|
-
# Call parent's function
|
|
527
|
-
return <button onClick={props.onClick}>
|
|
528
|
-
Click me
|
|
529
|
-
</button>;
|
|
530
|
-
}
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
This lets children trigger parent behavior!
|
|
534
|
-
|
|
535
|
-
### Updating State in Event Handlers
|
|
536
|
-
|
|
537
|
-
```jac
|
|
538
|
-
def app() -> any {
|
|
539
|
-
let [count, setCount] = useState(0);
|
|
540
|
-
|
|
541
|
-
def increment() -> None {
|
|
542
|
-
setCount(count + 1); # Update state
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
return <button onClick={increment}>
|
|
546
|
-
Count: {count}
|
|
547
|
-
</button>;
|
|
548
|
-
}
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
When state updates, React re-renders the component with the new value!
|
|
552
|
-
|
|
553
|
-
### Array Methods for State Updates
|
|
554
|
-
|
|
555
|
-
**`.concat()` - Add items**
|
|
556
|
-
|
|
557
|
-
```jac
|
|
558
|
-
# Correct way to add
|
|
559
|
-
setTodos(todos.concat([newTodo]));
|
|
560
|
-
|
|
561
|
-
# Wrong (modifies original)
|
|
562
|
-
todos.push(newTodo);
|
|
563
|
-
setTodos(todos);
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
**`.map()` - Update items**
|
|
567
|
-
|
|
568
|
-
```jac
|
|
569
|
-
# Toggle a todo
|
|
570
|
-
setTodos(todos.map(lambda todo: any -> any {
|
|
571
|
-
if todo["id"] == targetId {
|
|
572
|
-
return {"id": todo["id"], "done": not todo["done"]};
|
|
573
|
-
}
|
|
574
|
-
return todo;
|
|
575
|
-
}));
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
**`.filter()` - Remove items**
|
|
579
|
-
|
|
580
|
-
```jac
|
|
581
|
-
# Delete a todo
|
|
582
|
-
setTodos(todos.filter(lambda todo: any -> bool {
|
|
583
|
-
return todo["id"] != targetId;
|
|
584
|
-
}));
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
### Inline vs Named Functions
|
|
588
|
-
|
|
589
|
-
**Inline (good for simple logic):**
|
|
590
|
-
|
|
591
|
-
```jac
|
|
592
|
-
<button onClick={lambda e: any -> None {
|
|
593
|
-
setCount(count + 1);
|
|
594
|
-
}}>
|
|
595
|
-
Click
|
|
596
|
-
</button>
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
**Named (good for complex logic):**
|
|
600
|
-
|
|
601
|
-
```jac
|
|
602
|
-
def handleClick() -> None {
|
|
603
|
-
if count < 10 {
|
|
604
|
-
setCount(count + 1);
|
|
605
|
-
} else {
|
|
606
|
-
alert("Max reached!");
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
<button onClick={handleClick}>Click</button>
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
### Event Handler Common Patterns
|
|
614
|
-
|
|
615
|
-
**Pattern 1: Toggle Boolean**
|
|
616
|
-
|
|
617
|
-
```jac
|
|
618
|
-
let [isOpen, setIsOpen] = useState(false);
|
|
619
|
-
|
|
620
|
-
def toggle() -> None {
|
|
621
|
-
setIsOpen(not isOpen);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
<button onClick={toggle}>Toggle</button>
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
**Pattern 2: Update Input**
|
|
628
|
-
|
|
629
|
-
```jac
|
|
630
|
-
let [text, setText] = useState("");
|
|
631
|
-
|
|
632
|
-
<input
|
|
633
|
-
value={text}
|
|
634
|
-
onChange={lambda e: any -> None {
|
|
635
|
-
setText(e.target.value);
|
|
636
|
-
}}
|
|
637
|
-
/>
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
**Pattern 3: Add to List**
|
|
641
|
-
|
|
642
|
-
```jac
|
|
643
|
-
let [items, setItems] = useState([]);
|
|
644
|
-
|
|
645
|
-
def addItem(newItem: any) -> None {
|
|
646
|
-
setItems(items.concat([newItem]));
|
|
647
|
-
}
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
**Pattern 4: Remove from List**
|
|
651
|
-
|
|
652
|
-
```jac
|
|
653
|
-
def removeItem(id: any) -> None {
|
|
654
|
-
setItems(items.filter(lambda item: any -> bool {
|
|
655
|
-
return item.id != id;
|
|
656
|
-
}));
|
|
657
|
-
}
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
---
|
|
661
|
-
|
|
662
|
-
## What You've Learned
|
|
663
|
-
|
|
664
|
-
- What event handlers are
|
|
665
|
-
- Common events (onClick, onChange, onKeyPress)
|
|
666
|
-
- Event handler syntax with lambda functions
|
|
667
|
-
- The event object (`e`)
|
|
668
|
-
- Passing functions as props
|
|
669
|
-
- Updating state in event handlers
|
|
670
|
-
- Array methods (concat, map, filter)
|
|
671
|
-
- Inline vs named functions
|
|
672
|
-
|
|
673
|
-
---
|
|
674
|
-
|
|
675
|
-
## Common Issues
|
|
676
|
-
|
|
677
|
-
### Issue: Event handler not firing
|
|
678
|
-
|
|
679
|
-
**Check:**
|
|
680
|
-
- Did you use `onClick` not `onclick`? (capital C)
|
|
681
|
-
- Did you pass a function? `onClick={myFunction}` not `onClick={myFunction()}`
|
|
682
|
-
|
|
683
|
-
### Issue: Input not updating
|
|
684
|
-
|
|
685
|
-
**Check:**
|
|
686
|
-
- Did you add both `value` and `onChange`?
|
|
687
|
-
- Is `onChange` calling the state setter?
|
|
688
|
-
|
|
689
|
-
```jac
|
|
690
|
-
# Correct
|
|
691
|
-
<input
|
|
692
|
-
value={text}
|
|
693
|
-
onChange={lambda e: any -> None {
|
|
694
|
-
setText(e.target.value);
|
|
695
|
-
}}
|
|
696
|
-
/>
|
|
697
|
-
|
|
698
|
-
# Missing onChange
|
|
699
|
-
<input value={text} />
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
### Issue: State not updating
|
|
703
|
-
|
|
704
|
-
**Check:** Are you creating a new array/object?
|
|
705
|
-
|
|
706
|
-
```jac
|
|
707
|
-
# Wrong (modifying original)
|
|
708
|
-
todos.push(newTodo);
|
|
709
|
-
setTodos(todos);
|
|
710
|
-
|
|
711
|
-
# Correct (creating new array)
|
|
712
|
-
setTodos(todos.concat([newTodo]));
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
---
|
|
716
|
-
|
|
717
|
-
## Quick Exercise
|
|
718
|
-
|
|
719
|
-
Try adding a "Clear All" button:
|
|
720
|
-
|
|
721
|
-
```jac
|
|
722
|
-
def clearAll() -> None {
|
|
723
|
-
setTodos([]);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
<button onClick={clearAll}>Clear All</button>
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
And a "Clear Completed" button:
|
|
730
|
-
|
|
731
|
-
```jac
|
|
732
|
-
def clearCompleted() -> None {
|
|
733
|
-
setTodos(todos.filter(lambda todo: any -> bool {
|
|
734
|
-
return not todo["done"];
|
|
735
|
-
}));
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
<button onClick={clearCompleted}>Clear Completed</button>
|
|
739
|
-
```
|
|
740
|
-
|
|
741
|
-
---
|
|
742
|
-
|
|
743
|
-
## Next Step
|
|
744
|
-
|
|
745
|
-
Excellent! Your app is now fully interactive with local state. But when you refresh the page, all your todos disappear!
|
|
746
|
-
|
|
747
|
-
In the next step, we'll use **useEffect** to load data when the app starts!
|
|
748
|
-
|
|
749
|
-
**[Continue to Step 7: Effects](./step-07-effects.md)**
|