asasvirtuais 3.0.0 → 4.0.1
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 +495 -181
- package/package.json +46 -38
- package/packages/action.tsx +76 -0
- package/packages/fields.tsx +65 -0
- package/packages/form.tsx +184 -0
- package/packages/hooks.tsx +120 -0
- package/packages/interface.ts +52 -0
- package/packages/post/fields.tsx +73 -0
- package/packages/post/provider.tsx +17 -0
- package/packages/post/schema.ts +35 -0
- package/packages/providers.tsx +174 -0
- package/tsconfig.json +39 -38
- package/packages/fetch-interface/hooks.tsx +0 -126
- package/packages/fetch-interface/index.tsx +0 -93
- package/packages/fetch-interface/interface-provider.tsx +0 -11
- package/packages/fetch-interface/interface.ts +0 -68
- package/packages/forms/action.tsx +0 -93
- package/packages/forms/fields.tsx +0 -63
- package/packages/forms/hooks.tsx +0 -128
- package/packages/forms/index.tsx +0 -55
- package/packages/interface/action.tsx +0 -93
- package/packages/interface/fields.tsx +0 -63
- package/packages/interface/hooks.tsx +0 -128
- package/packages/interface/index.tsx +0 -388
- package/packages/interface/indexed-interface.tsx +0 -129
- package/packages/interface/interface-provider.tsx +0 -11
- package/packages/interface/interface.ts +0 -68
- package/packages/yaml-interface/hooks.tsx +0 -128
- package/packages/yaml-interface/index.ts +0 -147
- package/packages/yaml-interface/interface.ts +0 -68
package/README.md
CHANGED
|
@@ -1,181 +1,495 @@
|
|
|
1
|
-
# asasvirtuais
|
|
2
|
-
|
|
3
|
-
React
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
###
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
1
|
+
# asasvirtuais
|
|
2
|
+
|
|
3
|
+
A React framework for building full-stack apps where code is organized by feature, not by layer.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Primitives
|
|
8
|
+
|
|
9
|
+
Three building blocks, each usable on its own.
|
|
10
|
+
|
|
11
|
+
### `FieldsProvider` — field state
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { FieldsProvider } from 'asasvirtuais/fields'
|
|
15
|
+
|
|
16
|
+
<FieldsProvider defaults={{ title: '', done: false }}>
|
|
17
|
+
{({ fields, setField }) => (
|
|
18
|
+
<div>
|
|
19
|
+
<input value={fields.title} onChange={e => setField('title', e.target.value)} />
|
|
20
|
+
<input type="checkbox" checked={fields.done} onChange={e => setField('done', e.target.checked)} />
|
|
21
|
+
</div>
|
|
22
|
+
)}
|
|
23
|
+
</FieldsProvider>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### `ActionProvider` — async action state
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { ActionProvider } from 'asasvirtuais/action'
|
|
30
|
+
|
|
31
|
+
<ActionProvider params={{ id: todo.id }} action={archiveTodo} onResult={() => router.push('/')}>
|
|
32
|
+
{({ submit, loading, error }) => (
|
|
33
|
+
<button onClick={submit} disabled={loading}>
|
|
34
|
+
{loading ? 'Archiving...' : 'Archive'}
|
|
35
|
+
</button>
|
|
36
|
+
)}
|
|
37
|
+
</ActionProvider>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### `Form` — fields + action together
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { Form } from 'asasvirtuais/form'
|
|
44
|
+
|
|
45
|
+
<Form defaults={{ email: '', password: '' }} action={login} onResult={handleResult}>
|
|
46
|
+
{({ fields, setField, submit, loading, error }) => (
|
|
47
|
+
<form onSubmit={submit}>
|
|
48
|
+
<input value={fields.email} onChange={e => setField('email', e.target.value)} />
|
|
49
|
+
<input type="password" value={fields.password} onChange={e => setField('password', e.target.value)} />
|
|
50
|
+
<button type="submit" disabled={loading}>Login</button>
|
|
51
|
+
{error && <p>{error.message}</p>}
|
|
52
|
+
</form>
|
|
53
|
+
)}
|
|
54
|
+
</Form>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Full-stack CRUD
|
|
60
|
+
|
|
61
|
+
The framework provides a schema-first CRUD layer where create, update, and remove operations automatically keep the UI in sync through a reactive index — no manual state updates, no refetching.
|
|
62
|
+
|
|
63
|
+
### Project structure
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
app/
|
|
67
|
+
├── schema.ts # All table schemas in one place
|
|
68
|
+
├── actions.ts # Server actions — the backend
|
|
69
|
+
├── providers.tsx # App-level providers
|
|
70
|
+
├── layout.tsx
|
|
71
|
+
├── todos/
|
|
72
|
+
│ ├── schema.ts # Schema + types
|
|
73
|
+
│ ├── fields.tsx # Input components
|
|
74
|
+
│ ├── forms.tsx # Create / Update / Delete / Filter forms
|
|
75
|
+
│ ├── components.tsx # Display components
|
|
76
|
+
│ └── provider.tsx # TableProvider + hook
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### 1. Schema
|
|
82
|
+
|
|
83
|
+
Each model defines `readable` (what comes out of the database) and `writable` (what users can create or modify):
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// app/todos/schema.ts
|
|
87
|
+
import z from 'zod'
|
|
88
|
+
|
|
89
|
+
export const readable = z.object({
|
|
90
|
+
id: z.string(),
|
|
91
|
+
title: z.string(),
|
|
92
|
+
done: z.boolean(),
|
|
93
|
+
author: z.string(),
|
|
94
|
+
createdAt: z.string(),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
export const writable = readable.pick({
|
|
98
|
+
title: true,
|
|
99
|
+
done: true,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
export const schema = { readable, writable }
|
|
103
|
+
|
|
104
|
+
export type Readable = z.infer<typeof readable>
|
|
105
|
+
export type Writable = z.infer<typeof writable>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
All models are assembled into a single database schema file:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// app/schema.ts
|
|
112
|
+
import { schema as todosSchema } from './todos/schema'
|
|
113
|
+
import { schema as tagsSchema } from './tags/schema'
|
|
114
|
+
|
|
115
|
+
export const schema = {
|
|
116
|
+
todos: todosSchema,
|
|
117
|
+
tags: tagsSchema,
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### 2. Server actions — the backend
|
|
124
|
+
|
|
125
|
+
The backend is plain Next.js server actions. You pass them directly to the provider — no REST routes, no fetch client needed:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// app/actions.ts
|
|
129
|
+
'use server'
|
|
130
|
+
|
|
131
|
+
import { firestoreInterface } from 'asasvirtuais-firebase/interface'
|
|
132
|
+
import { auth0 } from '@/lib/auth0'
|
|
133
|
+
|
|
134
|
+
const db = firestoreInterface()
|
|
135
|
+
|
|
136
|
+
function clean(obj: any): any {
|
|
137
|
+
if (Array.isArray(obj)) return obj.map(clean)
|
|
138
|
+
if (obj !== null && typeof obj === 'object' && obj.constructor === Object) {
|
|
139
|
+
return Object.fromEntries(
|
|
140
|
+
Object.entries(obj)
|
|
141
|
+
.filter(([_, v]) => v !== undefined)
|
|
142
|
+
.map(([k, v]) => [k, clean(v)])
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
return obj
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const find = async (props: any) => db.find(props)
|
|
149
|
+
|
|
150
|
+
export const list = async (props: any) => db.list(props)
|
|
151
|
+
|
|
152
|
+
export const create = async (props: any) => {
|
|
153
|
+
const session = await auth0.getSession()
|
|
154
|
+
if (!session?.user) throw new Error('Unauthorized')
|
|
155
|
+
|
|
156
|
+
const data = clean({ ...props.data })
|
|
157
|
+
|
|
158
|
+
if (props.table === 'todos') {
|
|
159
|
+
data.author = session.user.id
|
|
160
|
+
data.done = false
|
|
161
|
+
data.createdAt = new Date().toISOString()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return db.create({ ...props, data })
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const update = async (props: any) => {
|
|
168
|
+
const session = await auth0.getSession()
|
|
169
|
+
if (!session?.user) throw new Error('Unauthorized')
|
|
170
|
+
|
|
171
|
+
const data = clean({ ...props.data })
|
|
172
|
+
|
|
173
|
+
if (props.table === 'todos') {
|
|
174
|
+
const existing = await db.find({ table: 'todos', id: props.id })
|
|
175
|
+
if (existing.author !== session.user.id) throw new Error('Forbidden')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return db.update({ ...props, data })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const remove = async (props: any) => {
|
|
182
|
+
const session = await auth0.getSession()
|
|
183
|
+
if (!session?.user) throw new Error('Unauthorized')
|
|
184
|
+
|
|
185
|
+
if (props.table === 'todos') {
|
|
186
|
+
const existing = await db.find({ table: 'todos', id: props.id })
|
|
187
|
+
if (existing.author !== session.user.id) throw new Error('Forbidden')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return db.remove(props)
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This is where business logic lives: auth, default values, permission checks. All in one place, all readable top to bottom.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
### 3. Providers
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
// app/providers.tsx
|
|
202
|
+
import { InterfaceProvider } from 'asasvirtuais/interface-provider'
|
|
203
|
+
import { DatabaseProvider } from 'asasvirtuais/react-interface'
|
|
204
|
+
import { TodosProvider } from '@/app/todos/provider'
|
|
205
|
+
import * as db from '@/app/actions'
|
|
206
|
+
|
|
207
|
+
export default function AppProviders({ children }: { children: React.ReactNode }) {
|
|
208
|
+
return (
|
|
209
|
+
<InterfaceProvider
|
|
210
|
+
find={db.find}
|
|
211
|
+
list={db.list}
|
|
212
|
+
create={db.create}
|
|
213
|
+
update={db.update}
|
|
214
|
+
remove={db.remove}
|
|
215
|
+
>
|
|
216
|
+
<DatabaseProvider>
|
|
217
|
+
<TodosProvider>
|
|
218
|
+
{children}
|
|
219
|
+
</TodosProvider>
|
|
220
|
+
</DatabaseProvider>
|
|
221
|
+
</InterfaceProvider>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
// app/layout.tsx
|
|
228
|
+
import AppProviders from './providers'
|
|
229
|
+
|
|
230
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
231
|
+
return (
|
|
232
|
+
<html lang="en">
|
|
233
|
+
<body>
|
|
234
|
+
<AppProviders>
|
|
235
|
+
{children}
|
|
236
|
+
</AppProviders>
|
|
237
|
+
</body>
|
|
238
|
+
</html>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### 4. Model provider
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
// app/todos/provider.tsx
|
|
249
|
+
'use client'
|
|
250
|
+
import { TableProvider, useTable } from 'asasvirtuais/react-interface'
|
|
251
|
+
import { useInterface } from 'asasvirtuais/interface-provider'
|
|
252
|
+
import { schema } from '.'
|
|
253
|
+
|
|
254
|
+
export function useTodos() {
|
|
255
|
+
return useTable('todos', schema)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function TodosProvider({ children }: { children: React.ReactNode }) {
|
|
259
|
+
return (
|
|
260
|
+
<TableProvider table="todos" schema={schema} interface={useInterface()}>
|
|
261
|
+
{children}
|
|
262
|
+
</TableProvider>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
### 5. UI
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
// app/todos/page.tsx
|
|
273
|
+
'use client'
|
|
274
|
+
import { useEffect } from 'react'
|
|
275
|
+
import { useTodos } from './provider'
|
|
276
|
+
import { SingleProvider } from 'asasvirtuais/react-interface'
|
|
277
|
+
import { schema } from '.'
|
|
278
|
+
import { TodoItem } from './components'
|
|
279
|
+
import { CreateTodo } from './forms'
|
|
280
|
+
|
|
281
|
+
export default function TodosPage() {
|
|
282
|
+
const { array, list } = useTodos()
|
|
283
|
+
|
|
284
|
+
useEffect(() => { list.trigger({}) }, [])
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div>
|
|
288
|
+
<CreateTodo />
|
|
289
|
+
{array.map(todo => (
|
|
290
|
+
<SingleProvider key={todo.id} id={todo.id} table="todos" schema={schema}>
|
|
291
|
+
<TodoItem />
|
|
292
|
+
</SingleProvider>
|
|
293
|
+
))}
|
|
294
|
+
</div>
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
When `create` resolves, the item appears in `array` immediately. Same for `update` and `remove`.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Listing vs. filtering
|
|
304
|
+
|
|
305
|
+
### `useTable().list` — reactive, global
|
|
306
|
+
|
|
307
|
+
Use this when you want all records in the reactive index. Results live in `array` and stay in sync with every create, update, and remove automatically:
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
const { array, list } = useTodos()
|
|
311
|
+
|
|
312
|
+
useEffect(() => { list.trigger({}) }, [])
|
|
313
|
+
|
|
314
|
+
// array updates automatically when any todo is created, updated, or removed
|
|
315
|
+
return array.map(todo => (
|
|
316
|
+
<SingleProvider key={todo.id} id={todo.id} table="todos" schema={schema}>
|
|
317
|
+
<TodoItem />
|
|
318
|
+
</SingleProvider>
|
|
319
|
+
))
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### `FilterForm` — local, paginated, or conditional
|
|
323
|
+
|
|
324
|
+
Use `FilterForm` when you need pagination, live search, or results that belong to the component rather than the global index. Results live in `form.result` and only update when `submit` is called:
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
import { FilterForm } from 'asasvirtuais/form'
|
|
328
|
+
import { schema } from '.'
|
|
329
|
+
|
|
330
|
+
<FilterForm table="todos" schema={schema} defaults={{ query: { done: false } }} autoTrigger>
|
|
331
|
+
{({ result, loading, fields, setField, submit }) => (
|
|
332
|
+
<div>
|
|
333
|
+
<input
|
|
334
|
+
placeholder="Search..."
|
|
335
|
+
value={fields.query?.title ?? ''}
|
|
336
|
+
onChange={e => {
|
|
337
|
+
setField('query', { title: e.target.value })
|
|
338
|
+
submit()
|
|
339
|
+
}}
|
|
340
|
+
/>
|
|
341
|
+
{loading && <p>Loading...</p>}
|
|
342
|
+
{result?.map(todo => <p key={todo.id}>{todo.title}</p>)}
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
</FilterForm>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Async selector fields
|
|
351
|
+
|
|
352
|
+
When a form needs the user to pick a record from another table, `FilterForm` composes naturally inside a field component. The field reads and writes to the parent form's context via `useFields()` — no props needed to bridge them.
|
|
353
|
+
|
|
354
|
+
Say a todo can be tagged, and the user needs to search and select a tag while creating the todo:
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
// app/todos/fields.tsx
|
|
358
|
+
import { useFields } from 'asasvirtuais/fields'
|
|
359
|
+
import { FilterForm } from 'asasvirtuais/form'
|
|
360
|
+
import { schema as tagsSchema } from '@/app/tags'
|
|
361
|
+
|
|
362
|
+
export function TagSelectorField() {
|
|
363
|
+
// reads/writes to whatever Form or FieldsProvider this is rendered inside
|
|
364
|
+
const { fields, setField } = useFields<{ tagId: string }>()
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<FilterForm table="tags" schema={tagsSchema} defaults={{ query: {} }}>
|
|
368
|
+
{({ fields: search, setField: setSearch, submit, result }) => (
|
|
369
|
+
<div>
|
|
370
|
+
<input
|
|
371
|
+
placeholder="Search tags..."
|
|
372
|
+
onChange={e => {
|
|
373
|
+
setSearch('query', { name: e.target.value })
|
|
374
|
+
submit()
|
|
375
|
+
}}
|
|
376
|
+
/>
|
|
377
|
+
<ul>
|
|
378
|
+
{result?.map(tag => (
|
|
379
|
+
<li
|
|
380
|
+
key={tag.id}
|
|
381
|
+
onClick={() => setField('tagId', tag.id)}
|
|
382
|
+
style={{ fontWeight: fields.tagId === tag.id ? 'bold' : 'normal' }}
|
|
383
|
+
>
|
|
384
|
+
{tag.name}
|
|
385
|
+
</li>
|
|
386
|
+
))}
|
|
387
|
+
</ul>
|
|
388
|
+
</div>
|
|
389
|
+
)}
|
|
390
|
+
</FilterForm>
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Use it inside any form — it just works:
|
|
396
|
+
|
|
397
|
+
```tsx
|
|
398
|
+
// app/todos/forms.tsx
|
|
399
|
+
import { CreateForm } from 'asasvirtuais/form'
|
|
400
|
+
import { schema } from '.'
|
|
401
|
+
import { TitleField, TagSelectorField } from './fields'
|
|
402
|
+
|
|
403
|
+
export function CreateTodo({ onSuccess }: { onSuccess?: () => void }) {
|
|
404
|
+
return (
|
|
405
|
+
<CreateForm table="todos" schema={schema} defaults={{ title: '', tagId: '' }} onSuccess={onSuccess}>
|
|
406
|
+
{({ submit, loading }) => (
|
|
407
|
+
<div>
|
|
408
|
+
<TitleField />
|
|
409
|
+
<TagSelectorField />
|
|
410
|
+
<button onClick={submit} disabled={loading}>
|
|
411
|
+
{loading ? 'Creating...' : 'Create Todo'}
|
|
412
|
+
</button>
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
415
|
+
</CreateForm>
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
The `FilterForm` queries the `tags` table asynchronously. The `CreateForm` owns the selected `tagId`. Neither knows about the other.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## The single record pattern
|
|
425
|
+
|
|
426
|
+
`SingleProvider` makes a record available to all its descendants without prop drilling. When multiple components share one record, wrap them all in one provider:
|
|
427
|
+
|
|
428
|
+
```tsx
|
|
429
|
+
import { SingleProvider, useSingle } from 'asasvirtuais/react-interface'
|
|
430
|
+
|
|
431
|
+
// Detail page
|
|
432
|
+
<SingleProvider id={params.id} table="todos" schema={schema}>
|
|
433
|
+
<TodoDetail />
|
|
434
|
+
<UpdateTodoForm />
|
|
435
|
+
<DeleteTodoButton />
|
|
436
|
+
</SingleProvider>
|
|
437
|
+
|
|
438
|
+
// Inside any of those:
|
|
439
|
+
function TodoDetail() {
|
|
440
|
+
const { single } = useSingle(schema, 'todos')
|
|
441
|
+
return <h1>{single.title}</h1>
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
If the record isn't in the reactive index yet, `SingleProvider` fetches it automatically.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Effects
|
|
450
|
+
|
|
451
|
+
There is no middleware or lifecycle configuration. Effects are code written around the action:
|
|
452
|
+
|
|
453
|
+
```tsx
|
|
454
|
+
// Before submit
|
|
455
|
+
<button onClick={() => {
|
|
456
|
+
validateForm(form.fields)
|
|
457
|
+
form.submit()
|
|
458
|
+
}}>
|
|
459
|
+
Save
|
|
460
|
+
</button>
|
|
461
|
+
|
|
462
|
+
// After success
|
|
463
|
+
<CreateForm
|
|
464
|
+
table="todos"
|
|
465
|
+
schema={schema}
|
|
466
|
+
onSuccess={todo => {
|
|
467
|
+
router.push(`/todos/${todo.id}`)
|
|
468
|
+
showNotification('Todo created!')
|
|
469
|
+
}}
|
|
470
|
+
>
|
|
471
|
+
{/* ... */}
|
|
472
|
+
</CreateForm>
|
|
473
|
+
|
|
474
|
+
// Using field values without submitting
|
|
475
|
+
<button onClick={() => saveDraftLocally(form.fields)}>
|
|
476
|
+
Save Draft
|
|
477
|
+
</button>
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Naming pattern examples
|
|
483
|
+
|
|
484
|
+
| Concept | Pattern | Example |
|
|
485
|
+
|---|---|---|
|
|
486
|
+
| Table name | lowercase plural | `'todos'` |
|
|
487
|
+
| Schema types | `Readable`, `Writable` | `type Readable = z.infer<...>` |
|
|
488
|
+
| Field components | `{Field}Field` | `TitleField`, `DoneField` |
|
|
489
|
+
| Provider | `{Model}sProvider` | `TodosProvider` |
|
|
490
|
+
| Hook | `use{Model}s()` | `useTodos()` |
|
|
491
|
+
| Create form | `Create{Model}` | `CreateTodo` |
|
|
492
|
+
| Update form | `Update{Model}` | `UpdateTodo` |
|
|
493
|
+
| Delete action | `Delete{Model}` | `DeleteTodo` |
|
|
494
|
+
| Item component | `{Model}Item` | `TodoItem` |
|
|
495
|
+
| Detail component | `Single{Model}` | `SingleTodo` |
|