asasvirtuais 0.7.17 → 0.8.2

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 CHANGED
@@ -1,421 +1,474 @@
1
- # Asas Virtuais Framework
2
-
3
- Asas Virtuais is a full-stack, schema-driven framework for Next.js designed for rapid, type-safe, and maintainable application development. It provides a powerful abstraction layer for data management, allowing you to define your data shape once with Zod and get automatically generated API endpoints, client-side hooks, and form components.
4
-
5
- ## Core Principles
6
-
7
- - **Schema First**: Define your data models using Zod schemas. This provides a single source of truth for validation, type safety, and data shape across the entire stack.
8
- - **Interface-Driven**: Code against a standardized `TableInterface` on both the client and server. This decouples your application logic from the specific database implementation, allowing for easy swapping of backends.
9
- - **Convention over Configuration**: The framework automatically generates RESTful API endpoints and client-side hooks from your schema, minimizing boilerplate and accelerating development.
10
- - **Extensible by Design**: Every layer is built to be wrapped or replaced. You can easily add custom logic like authentication to API endpoints, create new database adapters, or build bespoke form field components.
11
-
12
- ## Step-by-Step Guide
13
-
14
- This guide will walk you through building a feature using the Asas Virtuais framework.
15
-
16
- ### Step 1: Define Your Schema
17
-
18
- Everything starts with your data schema. Use Zod to define `readable` (data sent to the client) and `writable` (data accepted from the client) shapes for each table.
19
-
20
- `app/database.ts`:
21
- ```ts
22
- import { z } from 'zod'
23
-
24
- export const schema = {
25
- // A generic 'users' table
26
- users: {
27
- readable: z.object({
28
- id: z.string(),
29
- name: z.string(),
30
- email: z.string().email(),
31
- }),
32
- writable: z.object({
33
- name: z.string().nonempty('Name is required'),
34
- email: z.string().email(),
35
- })
36
- },
37
- // A generic 'posts' table
38
- posts: {
39
- readable: z.object({
40
- id: z.string(),
41
- title: z.string(),
42
- content: z.string().optional(),
43
- authorId: z.string(),
44
- }),
45
- writable: z.object({
46
- title: z.string().nonempty('Title is required'),
47
- content: z.string().optional(),
48
- authorId: z.string(),
49
- })
50
- },
51
- }
52
- ```
53
-
54
- ### Step 2: Create a Server-Side Adapter
55
-
56
- Implement the `TableInterface` for your chosen database. The framework is backend-agnostic. This project uses a Firestore adapter.
57
-
58
- `packages/firestore.ts`:
59
- ```ts
60
- import { TableInterface } from './interface'
61
- import { firestore } from './firebase'
62
-
63
- // A generic Firestore adapter implementing the standard interface
64
- export function firestoreInterface<...>(defaultTable?: T): TableInterface<...> {
65
- return {
66
- async find({ table = defaultTable, id }) {
67
- const docSnap = await firestore.collection(table).doc(id).get()
68
- if (docSnap.exists) return { id: docSnap.id, ...docSnap.data() }
69
- throw new Error(`Record not found`)
70
- },
71
- // ... list, create, update, remove implementations
72
- }
73
- }
74
- ```
75
-
76
- Then, instantiate the adapter for each of your tables.
77
-
78
- `app/server.ts`:
79
- ```ts
80
- import { firestoreInterface } from '@/packages/firestore'
81
- import { schema } from './database'
82
-
83
- // Create a server-side data access object
84
- export const server = {
85
- users: firestoreInterface<typeof schema, 'users'>('users'),
86
- posts: firestoreInterface<typeof schema, 'posts'>('posts'),
87
- }
88
- ```
89
-
90
- ### Step 3: Expose API Endpoints
91
-
92
- With the server adapter ready, you can expose it as a RESTful API using `createDynamicRoute`. This automatically creates `GET`, `POST`, `PATCH`, and `DELETE` handlers. You can easily wrap the base implementation to add cross-cutting concerns like authentication.
93
-
94
- `app/api/v1/[...params]/route.ts`:
95
- ```ts
96
- import { authenticate } from '@/packages/auth0' // Your authentication logic
97
- import { createDynamicRoute } from '@/packages/next-interface'
98
- import { firestoreInterface } from '@/packages/firestore'
99
-
100
- const dbInterface = firestoreInterface()
101
-
102
- // Wrap the base interface to enforce business logic and authentication
103
- const apiInterface = {
104
- async create(props) {
105
- const user = await authenticate()
106
- // Example: Inject the authorId automatically
107
- if (props.table === 'posts') {
108
- props.data.authorId = user.id
109
- }
110
- return dbInterface.create(props)
111
- },
112
- async find(props) {
113
- const user = await authenticate()
114
- const result = await dbInterface.find(props)
115
- // Example: Check for ownership on 'posts'
116
- if (result.authorId && result.authorId !== user.id) {
117
- throw new Error('Unauthorized')
118
- }
119
- return result
120
- },
121
- // ... implementations for update, remove, and list with auth checks
122
- } as typeof dbInterface
123
-
124
- // Export the dynamic route handlers
125
- export const GET = createDynamicRoute(apiInterface)
126
- export const POST = createDynamicRoute(apiInterface)
127
- export const DELETE = createDynamicRoute(apiInterface)
128
- export const PATCH = createDynamicRoute(apiInterface)
129
- ```
130
-
131
- ### Step 4: Initialize the Client Interface
132
-
133
- On the client, initialize the React interface. This will provide all the hooks and components you need to interact with your API.
134
-
135
- `app/interface.tsx`:
136
- ```tsx
137
- 'use client'
138
- import { fetchInterface } from '@/packages/fetch-interface'
139
- import { schema } from './database'
140
- import { reactInterface } from '@/packages/react-interface'
141
-
142
- // Create the client-side fetcher
143
- const fetcher = fetchInterface({ schema, baseUrl: '/api/v1' })
144
-
145
- // Export all the React hooks and components for the app
146
- export const {
147
- DatabaseProvider,
148
- useTable,
149
- CreateForm,
150
- UpdateForm,
151
- SingleProvider,
152
- useSingle,
153
- // ... and more
154
- } = reactInterface<typeof schema>(schema, fetcher)
155
- ```
156
-
157
- ### Step 5: Provide Data with `DatabaseProvider`
158
-
159
- In your layouts, use `DatabaseProvider` to fetch initial data and make it available to all components underneath it via React Context.
160
-
161
- `app/posts/[id]/layout.tsx`:
162
- ```tsx
163
- import { DatabaseProvider, SingleProvider } from '@/app/interface'
164
- import { server } from '@/app/server'
165
- import { layout } from '@/packages/next'
166
-
167
- export default layout<{ id: string }>( async ({ id, children }) => {
168
- // Fetch initial data on the server
169
- const post = await server.posts.find({ id })
170
-
171
- // Provide the fetched data to the client-side context
172
- return (
173
- <DatabaseProvider posts={{ [id]: post }} users={{}}>
174
- <SingleProvider id={id} table='posts'>
175
- {children}
176
- </SingleProvider>
177
- </DatabaseProvider>
178
- )
179
- } )
180
- ```
181
-
182
- ### Step 6: Consume Data with Hooks
183
-
184
- Use the provided hooks to access and manipulate data in your components.
185
-
186
- #### `useTable`
187
- To work with a collection of records (list, create, update, delete).
188
-
189
- `app/posts/page.tsx`:
190
- ```tsx
191
- 'use client'
192
- import { useTable } from '@/app/interface'
193
- import Link from 'next/link'
194
- import { IconButton } from '@chakra-ui/react'
195
- import { LuTrash } from 'react-icons/lu'
196
-
197
- export default function PostsPage() {
198
- const { array: posts, remove, list } = useTable('posts')
199
-
200
- useEffect(() => {
201
- list.trigger({}) // Fetch all posts
202
- }, [])
203
-
204
- return (
205
- <VStack>
206
- {posts.map((post) => (
207
- <HStack key={post.id}>
208
- <Link href={`/posts/${post.id}`}>
209
- <Text>{post.title}</Text>
210
- </Link>
211
- <IconButton
212
- onClick={() => remove.trigger({id: post.id})}
213
- disabled={remove.loading}
214
- icon={<LuTrash />}
215
- />
216
- </HStack>
217
- ))}
218
- </VStack>
219
- )
220
- }
221
- ```
222
-
223
- #### `useSingle`
224
- To work with a single record, often used within a `SingleProvider`.
225
-
226
- `app/posts/[id]/page.tsx`:
227
- ```tsx
228
- 'use client'
229
- import { useSingle } from '@/app/interface'
230
-
231
- export default function PostPage() {
232
- // Get the current post from the context provided by SingleProvider
233
- const { single: post } = useSingle('posts')
234
-
235
- return (
236
- <article>
237
- <h1>{post.title}</h1>
238
- <p>{post.content}</p>
239
- </article>
240
- )
241
- }
242
- ```
243
-
244
- ### Step 7: High-Level Abstracted Forms
245
-
246
- The framework provides `CreateForm` and `UpdateForm` components that handle form state, submission, and API calls for you.
247
-
248
- `app/posts/new/page.tsx`:
249
- ```tsx
250
- 'use client'
251
- import { CreateForm } from '@/app/interface'
252
- import { Input, Button, Textarea } from '@chakra-ui/react'
253
-
254
- export function NewPostPage() {
255
- const { user } = useUser() // from @auth0/nextjs-auth0
256
-
257
- return (
258
- <CreateForm
259
- table='posts'
260
- // Set default values for the form
261
- defaults={{ authorId: user.id, title: 'New Post' }}
262
- onSuccess={(post) => router.push(`/posts/${post.id}`)}
263
- >
264
- {/* The child is a render prop with form state and field state */}
265
- {({ loading, fields, setField, submit }) => (
266
- <form onSubmit={submit}>
267
- <Input
268
- value={fields.title}
269
- onChange={(e) => setField('title', e.target.value)}
270
- />
271
- <Textarea
272
- value={fields.content}
273
- onChange={(e) => setField('content', e.target.value)}
274
- />
275
- <Button type='submit' disabled={loading}>
276
- {loading ? 'Saving...' : 'Create Post'}
277
- </Button>
278
- </form>
279
- )}
280
- </CreateForm>
281
- )
282
- }
283
- ```
284
-
285
- #### `FilterForm`
286
- Use `FilterForm` to easily create search and filter functionality. It wraps the `list` method of a table and displays the results.
287
-
288
- `app/posts/SearchPosts.tsx`:
289
- ```tsx
290
- 'use client'
291
- import { FilterForm } from '@/app/interface'
292
- import { Input, Button, VStack, Text, Box } from '@chakra-ui/react'
293
-
294
- function SearchPosts() {
295
- return (
296
- <Box>
297
- <FilterForm
298
- table="posts"
299
- // Set default filter values. The form will initially load with these.
300
- defaults={{ title: '' }}
301
- >
302
- {({ loading, fields, setField, submit, result }) => (
303
- <VStack as="form" onSubmit={submit} align="stretch" spacing={4}>
304
- <Input
305
- placeholder="Search by title..."
306
- value={fields.title}
307
- onChange={(e) => setField('title', e.target.value)}
308
- />
309
- <Button type="submit" disabled={loading} alignSelf="flex-start">
310
- {loading ? 'Searching...' : 'Search'}
311
- </Button>
312
-
313
- {/* Display the results from the list call */}
314
- <VStack align="stretch" mt={4} spacing={2}>
315
- {result && result.length > 0 ? (
316
- result.map(post => (
317
- <Text key={post.id}>- {post.title}</Text>
318
- ))
319
- ) : (
320
- <Text color="gray.500">No results found.</Text>
321
- )}
322
- </VStack>
323
- </VStack>
324
- )}
325
- </FilterForm>
326
- </Box>
327
- );
328
- }
329
- ```
330
-
331
- ## Form and Field Primitives
332
-
333
- The `CreateForm` and `UpdateForm` components are convenient abstractions. For more control, you can use the underlying `FormProvider` and `FieldsProvider` primitives directly.
334
-
335
- ### `packages/form.tsx`
336
-
337
- `FormProvider` is a render-prop component that wraps your form submission logic. It manages the `loading`, `error`, and `result` state of an async callback.
338
-
339
- - **Props**:
340
- - `callback`: An async function that takes the form data and performs the submission.
341
- - `data`: The initial data for the form.
342
- - `onSuccess`: An optional function to call with the result of a successful submission.
343
- - **Render Prop State**:
344
- - `loading`: `boolean` - True while the callback is executing.
345
- - `error`: `Error | null` - The error object if the submission fails.
346
- - `result`: `Result | null` - The data returned from a successful submission.
347
- - `submit`: `function` - The function to trigger the form submission.
348
- - `values`: `Fields` - The current data object for the form.
349
-
350
- ### `packages/fields.tsx`
351
-
352
- `FieldsProvider` manages the state of your form's input fields. It holds the `fields` object and provides `setField` and `setFields` methods to update it.
353
-
354
- - **Props**:
355
- - `defaults`: The initial values for the form fields.
356
- - **Render Prop State**:
357
- - `fields`: `T` - The object containing the current state of all form fields.
358
- - `setField`: `(name, value)` - A function to update a single field's value.
359
- - `setFields`: `(newFields)` - A function to replace the entire fields object.
360
-
361
- ### Using `FormProvider` and `FieldsProvider` Together
362
-
363
- Here is how you can build a form from scratch using the low-level primitives. This pattern is what `CreateForm` uses internally.
364
-
365
- ```tsx
366
- import { FormProvider } from 'asasvirtuais/form'
367
- import { FieldsProvider } from 'asasvirtuais/fields'
368
- import { Input, Button, Spinner, Text, Textarea, Stack } from '@chakra-ui/react'
369
-
370
- function NewPostForm({ onCreate }) {
371
- // The function that will be called on form submission
372
- const createPostCallback = async (fields) => {
373
- // This would typically be an API call, e.g., from useTable()
374
- const newPost = await api.posts.create({ data: fields });
375
- return newPost;
376
- };
377
-
378
- return (
379
- // 1. FormProvider handles the submission process
380
- <FormProvider callback={createPostCallback} data={{}} onSuccess={onCreate}>
381
- {({ loading, error, submit }) => (
382
- // 2. FieldsProvider manages the state of the form's inputs
383
- <FieldsProvider defaults={{ title: '', content: '', authorId: '1' }}>
384
- {({ fields, setField }) => (
385
- <form onSubmit={(e) => {
386
- e.preventDefault();
387
- // Pass the current field state to the submit function
388
- submit(fields);
389
- }}>
390
- <Stack>
391
- <label>Title</label>
392
- <Input
393
- value={fields.title}
394
- onChange={(e) => setField('title', e.target.value)}
395
- />
396
-
397
- <label>Content</label>
398
- <Textarea
399
- value={fields.content}
400
- onChange={(e) => setField('content', e.target.value)}
401
- />
402
-
403
- {error && <Text color="red.500">{error.message}</Text>}
404
-
405
- <Button type="submit" disabled={loading}>
406
- {loading ? <Spinner /> : 'Create Post'}
407
- </Button>
408
- </Stack>
409
- </form>
410
- )}
411
- </FieldsProvider>
412
- )}
413
- </FormProvider>
414
- );
415
- }
416
- ```
417
-
418
- ## Roadmap
419
-
420
- - [ ] Relationships, linked records, TableLookup field
421
- - [ ] Realtime database with feathersjs
1
+ # Asas Virtuais Framework
2
+
3
+ Asas Virtuais is a full-stack, schema-driven framework for Next.js designed for rapid, type-safe, and maintainable application development. It provides a powerful abstraction layer for data management, allowing you to define your data shape once with Zod and get automatically generated API endpoints, client-side hooks, and form components.
4
+
5
+ ## Core Principles
6
+
7
+ - **Schema First**: Define your data models using Zod schemas. This provides a single source of truth for validation, type safety, and data shape across the entire stack.
8
+ - **Interface-Driven**: Code against a standardized `TableInterface` on both the client and server. This decouples your application logic from the specific database implementation, allowing for easy swapping of backends.
9
+ - **Convention over Configuration**: The framework automatically generates RESTful API endpoints and client-side hooks from your schema, minimizing boilerplate and accelerating development.
10
+ - **Extensible by Design**: Every layer is built to be wrapped or replaced. You can easily add custom logic like authentication to API endpoints, create new database adapters, or build bespoke form field components.
11
+
12
+ ## Step-by-Step Guide
13
+
14
+ This guide will walk you through building a feature using the Asas Virtuais framework.
15
+
16
+ ### Step 1: Define Your Schema
17
+
18
+ Everything starts with your data schema. Use Zod to define `readable` (data sent to the client) and `writable` (data accepted from the client) shapes for each table.
19
+
20
+ `app/database.ts`:
21
+ ```ts
22
+ import { z } from 'zod'
23
+
24
+ export const schema = {
25
+ // A generic 'users' table
26
+ users: {
27
+ readable: z.object({
28
+ id: z.string(),
29
+ name: z.string(),
30
+ email: z.string().email(),
31
+ }),
32
+ writable: z.object({
33
+ name: z.string().nonempty('Name is required'),
34
+ email: z.string().email(),
35
+ })
36
+ },
37
+ // A generic 'posts' table
38
+ posts: {
39
+ readable: z.object({
40
+ id: z.string(),
41
+ title: z.string(),
42
+ content: z.string().optional(),
43
+ authorId: z.string(),
44
+ }),
45
+ writable: z.object({
46
+ title: z.string().nonempty('Title is required'),
47
+ content: z.string().optional(),
48
+ authorId: z.string(),
49
+ })
50
+ },
51
+ }
52
+ ```
53
+
54
+ ### Step 2: Create a Server-Side Adapter
55
+
56
+ Implement the `TableInterface` for your chosen database. The framework is backend-agnostic. This project uses a Firestore adapter.
57
+
58
+ `packages/firestore.ts`:
59
+ ```ts
60
+ import { TableInterface } from './interface'
61
+ import { firestore } from './firebase'
62
+
63
+ // A generic Firestore adapter implementing the standard interface
64
+ export function firestoreInterface<...>(defaultTable?: T): TableInterface<...> {
65
+ return {
66
+ async find({ table = defaultTable, id }) {
67
+ const docSnap = await firestore.collection(table).doc(id).get()
68
+ if (docSnap.exists) return { id: docSnap.id, ...docSnap.data() }
69
+ throw new Error(`Record not found`)
70
+ },
71
+ // ... list, create, update, remove implementations
72
+ }
73
+ }
74
+ ```
75
+
76
+ Then, instantiate the adapter for each of your tables.
77
+
78
+ `app/server.ts`:
79
+ ```ts
80
+ import { firestoreInterface } from '@/packages/firestore'
81
+ import { schema } from './database'
82
+
83
+ // Create a server-side data access object
84
+ export const server = {
85
+ users: firestoreInterface<typeof schema, 'users'>('users'),
86
+ posts: firestoreInterface<typeof schema, 'posts'>('posts'),
87
+ }
88
+ ```
89
+
90
+ ### Step 3: Expose API Endpoints
91
+
92
+ With the server adapter ready, you can expose it as a RESTful API using `createDynamicRoute`. This automatically creates `GET`, `POST`, `PATCH`, and `DELETE` handlers. You can easily wrap the base implementation to add cross-cutting concerns like authentication.
93
+
94
+ `app/api/v1/[...params]/route.ts`:
95
+ ```ts
96
+ import { authenticate } from '@/packages/auth0' // Your authentication logic
97
+ import { createDynamicRoute } from '@/packages/next-interface'
98
+ import { firestoreInterface } from '@/packages/firestore'
99
+
100
+ const dbInterface = firestoreInterface()
101
+
102
+ // Wrap the base interface to enforce business logic and authentication
103
+ const apiInterface = {
104
+ async create(props) {
105
+ const user = await authenticate()
106
+ // Example: Inject the authorId automatically
107
+ if (props.table === 'posts') {
108
+ props.data.authorId = user.id
109
+ }
110
+ return dbInterface.create(props)
111
+ },
112
+ async find(props) {
113
+ const user = await authenticate()
114
+ const result = await dbInterface.find(props)
115
+ // Example: Check for ownership on 'posts'
116
+ if (result.authorId && result.authorId !== user.id) {
117
+ throw new Error('Unauthorized')
118
+ }
119
+ return result
120
+ },
121
+ // ... implementations for update, remove, and list with auth checks
122
+ } as typeof dbInterface
123
+
124
+ // Export the dynamic route handlers
125
+ export const GET = createDynamicRoute(apiInterface)
126
+ export const POST = createDynamicRoute(apiInterface)
127
+ export const DELETE = createDynamicRoute(apiInterface)
128
+ export const PATCH = createDynamicRoute(apiInterface)
129
+ ```
130
+
131
+ ### Step 4: Initialize the Client Interface
132
+
133
+ On the client, initialize the React interface. This will provide all the hooks and components you need to interact with your API.
134
+
135
+ `app/interface.tsx`:
136
+ ```tsx
137
+ 'use client'
138
+ import { fetchInterface } from '@/packages/fetch-interface'
139
+ import { schema } from './database'
140
+ import { reactInterface } from '@/packages/react-interface'
141
+
142
+ // Create the client-side fetcher
143
+ const fetcher = fetchInterface({ schema, baseUrl: '/api/v1' })
144
+
145
+ // Export all the React hooks and components for the app
146
+ export const {
147
+ DatabaseProvider,
148
+ useTable,
149
+ CreateForm,
150
+ UpdateForm,
151
+ SingleProvider,
152
+ useSingle,
153
+ // ... and more
154
+ } = reactInterface<typeof schema>(schema, fetcher)
155
+ ```
156
+
157
+ ### Step 5: Provide Data with `DatabaseProvider`
158
+
159
+ In your layouts, use `DatabaseProvider` to fetch initial data and make it available to all components underneath it via React Context.
160
+
161
+ `app/posts/[id]/layout.tsx`:
162
+ ```tsx
163
+ import { DatabaseProvider, SingleProvider } from '@/app/interface'
164
+ import { server } from '@/app/server'
165
+ import { layout } from '@/packages/next'
166
+
167
+ export default layout<{ id: string }>( async ({ id, children }) => {
168
+ // Fetch initial data on the server
169
+ const post = await server.posts.find({ id })
170
+
171
+ // Provide the fetched data to the client-side context
172
+ return (
173
+ <DatabaseProvider posts={{ [id]: post }} users={{}}>
174
+ <SingleProvider id={id} table='posts'>
175
+ {children}
176
+ </SingleProvider>
177
+ </DatabaseProvider>
178
+ )
179
+ } )
180
+ ```
181
+
182
+ ### Step 6: Consume Data with Hooks
183
+
184
+ Use the provided hooks to access and manipulate data in your components.
185
+
186
+ #### `useTable`
187
+ To work with a collection of records (list, create, update, delete).
188
+
189
+ `app/posts/page.tsx`:
190
+ ```tsx
191
+ 'use client'
192
+ import { useTable } from '@/app/interface'
193
+ import Link from 'next/link'
194
+ import { IconButton } from '@chakra-ui/react'
195
+ import { LuTrash } from 'react-icons/lu'
196
+
197
+ export default function PostsPage() {
198
+ const { array: posts, remove, list } = useTable('posts')
199
+
200
+ useEffect(() => {
201
+ list.trigger({}) // Fetch all posts
202
+ }, [])
203
+
204
+ return (
205
+ <VStack>
206
+ {posts.map((post) => (
207
+ <HStack key={post.id}>
208
+ <Link href={`/posts/${post.id}`}>
209
+ <Text>{post.title}</Text>
210
+ </Link>
211
+ <IconButton
212
+ onClick={() => remove.trigger({id: post.id})}
213
+ disabled={remove.loading}
214
+ icon={<LuTrash />}
215
+ />
216
+ </HStack>
217
+ ))}
218
+ </VStack>
219
+ )
220
+ }
221
+ ```
222
+
223
+ #### `useSingle`
224
+ To work with a single record, often used within a `SingleProvider`.
225
+
226
+ `app/posts/[id]/page.tsx`:
227
+ ```tsx
228
+ 'use client'
229
+ import { useSingle } from '@/app/interface'
230
+
231
+ export default function PostPage() {
232
+ // Get the current post from the context provided by SingleProvider
233
+ const { single: post } = useSingle('posts')
234
+
235
+ return (
236
+ <article>
237
+ <h1>{post.title}</h1>
238
+ <p>{post.content}</p>
239
+ </article>
240
+ )
241
+ }
242
+ ```
243
+
244
+ ### Step 7: High-Level Abstracted Forms
245
+
246
+ The framework provides `CreateForm` and `UpdateForm` components that handle form state, submission, and API calls for you.
247
+
248
+ `app/posts/new/page.tsx`:
249
+ ```tsx
250
+ 'use client'
251
+ import { CreateForm } from '@/app/interface'
252
+ import { Input, Button, Textarea } from '@chakra-ui/react'
253
+
254
+ export function NewPostPage() {
255
+ const { user } = useUser() // from @auth0/nextjs-auth0
256
+
257
+ return (
258
+ <CreateForm
259
+ table='posts'
260
+ // Set default values for the form
261
+ defaults={{ authorId: user.id, title: 'New Post' }}
262
+ onSuccess={(post) => router.push(`/posts/${post.id}`)}
263
+ >
264
+ {/* The child is a render prop with form state and field state */}
265
+ {({ loading, fields, setField, submit }) => (
266
+ <form onSubmit={submit}>
267
+ <Input
268
+ value={fields.title}
269
+ onChange={(e) => setField('title', e.target.value)}
270
+ />
271
+ <Textarea
272
+ value={fields.content}
273
+ onChange={(e) => setField('content', e.target.value)}
274
+ />
275
+ <Button type='submit' disabled={loading}>
276
+ {loading ? 'Saving...' : 'Create Post'}
277
+ </Button>
278
+ </form>
279
+ )}
280
+ </CreateForm>
281
+ )
282
+ }
283
+ ```
284
+
285
+ #### `FilterForm`
286
+ Use `FilterForm` to easily create search and filter functionality. It wraps the `list` method of a table and displays the results.
287
+
288
+ `app/posts/SearchPosts.tsx`:
289
+ ```tsx
290
+ 'use client'
291
+ import { FilterForm } from '@/app/interface'
292
+ import { Input, Button, VStack, Text, Box } from '@chakra-ui/react'
293
+
294
+ function SearchPosts() {
295
+ return (
296
+ <Box>
297
+ <FilterForm
298
+ table="posts"
299
+ // Set default filter values. The form will initially load with these.
300
+ defaults={{ title: '' }}
301
+ >
302
+ {({ loading, fields, setField, submit, result }) => (
303
+ <VStack as="form" onSubmit={submit} align="stretch" spacing={4}>
304
+ <Input
305
+ placeholder="Search by title..."
306
+ value={fields.title}
307
+ onChange={(e) => setField('title', e.target.value)}
308
+ />
309
+ <Button type="submit" disabled={loading} alignSelf="flex-start">
310
+ {loading ? 'Searching...' : 'Search'}
311
+ </Button>
312
+
313
+ {/* Display the results from the list call */}
314
+ <VStack align="stretch" mt={4} spacing={2}>
315
+ {result && result.length > 0 ? (
316
+ result.map(post => (
317
+ <Text key={post.id}>- {post.title}</Text>
318
+ ))
319
+ ) : (
320
+ <Text color="gray.500">No results found.</Text>
321
+ )}
322
+ </VStack>
323
+ </VStack>
324
+ )}
325
+ </FilterForm>
326
+ </Box>
327
+ );
328
+ }
329
+ ```
330
+
331
+ ## Form and Field Primitives
332
+
333
+ The `CreateForm` and `UpdateForm` components are convenient abstractions. For more control, you can use the underlying `Form`, `ActionProvider`, and `FieldsProvider` primitives directly.
334
+
335
+ ### `packages/action.tsx`
336
+
337
+ `ActionProvider` is a render-prop component that wraps your form submission logic. It manages the `loading`, `error`, and `result` state of an async action.
338
+
339
+ - **Props**:
340
+ - `action`: An async function that takes the form data and performs the submission.
341
+ - `params`: The data to pass to the action function.
342
+ - `onError`: An optional function to call when the action fails.
343
+ - `preload`: Optional boolean to trigger the action on mount.
344
+ - **Render Prop State**:
345
+ - `loading`: `boolean` - True while the action is executing.
346
+ - `error`: `Error | null` - The error object if the submission fails.
347
+ - `result`: `Result | null` - The data returned from a successful submission.
348
+ - `submit`: `function` - The function to trigger the action.
349
+ - `data`: `Fields` - The current data object for the action.
350
+
351
+ ### `packages/fields.tsx`
352
+
353
+ `FieldsProvider` manages the state of your form's input fields. It holds the `fields` object and provides `setField` and `setFields` methods to update it.
354
+
355
+ - **Props**:
356
+ - `defaults`: The initial values for the form fields.
357
+ - **Render Prop State**:
358
+ - `fields`: `T` - The object containing the current state of all form fields.
359
+ - `setField`: `(name, value)` - A function to update a single field's value.
360
+ - `setFields`: `(newFields)` - A function to replace the entire fields object.
361
+
362
+ ### `packages/form.tsx`
363
+
364
+ `Form` is a convenience component that combines `FieldsProvider` and `ActionProvider` into a single component for simpler usage.
365
+
366
+ - **Props**:
367
+ - `action`: An async function that takes the form data and performs the submission.
368
+ - `defaults`: The initial values for the form fields.
369
+ - `onError`: An optional function to call when the action fails.
370
+ - `preload`: Optional boolean to trigger the action on mount.
371
+ - **Render Prop State**:
372
+ - Combines all state from both `FieldsProvider` and `ActionProvider`
373
+ - `fields`, `setField`, `setFields` (from FieldsProvider)
374
+ - `loading`, `error`, `result`, `submit`, `data` (from ActionProvider)
375
+
376
+ ### Using Form Primitives
377
+
378
+ Here are different ways to build a form using the low-level primitives:
379
+
380
+ **Option 1: Using `Form` (Recommended - Combines Fields + Action)**
381
+
382
+ ```tsx
383
+ import { Form } from 'asasvirtuais/form'
384
+ import { Input, Button, Spinner, Text, Textarea, Stack } from '@chakra-ui/react'
385
+
386
+ function NewPostForm({ onCreate }) {
387
+ const createPostAction = async (fields) => {
388
+ const newPost = await api.posts.create({ data: fields });
389
+ return newPost;
390
+ };
391
+
392
+ return (
393
+ <Form action={createPostAction} defaults={{ title: '', content: '', authorId: '1' }}>
394
+ {({ fields, setField, loading, error, submit }) => (
395
+ <form onSubmit={submit}>
396
+ <Stack>
397
+ <label>Title</label>
398
+ <Input
399
+ value={fields.title}
400
+ onChange={(e) => setField('title', e.target.value)}
401
+ />
402
+
403
+ <label>Content</label>
404
+ <Textarea
405
+ value={fields.content}
406
+ onChange={(e) => setField('content', e.target.value)}
407
+ />
408
+
409
+ {error && <Text color="red.500">{error.message}</Text>}
410
+
411
+ <Button type="submit" disabled={loading}>
412
+ {loading ? <Spinner /> : 'Create Post'}
413
+ </Button>
414
+ </Stack>
415
+ </form>
416
+ )}
417
+ </Form>
418
+ );
419
+ }
420
+ ```
421
+
422
+ **Option 2: Using `ActionProvider` and `FieldsProvider` Separately (Maximum Control)**
423
+
424
+ ```tsx
425
+ import { ActionProvider } from 'asasvirtuais/action'
426
+ import { FieldsProvider } from 'asasvirtuais/fields'
427
+ import { Input, Button, Spinner, Text, Textarea, Stack } from '@chakra-ui/react'
428
+
429
+ function NewPostForm({ onCreate }) {
430
+ const createPostAction = async (fields) => {
431
+ const newPost = await api.posts.create({ data: fields });
432
+ return newPost;
433
+ };
434
+
435
+ return (
436
+ // 1. FieldsProvider manages the state of the form's inputs
437
+ <FieldsProvider defaults={{ title: '', content: '', authorId: '1' }}>
438
+ {({ fields, setField }) => (
439
+ // 2. ActionProvider handles the submission process
440
+ <ActionProvider action={createPostAction} params={fields}>
441
+ {({ loading, error, submit }) => (
442
+ <form onSubmit={submit}>
443
+ <Stack>
444
+ <label>Title</label>
445
+ <Input
446
+ value={fields.title}
447
+ onChange={(e) => setField('title', e.target.value)}
448
+ />
449
+
450
+ <label>Content</label>
451
+ <Textarea
452
+ value={fields.content}
453
+ onChange={(e) => setField('content', e.target.value)}
454
+ />
455
+
456
+ {error && <Text color="red.500">{error.message}</Text>}
457
+
458
+ <Button type="submit" disabled={loading}>
459
+ {loading ? <Spinner /> : 'Create Post'}
460
+ </Button>
461
+ </Stack>
462
+ </form>
463
+ )}
464
+ </ActionProvider>
465
+ )}
466
+ </FieldsProvider>
467
+ );
468
+ }
469
+ ```
470
+
471
+ ## Roadmap
472
+
473
+ - [ ] Relationships, linked records, TableLookup field
474
+ - [ ] Realtime database with feathersjs