sigpro 1.0.14 → 1.2.39
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.
- package/Readme.md +164 -1008
- package/dist/sigpro.editor.js +1 -0
- package/dist/sigpro.grid.js +78 -0
- package/dist/sigpro.js +1 -0
- package/dist/sigpro.ui.css +2 -0
- package/dist/sigpro.ui.js +1 -0
- package/dist/sigpro.utils.js +1 -0
- package/dist/sigpro.vite.js +4 -0
- package/package.json +64 -14
- package/sigpro.d.ts +395 -0
- package/.github/workflows/publish.yml +0 -25
- package/bun.lock +0 -385
- package/docs/404.html +0 -22
- package/docs/api/components.html +0 -595
- package/docs/api/effects.html +0 -787
- package/docs/api/fetch.html +0 -873
- package/docs/api/pages.html +0 -405
- package/docs/api/quick.html +0 -217
- package/docs/api/routing.html +0 -628
- package/docs/api/signals.html +0 -683
- package/docs/api/storage.html +0 -820
- package/docs/assets/api_components.md.BlFwj17l.js +0 -571
- package/docs/assets/api_components.md.BlFwj17l.lean.js +0 -1
- package/docs/assets/api_effects.md.Br_yStBS.js +0 -763
- package/docs/assets/api_effects.md.Br_yStBS.lean.js +0 -1
- package/docs/assets/api_fetch.md.DQLBJSoq.js +0 -849
- package/docs/assets/api_fetch.md.DQLBJSoq.lean.js +0 -1
- package/docs/assets/api_pages.md.BP19nHXw.js +0 -381
- package/docs/assets/api_pages.md.BP19nHXw.lean.js +0 -1
- package/docs/assets/api_quick.md.BDS3ttnt.js +0 -193
- package/docs/assets/api_quick.md.BDS3ttnt.lean.js +0 -1
- package/docs/assets/api_routing.md.7SNAZXtp.js +0 -604
- package/docs/assets/api_routing.md.7SNAZXtp.lean.js +0 -1
- package/docs/assets/api_signals.md.CrW68-BA.js +0 -659
- package/docs/assets/api_signals.md.CrW68-BA.lean.js +0 -1
- package/docs/assets/api_storage.md.COEWBXHk.js +0 -796
- package/docs/assets/api_storage.md.COEWBXHk.lean.js +0 -1
- package/docs/assets/app.DtmzNmNl.js +0 -1
- package/docs/assets/chunks/framework.C8AWLET_.js +0 -19
- package/docs/assets/chunks/theme.yfWKMLQM.js +0 -1
- package/docs/assets/guide_getting-started.md.BeQpK3vd.js +0 -172
- package/docs/assets/guide_getting-started.md.BeQpK3vd.lean.js +0 -1
- package/docs/assets/guide_why.md.DXchYMN-.js +0 -23
- package/docs/assets/guide_why.md.DXchYMN-.lean.js +0 -1
- package/docs/assets/index.md.uvMJmU4o.js +0 -1
- package/docs/assets/index.md.uvMJmU4o.lean.js +0 -1
- package/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/docs/assets/style.DJRheFKp.css +0 -1
- package/docs/assets/ui_intro.md.gZ21GFqo.js +0 -1
- package/docs/assets/ui_intro.md.gZ21GFqo.lean.js +0 -1
- package/docs/assets/vite_plugin.md.gDWEi8f0.js +0 -225
- package/docs/assets/vite_plugin.md.gDWEi8f0.lean.js +0 -1
- package/docs/guide/getting-started.html +0 -196
- package/docs/guide/why.html +0 -47
- package/docs/hashmap.json +0 -1
- package/docs/index.html +0 -25
- package/docs/logo.svg +0 -118
- package/docs/ui/intro.html +0 -25
- package/docs/vite/plugin.html +0 -249
- package/docs/vp-icons.css +0 -1
- package/index.js +0 -3
- package/packages/docs/.vitepress/cache/deps/@theme_index.js +0 -275
- package/packages/docs/.vitepress/cache/deps/@theme_index.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/_metadata.json +0 -40
- package/packages/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js +0 -12951
- package/packages/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js +0 -9719
- package/packages/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/package.json +0 -3
- package/packages/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4505
- package/packages/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -583
- package/packages/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/vue.js +0 -347
- package/packages/docs/.vitepress/cache/deps/vue.js.map +0 -7
- package/packages/docs/.vitepress/config.js +0 -68
- package/packages/docs/api/components.md +0 -760
- package/packages/docs/api/effects.md +0 -1039
- package/packages/docs/api/fetch.md +0 -998
- package/packages/docs/api/pages.md +0 -497
- package/packages/docs/api/quick.md +0 -436
- package/packages/docs/api/routing.md +0 -784
- package/packages/docs/api/signals.md +0 -899
- package/packages/docs/api/storage.md +0 -952
- package/packages/docs/guide/getting-started.md +0 -308
- package/packages/docs/guide/why.md +0 -135
- package/packages/docs/index.md +0 -84
- package/packages/docs/logo.svg +0 -118
- package/packages/docs/public/logo.svg +0 -118
- package/packages/docs/ui/intro.md +0 -16
- package/packages/docs/vite/plugin.md +0 -423
- package/packages/sigpro/plugin.js +0 -91
- package/packages/sigpro/plugin.min.js +0 -1
- package/packages/sigpro/sigpro.js +0 -631
- package/packages/sigpro/sigpro.min.js +0 -1
- package/vite.config.js +0 -24
|
@@ -1,998 +0,0 @@
|
|
|
1
|
-
# Fetch API 🌐
|
|
2
|
-
|
|
3
|
-
SigPro provides a simple, lightweight wrapper around the native Fetch API that integrates seamlessly with signals for loading state management. It's designed for common use cases with sensible defaults.
|
|
4
|
-
|
|
5
|
-
## Core Concepts
|
|
6
|
-
|
|
7
|
-
### What is `$.fetch`?
|
|
8
|
-
|
|
9
|
-
A ultra-simple fetch wrapper that:
|
|
10
|
-
- **Automatically handles JSON** serialization and parsing
|
|
11
|
-
- **Integrates with signals** for loading state
|
|
12
|
-
- **Returns `null` on error** (no try/catch needed for basic usage)
|
|
13
|
-
- **Works great with effects** for reactive data fetching
|
|
14
|
-
|
|
15
|
-
## `$.fetch(url, data, [loading])`
|
|
16
|
-
|
|
17
|
-
Makes a POST request with JSON data and optional loading signal.
|
|
18
|
-
|
|
19
|
-
```javascript
|
|
20
|
-
import { $ } from 'sigpro';
|
|
21
|
-
|
|
22
|
-
const loading = $(false);
|
|
23
|
-
|
|
24
|
-
async function loadUser() {
|
|
25
|
-
const user = await $.fetch('/api/user', { id: 123 }, loading);
|
|
26
|
-
if (user) {
|
|
27
|
-
console.log('User loaded:', user);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## 📋 API Reference
|
|
33
|
-
|
|
34
|
-
### Parameters
|
|
35
|
-
|
|
36
|
-
| Parameter | Type | Description |
|
|
37
|
-
|-----------|------|-------------|
|
|
38
|
-
| `url` | `string` | Endpoint URL |
|
|
39
|
-
| `data` | `Object` | Data to send (automatically JSON.stringify'd) |
|
|
40
|
-
| `loading` | `Function` (optional) | Signal function to track loading state |
|
|
41
|
-
|
|
42
|
-
### Returns
|
|
43
|
-
|
|
44
|
-
| Return | Description |
|
|
45
|
-
|--------|-------------|
|
|
46
|
-
| `Promise<Object\|null>` | Parsed JSON response or `null` on error |
|
|
47
|
-
|
|
48
|
-
## 🎯 Basic Examples
|
|
49
|
-
|
|
50
|
-
### Simple Data Fetching
|
|
51
|
-
|
|
52
|
-
```javascript
|
|
53
|
-
import { $ } from 'sigpro';
|
|
54
|
-
|
|
55
|
-
const userData = $(null);
|
|
56
|
-
|
|
57
|
-
async function fetchUser(id) {
|
|
58
|
-
const data = await $.fetch('/api/user', { id });
|
|
59
|
-
if (data) {
|
|
60
|
-
userData(data);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
fetchUser(123);
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### With Loading State
|
|
68
|
-
|
|
69
|
-
```javascript
|
|
70
|
-
import { $, html } from 'sigpro';
|
|
71
|
-
|
|
72
|
-
const user = $(null);
|
|
73
|
-
const loading = $(false);
|
|
74
|
-
|
|
75
|
-
async function loadUser(id) {
|
|
76
|
-
const data = await $.fetch('/api/user', { id }, loading);
|
|
77
|
-
if (data) user(data);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// In your template
|
|
81
|
-
html`
|
|
82
|
-
<div>
|
|
83
|
-
${() => loading() ? html`
|
|
84
|
-
<div class="spinner">Loading...</div>
|
|
85
|
-
` : user() ? html`
|
|
86
|
-
<div>
|
|
87
|
-
<h2>${user().name}</h2>
|
|
88
|
-
<p>Email: ${user().email}</p>
|
|
89
|
-
</div>
|
|
90
|
-
` : html`
|
|
91
|
-
<p>No user found</p>
|
|
92
|
-
`}
|
|
93
|
-
</div>
|
|
94
|
-
`;
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### In an Effect
|
|
98
|
-
|
|
99
|
-
```javascript
|
|
100
|
-
import { $ } from 'sigpro';
|
|
101
|
-
|
|
102
|
-
const userId = $(1);
|
|
103
|
-
const user = $(null);
|
|
104
|
-
const loading = $(false);
|
|
105
|
-
|
|
106
|
-
$.effect(() => {
|
|
107
|
-
const id = userId();
|
|
108
|
-
if (id) {
|
|
109
|
-
$.fetch(`/api/users/${id}`, null, loading).then(data => {
|
|
110
|
-
if (data) user(data);
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
userId(2); // Automatically fetches new user
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## 🚀 Advanced Examples
|
|
119
|
-
|
|
120
|
-
### User Profile with Loading States
|
|
121
|
-
|
|
122
|
-
```javascript
|
|
123
|
-
import { $, html } from 'sigpro';
|
|
124
|
-
|
|
125
|
-
const Profile = () => {
|
|
126
|
-
const userId = $(1);
|
|
127
|
-
const user = $(null);
|
|
128
|
-
const loading = $(false);
|
|
129
|
-
const error = $(null);
|
|
130
|
-
|
|
131
|
-
const fetchUser = async (id) => {
|
|
132
|
-
error(null);
|
|
133
|
-
const data = await $.fetch('/api/user', { id }, loading);
|
|
134
|
-
if (data) {
|
|
135
|
-
user(data);
|
|
136
|
-
} else {
|
|
137
|
-
error('Failed to load user');
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
// Fetch when userId changes
|
|
142
|
-
$.effect(() => {
|
|
143
|
-
fetchUser(userId());
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
return html`
|
|
147
|
-
<div class="profile">
|
|
148
|
-
<div class="user-selector">
|
|
149
|
-
<button @click=${() => userId(1)}>User 1</button>
|
|
150
|
-
<button @click=${() => userId(2)}>User 2</button>
|
|
151
|
-
<button @click=${() => userId(3)}>User 3</button>
|
|
152
|
-
</div>
|
|
153
|
-
|
|
154
|
-
${() => {
|
|
155
|
-
if (loading()) {
|
|
156
|
-
return html`<div class="spinner">Loading profile...</div>`;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (error()) {
|
|
160
|
-
return html`<div class="error">${error()}</div>`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (user()) {
|
|
164
|
-
return html`
|
|
165
|
-
<div class="user-info">
|
|
166
|
-
<h2>${user().name}</h2>
|
|
167
|
-
<p>Email: ${user().email}</p>
|
|
168
|
-
<p>Role: ${user().role}</p>
|
|
169
|
-
<p>Joined: ${new Date(user().joined).toLocaleDateString()}</p>
|
|
170
|
-
</div>
|
|
171
|
-
`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return html`<p>Select a user</p>`;
|
|
175
|
-
}}
|
|
176
|
-
</div>
|
|
177
|
-
`;
|
|
178
|
-
};
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### Todo List with API
|
|
182
|
-
|
|
183
|
-
```javascript
|
|
184
|
-
import { $, html } from 'sigpro';
|
|
185
|
-
|
|
186
|
-
const TodoApp = () => {
|
|
187
|
-
const todos = $([]);
|
|
188
|
-
const loading = $(false);
|
|
189
|
-
const newTodo = $('');
|
|
190
|
-
const filter = $('all'); // 'all', 'active', 'completed'
|
|
191
|
-
|
|
192
|
-
// Load todos
|
|
193
|
-
const loadTodos = async () => {
|
|
194
|
-
const data = await $.fetch('/api/todos', {}, loading);
|
|
195
|
-
if (data) todos(data);
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
// Add todo
|
|
199
|
-
const addTodo = async () => {
|
|
200
|
-
if (!newTodo().trim()) return;
|
|
201
|
-
|
|
202
|
-
const todo = await $.fetch('/api/todos', {
|
|
203
|
-
text: newTodo(),
|
|
204
|
-
completed: false
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
if (todo) {
|
|
208
|
-
todos([...todos(), todo]);
|
|
209
|
-
newTodo('');
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
// Toggle todo
|
|
214
|
-
const toggleTodo = async (id, completed) => {
|
|
215
|
-
const updated = await $.fetch(`/api/todos/${id}`, {
|
|
216
|
-
completed: !completed
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
if (updated) {
|
|
220
|
-
todos(todos().map(t =>
|
|
221
|
-
t.id === id ? updated : t
|
|
222
|
-
));
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
// Delete todo
|
|
227
|
-
const deleteTodo = async (id) => {
|
|
228
|
-
const result = await $.fetch(`/api/todos/${id}/delete`, {});
|
|
229
|
-
if (result) {
|
|
230
|
-
todos(todos().filter(t => t.id !== id));
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
// Filtered todos
|
|
235
|
-
const filteredTodos = $(() => {
|
|
236
|
-
const currentFilter = filter();
|
|
237
|
-
if (currentFilter === 'all') return todos();
|
|
238
|
-
if (currentFilter === 'active') {
|
|
239
|
-
return todos().filter(t => !t.completed);
|
|
240
|
-
}
|
|
241
|
-
return todos().filter(t => t.completed);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Load on mount
|
|
245
|
-
loadTodos();
|
|
246
|
-
|
|
247
|
-
return html`
|
|
248
|
-
<div class="todo-app">
|
|
249
|
-
<h1>Todo List</h1>
|
|
250
|
-
|
|
251
|
-
<div class="add-todo">
|
|
252
|
-
<input
|
|
253
|
-
type="text"
|
|
254
|
-
:value=${newTodo}
|
|
255
|
-
@keydown.enter=${addTodo}
|
|
256
|
-
placeholder="Add a new todo..."
|
|
257
|
-
/>
|
|
258
|
-
<button @click=${addTodo}>Add</button>
|
|
259
|
-
</div>
|
|
260
|
-
|
|
261
|
-
<div class="filters">
|
|
262
|
-
<button
|
|
263
|
-
class:active=${() => filter() === 'all'}
|
|
264
|
-
@click=${() => filter('all')}
|
|
265
|
-
>
|
|
266
|
-
All
|
|
267
|
-
</button>
|
|
268
|
-
<button
|
|
269
|
-
class:active=${() => filter() === 'active'}
|
|
270
|
-
@click=${() => filter('active')}
|
|
271
|
-
>
|
|
272
|
-
Active
|
|
273
|
-
</button>
|
|
274
|
-
<button
|
|
275
|
-
class:active=${() => filter() === 'completed'}
|
|
276
|
-
@click=${() => filter('completed')}
|
|
277
|
-
>
|
|
278
|
-
Completed
|
|
279
|
-
</button>
|
|
280
|
-
</div>
|
|
281
|
-
|
|
282
|
-
${() => loading() ? html`
|
|
283
|
-
<div class="spinner">Loading todos...</div>
|
|
284
|
-
) : html`
|
|
285
|
-
<ul class="todo-list">
|
|
286
|
-
${filteredTodos().map(todo => html`
|
|
287
|
-
<li class="todo-item">
|
|
288
|
-
<input
|
|
289
|
-
type="checkbox"
|
|
290
|
-
:checked=${todo.completed}
|
|
291
|
-
@change=${() => toggleTodo(todo.id, todo.completed)}
|
|
292
|
-
/>
|
|
293
|
-
<span class:completed=${todo.completed}>${todo.text}</span>
|
|
294
|
-
<button @click=${() => deleteTodo(todo.id)}>🗑️</button>
|
|
295
|
-
</li>
|
|
296
|
-
`)}
|
|
297
|
-
</ul>
|
|
298
|
-
`}
|
|
299
|
-
</div>
|
|
300
|
-
`;
|
|
301
|
-
};
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### Infinite Scroll with Pagination
|
|
305
|
-
|
|
306
|
-
```javascript
|
|
307
|
-
import { $, html } from 'sigpro';
|
|
308
|
-
|
|
309
|
-
const InfiniteScroll = () => {
|
|
310
|
-
const posts = $([]);
|
|
311
|
-
const page = $(1);
|
|
312
|
-
const loading = $(false);
|
|
313
|
-
const hasMore = $(true);
|
|
314
|
-
const error = $(null);
|
|
315
|
-
|
|
316
|
-
const loadMore = async () => {
|
|
317
|
-
if (loading() || !hasMore()) return;
|
|
318
|
-
|
|
319
|
-
const data = await $.fetch('/api/posts', {
|
|
320
|
-
page: page(),
|
|
321
|
-
limit: 10
|
|
322
|
-
}, loading);
|
|
323
|
-
|
|
324
|
-
if (data) {
|
|
325
|
-
if (data.posts.length === 0) {
|
|
326
|
-
hasMore(false);
|
|
327
|
-
} else {
|
|
328
|
-
posts([...posts(), ...data.posts]);
|
|
329
|
-
page(p => p + 1);
|
|
330
|
-
}
|
|
331
|
-
} else {
|
|
332
|
-
error('Failed to load posts');
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
// Intersection Observer for infinite scroll
|
|
337
|
-
$.effect(() => {
|
|
338
|
-
const observer = new IntersectionObserver(
|
|
339
|
-
(entries) => {
|
|
340
|
-
if (entries[0].isIntersecting) {
|
|
341
|
-
loadMore();
|
|
342
|
-
}
|
|
343
|
-
},
|
|
344
|
-
{ threshold: 0.1 }
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
const sentinel = document.getElementById('sentinel');
|
|
348
|
-
if (sentinel) observer.observe(sentinel);
|
|
349
|
-
|
|
350
|
-
return () => observer.disconnect();
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// Initial load
|
|
354
|
-
loadMore();
|
|
355
|
-
|
|
356
|
-
return html`
|
|
357
|
-
<div class="infinite-scroll">
|
|
358
|
-
<h1>Posts</h1>
|
|
359
|
-
|
|
360
|
-
<div class="posts">
|
|
361
|
-
${posts().map(post => html`
|
|
362
|
-
<article class="post">
|
|
363
|
-
<h2>${post.title}</h2>
|
|
364
|
-
<p>${post.body}</p>
|
|
365
|
-
<small>By ${post.author}</small>
|
|
366
|
-
</article>
|
|
367
|
-
`)}
|
|
368
|
-
</div>
|
|
369
|
-
|
|
370
|
-
<div id="sentinel" class="sentinel">
|
|
371
|
-
${() => {
|
|
372
|
-
if (loading()) {
|
|
373
|
-
return html`<div class="spinner">Loading more...</div>`;
|
|
374
|
-
}
|
|
375
|
-
if (error()) {
|
|
376
|
-
return html`<div class="error">${error()}</div>`;
|
|
377
|
-
}
|
|
378
|
-
if (!hasMore()) {
|
|
379
|
-
return html`<div class="end">No more posts</div>`;
|
|
380
|
-
}
|
|
381
|
-
return '';
|
|
382
|
-
}}
|
|
383
|
-
</div>
|
|
384
|
-
</div>
|
|
385
|
-
`;
|
|
386
|
-
};
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
### Search with Debounce
|
|
390
|
-
|
|
391
|
-
```javascript
|
|
392
|
-
import { $, html } from 'sigpro';
|
|
393
|
-
|
|
394
|
-
const SearchComponent = () => {
|
|
395
|
-
const query = $('');
|
|
396
|
-
const results = $([]);
|
|
397
|
-
const loading = $(false);
|
|
398
|
-
const error = $(null);
|
|
399
|
-
let searchTimeout;
|
|
400
|
-
|
|
401
|
-
const performSearch = async (searchQuery) => {
|
|
402
|
-
if (!searchQuery.trim()) {
|
|
403
|
-
results([]);
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const data = await $.fetch('/api/search', {
|
|
408
|
-
q: searchQuery
|
|
409
|
-
}, loading);
|
|
410
|
-
|
|
411
|
-
if (data) {
|
|
412
|
-
results(data);
|
|
413
|
-
} else {
|
|
414
|
-
error('Search failed');
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
// Debounced search
|
|
419
|
-
$.effect(() => {
|
|
420
|
-
const searchQuery = query();
|
|
421
|
-
|
|
422
|
-
clearTimeout(searchTimeout);
|
|
423
|
-
|
|
424
|
-
if (searchQuery.length < 2) {
|
|
425
|
-
results([]);
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
searchTimeout = setTimeout(() => {
|
|
430
|
-
performSearch(searchQuery);
|
|
431
|
-
}, 300);
|
|
432
|
-
|
|
433
|
-
return () => clearTimeout(searchTimeout);
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
return html`
|
|
437
|
-
<div class="search">
|
|
438
|
-
<div class="search-box">
|
|
439
|
-
<input
|
|
440
|
-
type="search"
|
|
441
|
-
:value=${query}
|
|
442
|
-
placeholder="Search..."
|
|
443
|
-
class="search-input"
|
|
444
|
-
/>
|
|
445
|
-
${() => loading() ? html`
|
|
446
|
-
<span class="spinner-small">⌛</span>
|
|
447
|
-
) : ''}
|
|
448
|
-
</div>
|
|
449
|
-
|
|
450
|
-
${() => {
|
|
451
|
-
if (error()) {
|
|
452
|
-
return html`<div class="error">${error()}</div>`;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (results().length > 0) {
|
|
456
|
-
return html`
|
|
457
|
-
<ul class="results">
|
|
458
|
-
${results().map(item => html`
|
|
459
|
-
<li class="result-item">
|
|
460
|
-
<h3>${item.title}</h3>
|
|
461
|
-
<p>${item.description}</p>
|
|
462
|
-
</li>
|
|
463
|
-
`)}
|
|
464
|
-
</ul>
|
|
465
|
-
`;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (query().length >= 2 && !loading()) {
|
|
469
|
-
return html`<p class="no-results">No results found</p>`;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return '';
|
|
473
|
-
}}
|
|
474
|
-
</div>
|
|
475
|
-
`;
|
|
476
|
-
};
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
### Form Submission
|
|
480
|
-
|
|
481
|
-
```javascript
|
|
482
|
-
import { $, html } from 'sigpro';
|
|
483
|
-
|
|
484
|
-
const ContactForm = () => {
|
|
485
|
-
const formData = $({
|
|
486
|
-
name: '',
|
|
487
|
-
email: '',
|
|
488
|
-
message: ''
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
const submitting = $(false);
|
|
492
|
-
const submitError = $(null);
|
|
493
|
-
const submitSuccess = $(false);
|
|
494
|
-
|
|
495
|
-
const handleSubmit = async (e) => {
|
|
496
|
-
e.preventDefault();
|
|
497
|
-
|
|
498
|
-
submitError(null);
|
|
499
|
-
submitSuccess(false);
|
|
500
|
-
|
|
501
|
-
const result = await $.fetch('/api/contact', formData(), submitting);
|
|
502
|
-
|
|
503
|
-
if (result) {
|
|
504
|
-
submitSuccess(true);
|
|
505
|
-
formData({ name: '', email: '', message: '' });
|
|
506
|
-
} else {
|
|
507
|
-
submitError('Failed to send message. Please try again.');
|
|
508
|
-
}
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
const updateField = (field, value) => {
|
|
512
|
-
formData({
|
|
513
|
-
...formData(),
|
|
514
|
-
[field]: value
|
|
515
|
-
});
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
return html`
|
|
519
|
-
<form class="contact-form" @submit=${handleSubmit}>
|
|
520
|
-
<h2>Contact Us</h2>
|
|
521
|
-
|
|
522
|
-
<div class="form-group">
|
|
523
|
-
<label for="name">Name:</label>
|
|
524
|
-
<input
|
|
525
|
-
type="text"
|
|
526
|
-
id="name"
|
|
527
|
-
:value=${() => formData().name}
|
|
528
|
-
@input=${(e) => updateField('name', e.target.value)}
|
|
529
|
-
required
|
|
530
|
-
?disabled=${submitting}
|
|
531
|
-
/>
|
|
532
|
-
</div>
|
|
533
|
-
|
|
534
|
-
<div class="form-group">
|
|
535
|
-
<label for="email">Email:</label>
|
|
536
|
-
<input
|
|
537
|
-
type="email"
|
|
538
|
-
id="email"
|
|
539
|
-
:value=${() => formData().email}
|
|
540
|
-
@input=${(e) => updateField('email', e.target.value)}
|
|
541
|
-
required
|
|
542
|
-
?disabled=${submitting}
|
|
543
|
-
/>
|
|
544
|
-
</div>
|
|
545
|
-
|
|
546
|
-
<div class="form-group">
|
|
547
|
-
<label for="message">Message:</label>
|
|
548
|
-
<textarea
|
|
549
|
-
id="message"
|
|
550
|
-
:value=${() => formData().message}
|
|
551
|
-
@input=${(e) => updateField('message', e.target.value)}
|
|
552
|
-
required
|
|
553
|
-
rows="5"
|
|
554
|
-
?disabled=${submitting}
|
|
555
|
-
></textarea>
|
|
556
|
-
</div>
|
|
557
|
-
|
|
558
|
-
${() => {
|
|
559
|
-
if (submitting()) {
|
|
560
|
-
return html`<div class="submitting">Sending...</div>`;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (submitError()) {
|
|
564
|
-
return html`<div class="error">${submitError()}</div>`;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
if (submitSuccess()) {
|
|
568
|
-
return html`<div class="success">Message sent successfully!</div>`;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return '';
|
|
572
|
-
}}
|
|
573
|
-
|
|
574
|
-
<button
|
|
575
|
-
type="submit"
|
|
576
|
-
?disabled=${submitting}
|
|
577
|
-
>
|
|
578
|
-
Send Message
|
|
579
|
-
</button>
|
|
580
|
-
</form>
|
|
581
|
-
`;
|
|
582
|
-
};
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
### Real-time Dashboard with Multiple Endpoints
|
|
586
|
-
|
|
587
|
-
```javascript
|
|
588
|
-
import { $, html } from 'sigpro';
|
|
589
|
-
|
|
590
|
-
const Dashboard = () => {
|
|
591
|
-
// Multiple data streams
|
|
592
|
-
const metrics = $({});
|
|
593
|
-
const alerts = $([]);
|
|
594
|
-
const logs = $([]);
|
|
595
|
-
|
|
596
|
-
const loading = $({
|
|
597
|
-
metrics: false,
|
|
598
|
-
alerts: false,
|
|
599
|
-
logs: false
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
const refreshInterval = $(5000); // 5 seconds
|
|
603
|
-
|
|
604
|
-
const fetchMetrics = async () => {
|
|
605
|
-
const data = await $.fetch('/api/metrics', {}, loading().metrics);
|
|
606
|
-
if (data) metrics(data);
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
const fetchAlerts = async () => {
|
|
610
|
-
const data = await $.fetch('/api/alerts', {}, loading().alerts);
|
|
611
|
-
if (data) alerts(data);
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
const fetchLogs = async () => {
|
|
615
|
-
const data = await $.fetch('/api/logs', {
|
|
616
|
-
limit: 50
|
|
617
|
-
}, loading().logs);
|
|
618
|
-
if (data) logs(data);
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
// Auto-refresh all data
|
|
622
|
-
$.effect(() => {
|
|
623
|
-
fetchMetrics();
|
|
624
|
-
fetchAlerts();
|
|
625
|
-
fetchLogs();
|
|
626
|
-
|
|
627
|
-
const interval = setInterval(() => {
|
|
628
|
-
fetchMetrics();
|
|
629
|
-
fetchAlerts();
|
|
630
|
-
}, refreshInterval());
|
|
631
|
-
|
|
632
|
-
return () => clearInterval(interval);
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
return html`
|
|
636
|
-
<div class="dashboard">
|
|
637
|
-
<header>
|
|
638
|
-
<h1>System Dashboard</h1>
|
|
639
|
-
<div class="refresh-control">
|
|
640
|
-
<label>
|
|
641
|
-
Refresh interval:
|
|
642
|
-
<select :value=${refreshInterval} @change=${(e) => refreshInterval(parseInt(e.target.value))}>
|
|
643
|
-
<option value="2000">2 seconds</option>
|
|
644
|
-
<option value="5000">5 seconds</option>
|
|
645
|
-
<option value="10000">10 seconds</option>
|
|
646
|
-
<option value="30000">30 seconds</option>
|
|
647
|
-
</select>
|
|
648
|
-
</label>
|
|
649
|
-
</div>
|
|
650
|
-
</header>
|
|
651
|
-
|
|
652
|
-
<div class="dashboard-grid">
|
|
653
|
-
<!-- Metrics Panel -->
|
|
654
|
-
<div class="panel metrics">
|
|
655
|
-
<h2>System Metrics</h2>
|
|
656
|
-
${() => loading().metrics ? html`
|
|
657
|
-
<div class="spinner">Loading metrics...</div>
|
|
658
|
-
) : html`
|
|
659
|
-
<div class="metrics-grid">
|
|
660
|
-
<div class="metric">
|
|
661
|
-
<label>CPU</label>
|
|
662
|
-
<span>${metrics().cpu || 0}%</span>
|
|
663
|
-
</div>
|
|
664
|
-
<div class="metric">
|
|
665
|
-
<label>Memory</label>
|
|
666
|
-
<span>${metrics().memory || 0}%</span>
|
|
667
|
-
</div>
|
|
668
|
-
<div class="metric">
|
|
669
|
-
<label>Requests</label>
|
|
670
|
-
<span>${metrics().requests || 0}/s</span>
|
|
671
|
-
</div>
|
|
672
|
-
</div>
|
|
673
|
-
`}
|
|
674
|
-
</div>
|
|
675
|
-
|
|
676
|
-
<!-- Alerts Panel -->
|
|
677
|
-
<div class="panel alerts">
|
|
678
|
-
<h2>Active Alerts</h2>
|
|
679
|
-
${() => loading().alerts ? html`
|
|
680
|
-
<div class="spinner">Loading alerts...</div>
|
|
681
|
-
) : alerts().length > 0 ? html`
|
|
682
|
-
<ul>
|
|
683
|
-
${alerts().map(alert => html`
|
|
684
|
-
<li class="alert ${alert.severity}">
|
|
685
|
-
<strong>${alert.type}</strong>
|
|
686
|
-
<p>${alert.message}</p>
|
|
687
|
-
<small>${new Date(alert.timestamp).toLocaleTimeString()}</small>
|
|
688
|
-
</li>
|
|
689
|
-
`)}
|
|
690
|
-
</ul>
|
|
691
|
-
) : html`
|
|
692
|
-
<p class="no-data">No active alerts</p>
|
|
693
|
-
`}
|
|
694
|
-
</div>
|
|
695
|
-
|
|
696
|
-
<!-- Logs Panel -->
|
|
697
|
-
<div class="panel logs">
|
|
698
|
-
<h2>Recent Logs</h2>
|
|
699
|
-
${() => loading().logs ? html`
|
|
700
|
-
<div class="spinner">Loading logs...</div>
|
|
701
|
-
) : html`
|
|
702
|
-
<ul>
|
|
703
|
-
${logs().map(log => html`
|
|
704
|
-
<li class="log ${log.level}">
|
|
705
|
-
<span class="timestamp">${new Date(log.timestamp).toLocaleTimeString()}</span>
|
|
706
|
-
<span class="message">${log.message}</span>
|
|
707
|
-
</li>
|
|
708
|
-
`)}
|
|
709
|
-
</ul>
|
|
710
|
-
`}
|
|
711
|
-
</div>
|
|
712
|
-
</div>
|
|
713
|
-
</div>
|
|
714
|
-
`;
|
|
715
|
-
};
|
|
716
|
-
```
|
|
717
|
-
|
|
718
|
-
### File Upload
|
|
719
|
-
|
|
720
|
-
```javascript
|
|
721
|
-
import { $, html } from 'sigpro';
|
|
722
|
-
|
|
723
|
-
const FileUploader = () => {
|
|
724
|
-
const files = $([]);
|
|
725
|
-
const uploading = $(false);
|
|
726
|
-
const uploadProgress = $({});
|
|
727
|
-
const uploadResults = $([]);
|
|
728
|
-
|
|
729
|
-
const handleFileSelect = (e) => {
|
|
730
|
-
files([...e.target.files]);
|
|
731
|
-
};
|
|
732
|
-
|
|
733
|
-
const uploadFiles = async () => {
|
|
734
|
-
if (files().length === 0) return;
|
|
735
|
-
|
|
736
|
-
uploading(true);
|
|
737
|
-
uploadResults([]);
|
|
738
|
-
|
|
739
|
-
for (const file of files()) {
|
|
740
|
-
const formData = new FormData();
|
|
741
|
-
formData.append('file', file);
|
|
742
|
-
|
|
743
|
-
// Track progress for this file
|
|
744
|
-
uploadProgress({
|
|
745
|
-
...uploadProgress(),
|
|
746
|
-
[file.name]: 0
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
try {
|
|
750
|
-
// Custom fetch for FormData
|
|
751
|
-
const response = await fetch('/api/upload', {
|
|
752
|
-
method: 'POST',
|
|
753
|
-
body: formData
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
const result = await response.json();
|
|
757
|
-
|
|
758
|
-
uploadResults([
|
|
759
|
-
...uploadResults(),
|
|
760
|
-
{ file: file.name, success: true, result }
|
|
761
|
-
]);
|
|
762
|
-
} catch (error) {
|
|
763
|
-
uploadResults([
|
|
764
|
-
...uploadResults(),
|
|
765
|
-
{ file: file.name, success: false, error: error.message }
|
|
766
|
-
]);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
uploadProgress({
|
|
770
|
-
...uploadProgress(),
|
|
771
|
-
[file.name]: 100
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
uploading(false);
|
|
776
|
-
};
|
|
777
|
-
|
|
778
|
-
return html`
|
|
779
|
-
<div class="file-uploader">
|
|
780
|
-
<h2>Upload Files</h2>
|
|
781
|
-
|
|
782
|
-
<input
|
|
783
|
-
type="file"
|
|
784
|
-
multiple
|
|
785
|
-
@change=${handleFileSelect}
|
|
786
|
-
?disabled=${uploading}
|
|
787
|
-
/>
|
|
788
|
-
|
|
789
|
-
${() => files().length > 0 ? html`
|
|
790
|
-
<div class="file-list">
|
|
791
|
-
<h3>Selected Files:</h3>
|
|
792
|
-
<ul>
|
|
793
|
-
${files().map(file => html`
|
|
794
|
-
<li>
|
|
795
|
-
${file.name} (${(file.size / 1024).toFixed(2)} KB)
|
|
796
|
-
${() => uploadProgress()[file.name] ? html`
|
|
797
|
-
<progress value="${uploadProgress()[file.name]}" max="100"></progress>
|
|
798
|
-
) : ''}
|
|
799
|
-
</li>
|
|
800
|
-
`)}
|
|
801
|
-
</ul>
|
|
802
|
-
|
|
803
|
-
<button
|
|
804
|
-
@click=${uploadFiles}
|
|
805
|
-
?disabled=${uploading}
|
|
806
|
-
>
|
|
807
|
-
${() => uploading() ? 'Uploading...' : 'Upload Files'}
|
|
808
|
-
</button>
|
|
809
|
-
</div>
|
|
810
|
-
` : ''}
|
|
811
|
-
|
|
812
|
-
${() => uploadResults().length > 0 ? html`
|
|
813
|
-
<div class="upload-results">
|
|
814
|
-
<h3>Upload Results:</h3>
|
|
815
|
-
<ul>
|
|
816
|
-
${uploadResults().map(result => html`
|
|
817
|
-
<li class="${result.success ? 'success' : 'error'}">
|
|
818
|
-
${result.file}:
|
|
819
|
-
${result.success ? 'Uploaded successfully' : `Failed: ${result.error}`}
|
|
820
|
-
</li>
|
|
821
|
-
`)}
|
|
822
|
-
</ul>
|
|
823
|
-
</div>
|
|
824
|
-
` : ''}
|
|
825
|
-
</div>
|
|
826
|
-
`;
|
|
827
|
-
};
|
|
828
|
-
```
|
|
829
|
-
|
|
830
|
-
### Retry Logic
|
|
831
|
-
|
|
832
|
-
```javascript
|
|
833
|
-
import { $ } from 'sigpro';
|
|
834
|
-
|
|
835
|
-
// Enhanced fetch with retry
|
|
836
|
-
const fetchWithRetry = async (url, data, loading, maxRetries = 3) => {
|
|
837
|
-
let lastError;
|
|
838
|
-
|
|
839
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
840
|
-
try {
|
|
841
|
-
if (loading) loading(true);
|
|
842
|
-
|
|
843
|
-
const result = await $.fetch(url, data);
|
|
844
|
-
if (result !== null) {
|
|
845
|
-
return result;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// If we get null but no error, wait and retry
|
|
849
|
-
if (attempt < maxRetries) {
|
|
850
|
-
await new Promise(resolve =>
|
|
851
|
-
setTimeout(resolve, Math.pow(2, attempt) * 1000) // Exponential backoff
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
|
-
} catch (error) {
|
|
855
|
-
lastError = error;
|
|
856
|
-
console.warn(`Attempt ${attempt} failed:`, error);
|
|
857
|
-
|
|
858
|
-
if (attempt < maxRetries) {
|
|
859
|
-
await new Promise(resolve =>
|
|
860
|
-
setTimeout(resolve, Math.pow(2, attempt) * 1000)
|
|
861
|
-
);
|
|
862
|
-
}
|
|
863
|
-
} finally {
|
|
864
|
-
if (attempt === maxRetries && loading) {
|
|
865
|
-
loading(false);
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
console.error('All retry attempts failed:', lastError);
|
|
871
|
-
return null;
|
|
872
|
-
};
|
|
873
|
-
|
|
874
|
-
// Usage
|
|
875
|
-
const loading = $(false);
|
|
876
|
-
const data = await fetchWithRetry('/api/unreliable-endpoint', {}, loading, 5);
|
|
877
|
-
```
|
|
878
|
-
|
|
879
|
-
## 🎯 Best Practices
|
|
880
|
-
|
|
881
|
-
### 1. Always Handle Null Responses
|
|
882
|
-
|
|
883
|
-
```javascript
|
|
884
|
-
// ❌ Don't assume success
|
|
885
|
-
const data = await $.fetch('/api/data');
|
|
886
|
-
console.log(data.property); // Might throw if data is null
|
|
887
|
-
|
|
888
|
-
// ✅ Check for null
|
|
889
|
-
const data = await $.fetch('/api/data');
|
|
890
|
-
if (data) {
|
|
891
|
-
console.log(data.property);
|
|
892
|
-
} else {
|
|
893
|
-
showError('Failed to load data');
|
|
894
|
-
}
|
|
895
|
-
```
|
|
896
|
-
|
|
897
|
-
### 2. Use with Effects for Reactivity
|
|
898
|
-
|
|
899
|
-
```javascript
|
|
900
|
-
// ❌ Manual fetching
|
|
901
|
-
button.addEventListener('click', async () => {
|
|
902
|
-
const data = await $.fetch('/api/data');
|
|
903
|
-
updateUI(data);
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
// ✅ Reactive fetching
|
|
907
|
-
const trigger = $(false);
|
|
908
|
-
|
|
909
|
-
$.effect(() => {
|
|
910
|
-
if (trigger()) {
|
|
911
|
-
$.fetch('/api/data').then(data => {
|
|
912
|
-
if (data) updateUI(data);
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
trigger(true); // Triggers fetch
|
|
918
|
-
```
|
|
919
|
-
|
|
920
|
-
### 3. Combine with Loading Signals
|
|
921
|
-
|
|
922
|
-
```javascript
|
|
923
|
-
// ✅ Always show loading state
|
|
924
|
-
const loading = $(false);
|
|
925
|
-
const data = $(null);
|
|
926
|
-
|
|
927
|
-
async function load() {
|
|
928
|
-
const result = await $.fetch('/api/data', {}, loading);
|
|
929
|
-
if (result) data(result);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// In template
|
|
933
|
-
html`
|
|
934
|
-
<div>
|
|
935
|
-
${() => loading() ? '<Spinner />' :
|
|
936
|
-
data() ? '<Data />' :
|
|
937
|
-
'<Empty />'}
|
|
938
|
-
</div>
|
|
939
|
-
`;
|
|
940
|
-
```
|
|
941
|
-
|
|
942
|
-
### 4. Cancel In-flight Requests
|
|
943
|
-
|
|
944
|
-
```javascript
|
|
945
|
-
// ✅ Use AbortController with effects
|
|
946
|
-
let controller;
|
|
947
|
-
|
|
948
|
-
$.effect(() => {
|
|
949
|
-
if (controller) {
|
|
950
|
-
controller.abort();
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
controller = new AbortController();
|
|
954
|
-
|
|
955
|
-
fetch(url, { signal: controller.signal })
|
|
956
|
-
.then(res => res.json())
|
|
957
|
-
.then(data => {
|
|
958
|
-
if (!controller.signal.aborted) {
|
|
959
|
-
updateData(data);
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
|
|
963
|
-
return () => controller.abort();
|
|
964
|
-
});
|
|
965
|
-
```
|
|
966
|
-
|
|
967
|
-
## 📊 Error Handling
|
|
968
|
-
|
|
969
|
-
### Basic Error Handling
|
|
970
|
-
|
|
971
|
-
```javascript
|
|
972
|
-
const data = await $.fetch('/api/data');
|
|
973
|
-
if (!data) {
|
|
974
|
-
// Handle error (show message, retry, etc.)
|
|
975
|
-
}
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
### With Error Signal
|
|
979
|
-
|
|
980
|
-
```javascript
|
|
981
|
-
const data = $(null);
|
|
982
|
-
const error = $(null);
|
|
983
|
-
const loading = $(false);
|
|
984
|
-
|
|
985
|
-
async function loadData() {
|
|
986
|
-
error(null);
|
|
987
|
-
const result = await $.fetch('/api/data', {}, loading);
|
|
988
|
-
|
|
989
|
-
if (result) {
|
|
990
|
-
data(result);
|
|
991
|
-
} else {
|
|
992
|
-
error('Failed to load data');
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
```
|
|
996
|
-
---
|
|
997
|
-
|
|
998
|
-
> **Pro Tip:** Combine `$.fetch` with `$.effect` and loading signals for a complete reactive data fetching solution. The loading signal integration makes it trivial to show loading states in your UI.
|