cabloy 5.1.60 → 5.1.61
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/.claude/hooks/contract-loop-gate.ts +296 -0
- package/.claude/settings.json +16 -0
- package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +1 -0
- package/.claude/skills/cabloy-contract-loop/SKILL.md +89 -16
- package/.claude/skills/cabloy-contract-loop/references/contract-loop-map.md +102 -14
- package/.claude/skills/cabloy-contract-loop/references/resource-custom-state-pattern.md +4 -0
- package/.claude/skills/cabloy-contract-loop/references/verification-checklist.md +32 -14
- package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +11 -0
- package/.claude/skills/cabloy-frontend-scaffold/references/follow-up-checklist.md +2 -0
- package/.claude/skills/cabloy-module-removal/SKILL.md +144 -0
- package/.claude/skills/cabloy-resource-field-update/SKILL.md +7 -0
- package/.claude/skills/cabloy-zova-source-reading/SKILL.md +221 -0
- package/.claude/skills/cabloy-zova-source-reading/references/analysis-modes.md +91 -0
- package/.claude/skills/cabloy-zova-source-reading/references/core-reading-paths.md +117 -0
- package/CHANGELOG.md +22 -0
- package/CLAUDE.md +10 -0
- package/cabloy-docs/.vitepress/config.mjs +50 -4
- package/cabloy-docs/ai/cli-to-skill-map.md +7 -0
- package/cabloy-docs/ai/docs-skills-rules-mapping.md +14 -0
- package/cabloy-docs/ai/future-skill-roadmap.md +10 -7
- package/cabloy-docs/ai/introduction.md +1 -0
- package/cabloy-docs/ai/playbook-backend-module.md +6 -0
- package/cabloy-docs/ai/playbook-module-removal.md +164 -0
- package/cabloy-docs/ai/skills.md +11 -0
- package/cabloy-docs/backend/dto-guide.md +6 -0
- package/cabloy-docs/backend/entity-guide.md +18 -0
- package/cabloy-docs/backend/introduction.md +2 -0
- package/cabloy-docs/backend/serialization-guide.md +10 -0
- package/cabloy-docs/backend/status-guide.md +271 -0
- package/cabloy-docs/frontend/api-guide.md +2 -0
- package/cabloy-docs/frontend/bean-scene-authoring.md +2 -0
- package/cabloy-docs/frontend/cli.md +12 -0
- package/cabloy-docs/frontend/command-scene-authoring.md +495 -0
- package/cabloy-docs/frontend/design-principles.md +6 -0
- package/cabloy-docs/frontend/fetch-interceptor-guide.md +440 -0
- package/cabloy-docs/frontend/form-guide.md +795 -0
- package/cabloy-docs/frontend/foundation.md +29 -0
- package/cabloy-docs/frontend/introduction.md +12 -1
- package/cabloy-docs/frontend/ioc-and-beans.md +6 -0
- package/cabloy-docs/frontend/mock-guide.md +1 -0
- package/cabloy-docs/frontend/model-architecture.md +252 -39
- package/cabloy-docs/frontend/model-resource-best-practices.md +379 -0
- package/cabloy-docs/frontend/model-resource-cookbook.md +505 -0
- package/cabloy-docs/frontend/model-resource-owner-pattern.md +382 -0
- package/cabloy-docs/frontend/model-resource-usage-guide.md +318 -0
- package/cabloy-docs/frontend/model-state-guide.md +366 -13
- package/cabloy-docs/frontend/openapi-sdk-guide.md +5 -2
- package/cabloy-docs/frontend/page-guide.md +6 -0
- package/cabloy-docs/frontend/quickstart.md +4 -0
- package/cabloy-docs/frontend/reading-zova-for-vue-developers.md +266 -0
- package/cabloy-docs/frontend/server-data.md +2 -0
- package/cabloy-docs/frontend/zova-form-source-reading-map.md +295 -0
- package/cabloy-docs/frontend/zova-form-under-the-hood.md +556 -0
- package/cabloy-docs/frontend/zova-reactivity-under-the-hood.md +320 -0
- package/cabloy-docs/frontend/zova-source-reading-map.md +327 -0
- package/cabloy-docs/frontend/zova-vs-vue3-comparison.md +308 -0
- package/cabloy-docs/fullstack/contract-loop-playbook.md +350 -0
- package/cabloy-docs/fullstack/frontend-metadata-to-backend.md +44 -1
- package/cabloy-docs/fullstack/introduction.md +12 -1
- package/cabloy-docs/fullstack/openapi-to-sdk.md +19 -9
- package/cabloy-docs/fullstack/tutorial-3-frontend-metadata-sharing.md +2 -2
- package/cabloy-docs/fullstack/tutorial-4-custom-level-renderers.md +30 -5
- package/cabloy-docs/fullstack/tutorial-5-backend-contract-sharing.md +9 -7
- package/cabloy-docs/fullstack/tutorial-6-one-contract-four-uses.md +2 -0
- package/cabloy-docs/fullstack/tutorials-overview.md +16 -3
- package/cabloy-docs/reference/bean-scene-boilerplates.md +2 -0
- package/package.json +2 -1
- package/scripts/init.ts +2 -18
- package/scripts/initTestData.ts +25 -0
- package/scripts/upgrade.ts +17 -2
- package/vona/pnpm-lock.yaml +94 -4
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/cli/templates/openapi/config/boilerplate/module/openapi.config.ts +6 -1
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-cli/cli-set-front/src/lib/bean/cli.openapi.generate.ts +34 -4
- package/zova/pnpm-lock.yaml +20 -20
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
# Form Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to build forms in Zova with a practical, tutorial-style path.
|
|
4
|
+
|
|
5
|
+
Zova Form is not only a thin Vue wrapper around a form library. It is a Zova-native form layer built around:
|
|
6
|
+
|
|
7
|
+
- controller-oriented frontend architecture
|
|
8
|
+
- schema-driven field rendering
|
|
9
|
+
- Zod-friendly validation
|
|
10
|
+
- behavior-based field layout and customization
|
|
11
|
+
- provider-based render selection
|
|
12
|
+
|
|
13
|
+
Use this page together with:
|
|
14
|
+
|
|
15
|
+
- [Component Guide](/frontend/component-guide)
|
|
16
|
+
- [Behavior Guide](/frontend/behavior-guide)
|
|
17
|
+
- [API Schema Guide](/frontend/api-schema-guide)
|
|
18
|
+
- [Zod Guide](/frontend/zod-guide)
|
|
19
|
+
- [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern)
|
|
20
|
+
- [Zova Form Under the Hood](/frontend/zova-form-under-the-hood)
|
|
21
|
+
- [Zova Form Source Reading Map](/frontend/zova-form-source-reading-map)
|
|
22
|
+
|
|
23
|
+
> [!TIP]
|
|
24
|
+
> **Zova Form docs path**
|
|
25
|
+
> 1. **[Form Guide](/frontend/form-guide)** — learn the public authoring surface
|
|
26
|
+
> 2. **[Zova Form Under the Hood](/frontend/zova-form-under-the-hood)** — learn how the runtime pieces cooperate
|
|
27
|
+
> 3. **[Zova Form Source Reading Map](/frontend/zova-form-source-reading-map)** — learn which files to read next
|
|
28
|
+
>
|
|
29
|
+
> **You are here:** step 1.
|
|
30
|
+
> **Next recommended page:** [Zova Form Under the Hood](/frontend/zova-form-under-the-hood).
|
|
31
|
+
|
|
32
|
+
If your next question is how these public APIs cooperate internally at runtime, continue with [Zova Form Under the Hood](/frontend/zova-form-under-the-hood).
|
|
33
|
+
|
|
34
|
+
## What you should learn first
|
|
35
|
+
|
|
36
|
+
If you only remember one idea, remember this one:
|
|
37
|
+
|
|
38
|
+
> In Zova, a form is centered on a form controller and field controllers, while schema, provider config, and behaviors decide how much of the UI should be automatic and how much should be customized.
|
|
39
|
+
|
|
40
|
+
That leads to four common authoring surfaces:
|
|
41
|
+
|
|
42
|
+
- `ZForm` — the root form component
|
|
43
|
+
- `ZFormFieldPreset` — the quickest way to render a standard field
|
|
44
|
+
- `ZFormField` — the low-level custom field entry
|
|
45
|
+
- `ZFormFieldBlank` — a free layout row for buttons and other non-field content
|
|
46
|
+
|
|
47
|
+
## One running example through this guide: Student
|
|
48
|
+
|
|
49
|
+
To keep the guide concrete, the examples below all use the same teaching resource:
|
|
50
|
+
|
|
51
|
+
- resource: `demo-student:student`
|
|
52
|
+
- representative fields: `name`, `level`, and `mobile`
|
|
53
|
+
- common scenes: Student create, Student edit, and Student view
|
|
54
|
+
|
|
55
|
+
That Student thread is a good specimen because it grows through the same path most real business forms follow:
|
|
56
|
+
|
|
57
|
+
1. generated CRUD structure
|
|
58
|
+
2. schema-driven form rendering
|
|
59
|
+
3. metadata-driven field rendering
|
|
60
|
+
4. custom field rendering only where the business UI needs it
|
|
61
|
+
|
|
62
|
+
So as you read the code samples below, treat them as different stages of the same Student form rather than unrelated fragments.
|
|
63
|
+
|
|
64
|
+
## Step 1: Choose the right form style
|
|
65
|
+
|
|
66
|
+
Before writing code, choose which of these three styles matches your Student page.
|
|
67
|
+
|
|
68
|
+
### Style A: schema-driven form
|
|
69
|
+
|
|
70
|
+
Use this when:
|
|
71
|
+
|
|
72
|
+
- the backend Student contract already describes the form well
|
|
73
|
+
- you want labels, defaults, readonly behavior, and render metadata to come from schema-driven truth
|
|
74
|
+
- you want the shortest path to a working Student create/edit/view page
|
|
75
|
+
|
|
76
|
+
### Style B: manual form
|
|
77
|
+
|
|
78
|
+
Use this when:
|
|
79
|
+
|
|
80
|
+
- one Student field needs very custom UI behavior
|
|
81
|
+
- the page has unusual interaction or layout rules
|
|
82
|
+
- you want to wire one field directly through a field slot
|
|
83
|
+
|
|
84
|
+
### Style C: mixed form
|
|
85
|
+
|
|
86
|
+
Use this when:
|
|
87
|
+
|
|
88
|
+
- most Student fields can use preset or schema-driven rendering
|
|
89
|
+
- one or two Student fields need a custom renderer
|
|
90
|
+
- you need action rows, helper rows, or page-specific layout blocks
|
|
91
|
+
|
|
92
|
+
A practical rule is:
|
|
93
|
+
|
|
94
|
+
- start with **schema-driven** if the Student contract is already strong
|
|
95
|
+
- use **mixed** for most business forms
|
|
96
|
+
- drop to **manual** only where the UI really needs it
|
|
97
|
+
|
|
98
|
+
## Step 2: Start with the smallest useful Student form
|
|
99
|
+
|
|
100
|
+
The smallest useful Zova form is usually a mixed Student form with `ZForm`, a couple of fields, and one submit row.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
<ZForm data={this.studentFormData} onSubmitData={data => this.submitStudent(data)}>
|
|
104
|
+
<ZFormFieldPreset
|
|
105
|
+
name="name"
|
|
106
|
+
layout={{ label: 'Student Name:' }}
|
|
107
|
+
></ZFormFieldPreset>
|
|
108
|
+
|
|
109
|
+
<ZFormFieldPreset
|
|
110
|
+
name="level"
|
|
111
|
+
render="basic-select:formFieldSelect"
|
|
112
|
+
options={{ items: this.levelItems }}
|
|
113
|
+
layout={{ label: 'Level:' }}
|
|
114
|
+
></ZFormFieldPreset>
|
|
115
|
+
|
|
116
|
+
<ZFormFieldBlank
|
|
117
|
+
slotDefault={$$form => {
|
|
118
|
+
return (
|
|
119
|
+
<button disabled={$$form.formState.isSubmitting} type="submit" class="btn btn-primary">
|
|
120
|
+
Save Student
|
|
121
|
+
</button>
|
|
122
|
+
);
|
|
123
|
+
}}
|
|
124
|
+
></ZFormFieldBlank>
|
|
125
|
+
</ZForm>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
What this gives you immediately:
|
|
129
|
+
|
|
130
|
+
- form state ownership through `ZForm`
|
|
131
|
+
- standard Student fields through `ZFormFieldPreset`
|
|
132
|
+
- submit state through `$$form.formState.isSubmitting`
|
|
133
|
+
- a Zova-native Student form flow without hand-building local state wiring
|
|
134
|
+
|
|
135
|
+
## Step 3: Understand the field components
|
|
136
|
+
|
|
137
|
+
The three field entries are intentionally different.
|
|
138
|
+
|
|
139
|
+
### `ZFormFieldPreset`
|
|
140
|
+
|
|
141
|
+
Use `ZFormFieldPreset` when you want the standard form pipeline with a reusable preset renderer.
|
|
142
|
+
|
|
143
|
+
Typical Student cases:
|
|
144
|
+
|
|
145
|
+
- `name` as a standard input
|
|
146
|
+
- `level` as a select field
|
|
147
|
+
- `mobile` as a standard input or masked input
|
|
148
|
+
- any provider-defined standard Student field component
|
|
149
|
+
|
|
150
|
+
Representative Student field:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
<ZFormFieldPreset
|
|
154
|
+
name="level"
|
|
155
|
+
render="basic-select:formFieldSelect"
|
|
156
|
+
options={{
|
|
157
|
+
items: this.levelItems,
|
|
158
|
+
placeholder: 'Choose a level',
|
|
159
|
+
}}
|
|
160
|
+
layout={{ label: 'Level:' }}
|
|
161
|
+
></ZFormFieldPreset>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### `ZFormField`
|
|
165
|
+
|
|
166
|
+
Use `ZFormField` when you want direct render control.
|
|
167
|
+
|
|
168
|
+
Representative Student field:
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
<ZFormField
|
|
172
|
+
name="mobile"
|
|
173
|
+
slotDefault={({ propsBucket, props }, $$formField) => {
|
|
174
|
+
return (
|
|
175
|
+
<input
|
|
176
|
+
{...props}
|
|
177
|
+
class="input"
|
|
178
|
+
value={propsBucket.value}
|
|
179
|
+
placeholder="Student mobile"
|
|
180
|
+
onInput={(e: Event) => {
|
|
181
|
+
$$formField.setValue((e.target as HTMLInputElement).value);
|
|
182
|
+
}}
|
|
183
|
+
onBlur={() => {
|
|
184
|
+
$$formField.handleBlur();
|
|
185
|
+
}}
|
|
186
|
+
></input>
|
|
187
|
+
);
|
|
188
|
+
}}
|
|
189
|
+
></ZFormField>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Use this when you need to:
|
|
193
|
+
|
|
194
|
+
- render one Student field manually
|
|
195
|
+
- integrate a custom Student UI component
|
|
196
|
+
- intercept input and blur behavior yourself
|
|
197
|
+
- mix Zova form state with a custom render contract
|
|
198
|
+
|
|
199
|
+
### `ZFormFieldBlank`
|
|
200
|
+
|
|
201
|
+
Use `ZFormFieldBlank` when the row is **not** a real data field.
|
|
202
|
+
|
|
203
|
+
Typical Student cases:
|
|
204
|
+
|
|
205
|
+
- save/cancel buttons
|
|
206
|
+
- helper text under the Student fields
|
|
207
|
+
- page-specific action rows
|
|
208
|
+
- custom layout blocks between field groups
|
|
209
|
+
|
|
210
|
+
Representative shape:
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
<ZFormFieldBlank
|
|
214
|
+
slotDefault={$$form => {
|
|
215
|
+
return (
|
|
216
|
+
<div class="flex gap-2">
|
|
217
|
+
<button disabled={$$form.formState.isSubmitting} type="submit" class="btn btn-primary">
|
|
218
|
+
Save Student
|
|
219
|
+
</button>
|
|
220
|
+
<button type="button" class="btn btn-default" onClick={() => this.cancelEdit()}>
|
|
221
|
+
Cancel
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}}
|
|
226
|
+
></ZFormFieldBlank>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
A simple memory aid is:
|
|
230
|
+
|
|
231
|
+
- `ZFormFieldPreset` = convention-first Student field
|
|
232
|
+
- `ZFormField` = custom Student field render
|
|
233
|
+
- `ZFormFieldBlank` = free row inside the Student form
|
|
234
|
+
|
|
235
|
+
## Step 4: Add submit behavior
|
|
236
|
+
|
|
237
|
+
The most common submit hook is `onSubmitData`.
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
<ZForm
|
|
241
|
+
data={this.studentFormData}
|
|
242
|
+
onSubmitData={data => {
|
|
243
|
+
return this.submitStudent(data);
|
|
244
|
+
}}
|
|
245
|
+
></ZForm>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`onSubmitData` receives submission data plus form API context.
|
|
249
|
+
|
|
250
|
+
In practice, that means you can:
|
|
251
|
+
|
|
252
|
+
- read `data.value` as the submitted Student form data
|
|
253
|
+
- use `data.formApi` when you need lower-level form access
|
|
254
|
+
- pass typed submit metadata if your workflow uses it
|
|
255
|
+
|
|
256
|
+
### Show loading state
|
|
257
|
+
|
|
258
|
+
A common pattern is to read loading state from `formState`.
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
slotFooter={$$form => {
|
|
262
|
+
return (
|
|
263
|
+
<div>
|
|
264
|
+
{$$form.formState.isSubmitting && (
|
|
265
|
+
<span class="loading loading-spinner text-primary"></span>
|
|
266
|
+
)}
|
|
267
|
+
<button class="btn btn-primary">Save Student</button>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Show errors
|
|
274
|
+
|
|
275
|
+
Use `onShowError` when you want a centralized user-facing error handler.
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
<ZForm
|
|
279
|
+
onShowError={({ error }) => {
|
|
280
|
+
window.alert(error.message)
|
|
281
|
+
}}
|
|
282
|
+
></ZForm>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
That is useful when the default Student field-level error state is not enough and the page also wants a broader error message.
|
|
286
|
+
|
|
287
|
+
## Step 5: Move to schema-driven Student rendering when possible
|
|
288
|
+
|
|
289
|
+
When `ZForm` receives a `schema` and you do **not** provide a default body slot for the form, Zova can render fields automatically from the schema properties.
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
<ZForm
|
|
293
|
+
data={this.studentFormData}
|
|
294
|
+
schema={this.studentFormSchema}
|
|
295
|
+
formMeta={this.studentFormMeta}
|
|
296
|
+
onSubmitData={data => this.submitStudent(data)}
|
|
297
|
+
></ZForm>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
This is usually the best next step after a manual Student prototype because it lets the backend Student contract drive more of the UI.
|
|
301
|
+
|
|
302
|
+
Schema-driven rendering is a strong fit when:
|
|
303
|
+
|
|
304
|
+
- the Student schema already carries field metadata
|
|
305
|
+
- frontend and backend should stay close to the same Student contract truth
|
|
306
|
+
- you want to reduce duplicated Student field configuration
|
|
307
|
+
|
|
308
|
+
Read together with [API Schema Guide](/frontend/api-schema-guide).
|
|
309
|
+
|
|
310
|
+
## Step 6: Add validation
|
|
311
|
+
|
|
312
|
+
Zova Form is built on top of TanStack Form and integrates naturally with Zod.
|
|
313
|
+
|
|
314
|
+
### Use schema-driven validation as the default
|
|
315
|
+
|
|
316
|
+
When the Student form has a Zod schema, field validation can often be derived from the field schema directly.
|
|
317
|
+
|
|
318
|
+
That is the best default when:
|
|
319
|
+
|
|
320
|
+
- the backend Student contract already defines the validation truth
|
|
321
|
+
- the frontend should stay close to the same validation semantics
|
|
322
|
+
|
|
323
|
+
### Add field-level validation when needed
|
|
324
|
+
|
|
325
|
+
You can also provide validators directly on a field.
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
<ZFormFieldPreset
|
|
329
|
+
name="name"
|
|
330
|
+
validators={{ onDynamic: z.string().min(3) }}
|
|
331
|
+
></ZFormFieldPreset>
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Or, for a custom Student field:
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
<ZFormField
|
|
338
|
+
name="mobile"
|
|
339
|
+
validators={{ onBlur: z.string().min(6) }}
|
|
340
|
+
></ZFormField>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
The common validator hooks are:
|
|
344
|
+
|
|
345
|
+
- `onDynamic`
|
|
346
|
+
- `onBlur`
|
|
347
|
+
- `onChange`
|
|
348
|
+
|
|
349
|
+
A practical rule is:
|
|
350
|
+
|
|
351
|
+
- use schema-derived validation for the default Student contract-driven case
|
|
352
|
+
- add field-level validators when one Student field needs an explicit frontend rule
|
|
353
|
+
|
|
354
|
+
### Handle invalid submit
|
|
355
|
+
|
|
356
|
+
Useful form-level hooks include:
|
|
357
|
+
|
|
358
|
+
- `onSubmitInvalid`
|
|
359
|
+
- `onShowError`
|
|
360
|
+
|
|
361
|
+
Zova Form also normalizes server validation failures back into the form pipeline. In practice, backend validation responses such as Student field-oriented 422 errors can flow back into form and field error state instead of being treated as unrelated generic exceptions.
|
|
362
|
+
|
|
363
|
+
## Step 7: Choose the form scene with `formMeta`
|
|
364
|
+
|
|
365
|
+
`formMeta` is the main scene-level mode input for the form.
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
this.studentFormMeta = {
|
|
369
|
+
formMode: 'edit',
|
|
370
|
+
editMode: 'update',
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
A practical way to read it is:
|
|
375
|
+
|
|
376
|
+
- `formMode: 'view'` -> Student view form
|
|
377
|
+
- `formMode: 'edit'` + `editMode: 'create'` -> Student create form
|
|
378
|
+
- `formMode: 'edit'` + `editMode: 'update'` -> Student edit form
|
|
379
|
+
|
|
380
|
+
This matters because the form pipeline can derive behavior such as readonly field state from the form scene.
|
|
381
|
+
|
|
382
|
+
If the Student form is in view mode, field rendering can become readonly automatically without every field re-implementing that rule manually.
|
|
383
|
+
|
|
384
|
+
## Step 8: Customize layout with `layout`, `options`, and `formProvider`
|
|
385
|
+
|
|
386
|
+
A Zova form is not driven by one source only.
|
|
387
|
+
|
|
388
|
+
In practice, Student field rendering can be influenced by:
|
|
389
|
+
|
|
390
|
+
- schema metadata
|
|
391
|
+
- field props
|
|
392
|
+
- form-level layout defaults
|
|
393
|
+
- provider defaults
|
|
394
|
+
- field-specific options
|
|
395
|
+
|
|
396
|
+
### Use `layout` for label and wrapper concerns
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
<ZFormFieldPreset
|
|
400
|
+
name="level"
|
|
401
|
+
layout={{ label: 'Student Level:' }}
|
|
402
|
+
></ZFormFieldPreset>
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Use `layout` for concerns such as:
|
|
406
|
+
|
|
407
|
+
- label text
|
|
408
|
+
- icon prefix/suffix
|
|
409
|
+
- field wrapper presentation
|
|
410
|
+
- row-level layout intent
|
|
411
|
+
|
|
412
|
+
### Use `options` for renderer-specific input props
|
|
413
|
+
|
|
414
|
+
```tsx
|
|
415
|
+
<ZFormFieldPreset
|
|
416
|
+
name="level"
|
|
417
|
+
render="basic-select:formFieldSelect"
|
|
418
|
+
options={{
|
|
419
|
+
items: this.levelItems,
|
|
420
|
+
placeholder: 'Choose a level',
|
|
421
|
+
}}
|
|
422
|
+
></ZFormFieldPreset>
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Use `options` when the concrete Student field renderer needs its own input options.
|
|
426
|
+
|
|
427
|
+
### Use `formProvider` for page-level behavior and render decisions
|
|
428
|
+
|
|
429
|
+
`formProvider` is the main provider-level customization surface.
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
432
|
+
<ZForm
|
|
433
|
+
formProvider={{ behaviors: { FormFieldLayout: 'demo-student:formFieldLayoutStudent' } }}
|
|
434
|
+
>
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
This is useful for:
|
|
438
|
+
|
|
439
|
+
- provider-defined Student field components
|
|
440
|
+
- provider-defined layout behavior
|
|
441
|
+
- page-level Student form customization without rewriting every field
|
|
442
|
+
|
|
443
|
+
Read together with [Behavior Guide](/frontend/behavior-guide).
|
|
444
|
+
|
|
445
|
+
## Step 9: Decide whether the root should be a real `<form>`
|
|
446
|
+
|
|
447
|
+
By default, `ZForm` uses `formTag="form"`.
|
|
448
|
+
|
|
449
|
+
That means:
|
|
450
|
+
|
|
451
|
+
- the root element is a native `<form>`
|
|
452
|
+
- normal submit behavior is available
|
|
453
|
+
- Zova wires the submit event into the form controller
|
|
454
|
+
|
|
455
|
+
If you need the Student form pipeline but do **not** want a native `<form>` root, switch the wrapper tag.
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
<ZForm
|
|
459
|
+
formTag="div"
|
|
460
|
+
data={this.studentFormData}
|
|
461
|
+
schema={this.studentFormSchema}
|
|
462
|
+
onSubmitData={data => this.submitStudent(data)}
|
|
463
|
+
></ZForm>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Use this when the Student form is embedded inside a larger block system or another page-level orchestration layer.
|
|
467
|
+
|
|
468
|
+
## Step 10: Use `controllerRef` when the page needs the form instance
|
|
469
|
+
|
|
470
|
+
Like other Zova components, the preferred instance reference is the **controller instance**, not a generic DOM ref.
|
|
471
|
+
|
|
472
|
+
```tsx
|
|
473
|
+
<ZForm
|
|
474
|
+
controllerRef={ref => {
|
|
475
|
+
this.studentFormRef = ref;
|
|
476
|
+
}}
|
|
477
|
+
></ZForm>
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
This is useful when you need to:
|
|
481
|
+
|
|
482
|
+
- submit the Student form programmatically
|
|
483
|
+
- inspect `formState`
|
|
484
|
+
- integrate the Student form into a larger page-entry or action system
|
|
485
|
+
|
|
486
|
+
Example:
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
<button
|
|
490
|
+
onClick={async () => {
|
|
491
|
+
await this.studentFormRef.submit();
|
|
492
|
+
}}
|
|
493
|
+
>
|
|
494
|
+
Save Student
|
|
495
|
+
</button>
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
## Step 11: Know when to use the advanced base classes
|
|
499
|
+
|
|
500
|
+
The module also exposes controller base classes for cases where the controller itself should participate more directly in form ownership.
|
|
501
|
+
|
|
502
|
+
The main exported bases are:
|
|
503
|
+
|
|
504
|
+
- `BeanControllerFormBase`
|
|
505
|
+
- `BeanControllerPageFormBase`
|
|
506
|
+
|
|
507
|
+
Use these when:
|
|
508
|
+
|
|
509
|
+
- a reusable Student-related component controller wants typed form access
|
|
510
|
+
- a Student page controller wants form-specific helpers while staying in the normal Zova page-controller model
|
|
511
|
+
- you want to keep Student page logic and form lifecycle close together
|
|
512
|
+
|
|
513
|
+
This is an advanced surface. For most page and component authoring, start with `ZForm` and the field components first.
|
|
514
|
+
|
|
515
|
+
## Step 12: Study one complete CRUD form pattern with the Student resource
|
|
516
|
+
|
|
517
|
+
When you move from a demo form to a real business page, the best source specimen in this repository is the **Student** teaching thread used across the fullstack tutorials.
|
|
518
|
+
|
|
519
|
+
That Student thread is useful because it grows from:
|
|
520
|
+
|
|
521
|
+
- generated CRUD structure
|
|
522
|
+
- schema-driven create/edit/view forms
|
|
523
|
+
- metadata-driven field rendering
|
|
524
|
+
- custom field rendering when the business UI needs it
|
|
525
|
+
|
|
526
|
+
Use the Student resource when you want a concrete mental model for how `ZForm` fits into Cabloy’s larger contract loop.
|
|
527
|
+
|
|
528
|
+
### The practical Student CRUD story
|
|
529
|
+
|
|
530
|
+
A standard Student entry form usually follows this business path:
|
|
531
|
+
|
|
532
|
+
1. the backend Student resource defines the contract truth
|
|
533
|
+
2. the frontend resource model loads schema and provider metadata for Student
|
|
534
|
+
3. the page decides whether the Student form is `create`, `edit`, or `view`
|
|
535
|
+
4. `ZForm` renders from Student form schema and Student form data
|
|
536
|
+
5. submit delegates back to the Student mutation owned by the model
|
|
537
|
+
|
|
538
|
+
So the page is **not** the place that should invent schema lookup rules or mutation policy.
|
|
539
|
+
|
|
540
|
+
### Controller-side shape for a Student entry page
|
|
541
|
+
|
|
542
|
+
A representative Student-oriented controller shape looks like this:
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
protected async __init__() {
|
|
546
|
+
this.$$modelResource = await this.bean._getBeanSelector(
|
|
547
|
+
'rest-resource.model.resource',
|
|
548
|
+
true,
|
|
549
|
+
'demo-student:student',
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
this.studentFormMeta = this.$computed(() => {
|
|
553
|
+
const formScene = this.entryId ? 'edit' : 'create';
|
|
554
|
+
return { ...formMetaFromFormScene(formScene), formScene };
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
this.studentFormProvider = this.$computed(() => {
|
|
558
|
+
return this.$$modelResource.formProvider;
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
this.studentFormSchema = this.$computed(() => {
|
|
562
|
+
return this.$$modelResource.getFormSchema(this.studentFormMeta);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
this.studentFormData = this.$computed(() => {
|
|
566
|
+
return this.$$modelResource.getFormData(this.studentFormMeta, this.entryId);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async submitStudent(data: TypeFormOnSubmitData<StudentFormData>) {
|
|
571
|
+
const mutationSubmit = this.$$modelResource.getFormMutationSubmit(this.studentFormMeta, this.entryId);
|
|
572
|
+
await mutationSubmit?.mutateAsync(data.value);
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Read that example as four ownership boundaries:
|
|
577
|
+
|
|
578
|
+
- `demo-student:student` identifies the business resource
|
|
579
|
+
- `ModelResource` owns resource-level Student form schema/data/provider lookup
|
|
580
|
+
- `studentFormMeta` decides whether the Student page is create/edit/view
|
|
581
|
+
- `submitStudent` delegates to the Student mutation policy instead of inventing a page-local submit rule
|
|
582
|
+
|
|
583
|
+
### Render-side shape for a Student entry page
|
|
584
|
+
|
|
585
|
+
Once the Student model side is prepared, the render side can stay thin:
|
|
586
|
+
|
|
587
|
+
```tsx
|
|
588
|
+
<ZForm
|
|
589
|
+
formTag="div"
|
|
590
|
+
data={this.studentFormData}
|
|
591
|
+
schema={this.studentFormSchema}
|
|
592
|
+
formMeta={this.studentFormMeta}
|
|
593
|
+
formProvider={this.studentFormProvider}
|
|
594
|
+
onSubmitData={data => this.submitStudent(data)}
|
|
595
|
+
onShowError={({ error }) => {
|
|
596
|
+
window.alert(error.message)
|
|
597
|
+
}}
|
|
598
|
+
></ZForm>
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
This is the important design lesson:
|
|
602
|
+
|
|
603
|
+
- the Student page renders the form
|
|
604
|
+
- the Student model owns form resource semantics
|
|
605
|
+
- the backend Student contract still remains the source of truth for schema-driven behavior
|
|
606
|
+
|
|
607
|
+
### How the Student example evolves over time
|
|
608
|
+
|
|
609
|
+
The Student tutorials show a useful growth path:
|
|
610
|
+
|
|
611
|
+
1. start with generated CRUD
|
|
612
|
+
2. let schema drive the Student create/edit/view surfaces
|
|
613
|
+
3. reuse built-in field renderers for fields like `level`
|
|
614
|
+
4. only add custom Student field renderers when the business UI really needs them
|
|
615
|
+
|
|
616
|
+
That is the same growth strategy you should usually follow in your own modules.
|
|
617
|
+
|
|
618
|
+
### Why this is the recommended CRUD architecture
|
|
619
|
+
|
|
620
|
+
This Student pattern is recommended because:
|
|
621
|
+
|
|
622
|
+
- the page does **not** own schema lookup rules
|
|
623
|
+
- the page does **not** invent its own submit mutation policy
|
|
624
|
+
- `ModelResource` owns resource-level form metadata and mutation selection
|
|
625
|
+
- `ZForm` stays focused on rendering, validation, and submission flow
|
|
626
|
+
- Student-specific UI can deepen later without breaking the contract-first structure
|
|
627
|
+
|
|
628
|
+
This is also why Cabloy can provide complete CRUD-style form pages with a strong schema-driven surface instead of requiring every page to hand-build form wiring from scratch.
|
|
629
|
+
|
|
630
|
+
Read together with:
|
|
631
|
+
|
|
632
|
+
- [Tutorial 2: Create Your First CRUD](/fullstack/tutorial-2-first-crud)
|
|
633
|
+
- [Tutorial 3: Frontend Metadata Sharing](/fullstack/tutorial-3-frontend-metadata-sharing)
|
|
634
|
+
- [Tutorial 4: Custom Form/Table Renderers for Level](/fullstack/tutorial-4-custom-level-renderers)
|
|
635
|
+
- [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern)
|
|
636
|
+
|
|
637
|
+
## Quick comparison: schema-driven vs manual vs mixed
|
|
638
|
+
|
|
639
|
+
Use this table when you are unsure which style to choose.
|
|
640
|
+
|
|
641
|
+
| Style | Best fit | Strengths | Trade-offs |
|
|
642
|
+
| --- | --- | --- | --- |
|
|
643
|
+
| Schema-driven | resource CRUD pages, contract-first forms, metadata-rich pages | least duplication, easiest to keep aligned with backend truth, fastest way to scale many forms | less suitable when one page has unusual UI structure |
|
|
644
|
+
| Manual | highly custom UI, one-off interaction-heavy forms | maximum render control | easiest way to drift away from contract truth and duplicate field wiring |
|
|
645
|
+
| Mixed | most business pages | keeps standard fields cheap while leaving room for custom rows or custom renderers | requires discipline about which layer should own each customization |
|
|
646
|
+
|
|
647
|
+
A practical recommendation is:
|
|
648
|
+
|
|
649
|
+
- use **schema-driven** for standard Student-style resource create/edit/view pages
|
|
650
|
+
- use **mixed** for most real business forms
|
|
651
|
+
- use **manual** only when the page truly needs renderer-level control
|
|
652
|
+
|
|
653
|
+
## Relationship map 1: business ownership from resource to form
|
|
654
|
+
|
|
655
|
+
Use this diagram when the question is:
|
|
656
|
+
|
|
657
|
+
- who owns the business scene?
|
|
658
|
+
- where do schema and data come from?
|
|
659
|
+
- how does a resource page reach `ZForm`?
|
|
660
|
+
|
|
661
|
+
```text
|
|
662
|
+
Backend resource contract (example: Student)
|
|
663
|
+
├─ field definitions
|
|
664
|
+
├─ validation truth
|
|
665
|
+
└─ frontend render metadata
|
|
666
|
+
│
|
|
667
|
+
▼
|
|
668
|
+
ModelResource / page controller
|
|
669
|
+
├─ provides formMeta
|
|
670
|
+
├─ provides formSchema
|
|
671
|
+
├─ provides formData
|
|
672
|
+
└─ provides formProvider
|
|
673
|
+
│
|
|
674
|
+
▼
|
|
675
|
+
ZForm
|
|
676
|
+
├─ owns form controller
|
|
677
|
+
├─ owns form state / submit / reset
|
|
678
|
+
├─ resolves schema properties
|
|
679
|
+
└─ creates field render context
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Read this top-down:
|
|
683
|
+
|
|
684
|
+
- the **backend resource** still defines the business contract
|
|
685
|
+
- the **resource model/page** translates that contract into page-ready form inputs
|
|
686
|
+
- `ZForm` owns the frontend form runtime
|
|
687
|
+
|
|
688
|
+
This is the diagram to use when you are debugging a CRUD form page at the business or architecture level.
|
|
689
|
+
|
|
690
|
+
## Relationship map 2: field rendering pipeline inside `ZForm`
|
|
691
|
+
|
|
692
|
+
Use this diagram when the question is:
|
|
693
|
+
|
|
694
|
+
- why did this field render that way?
|
|
695
|
+
- where did the final component choice come from?
|
|
696
|
+
- should I change schema metadata, field props, provider config, or behaviors?
|
|
697
|
+
|
|
698
|
+
```text
|
|
699
|
+
ZForm
|
|
700
|
+
└─ field render context
|
|
701
|
+
│
|
|
702
|
+
├───────────────┬───────────────────┐
|
|
703
|
+
▼ ▼ ▼
|
|
704
|
+
ZFormFieldPreset ZFormField ZFormFieldBlank
|
|
705
|
+
│ │ │
|
|
706
|
+
│ │ └─ free row for buttons / helper content
|
|
707
|
+
│ │
|
|
708
|
+
│ └─ manual slot render
|
|
709
|
+
│ + setValue / handleBlur
|
|
710
|
+
│
|
|
711
|
+
└─ preset-driven standard field
|
|
712
|
+
|
|
713
|
+
Field rendering pipeline
|
|
714
|
+
schema/rest + field props + layout + provider config
|
|
715
|
+
│
|
|
716
|
+
▼
|
|
717
|
+
formProvider.components
|
|
718
|
+
│
|
|
719
|
+
▼
|
|
720
|
+
behaviors
|
|
721
|
+
│
|
|
722
|
+
▼
|
|
723
|
+
final rendered field
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Read this top-down too:
|
|
727
|
+
|
|
728
|
+
- field components choose how much rendering is automatic vs custom
|
|
729
|
+
- schema metadata and field props shape the initial field contract
|
|
730
|
+
- `formProvider.components` decides the concrete renderer surface
|
|
731
|
+
- behaviors wrap or refine the final UI without changing the higher-level form contract
|
|
732
|
+
|
|
733
|
+
This is the diagram to use when you are debugging one field instead of the whole resource page.
|
|
734
|
+
|
|
735
|
+
## Recommended learning path
|
|
736
|
+
|
|
737
|
+
If you are new to Zova Form, use this order:
|
|
738
|
+
|
|
739
|
+
1. build one small mixed Student form with `ZForm`, `ZFormFieldPreset`, and `ZFormFieldBlank`
|
|
740
|
+
2. add `onSubmitData` and loading/error handling
|
|
741
|
+
3. move one Student page to schema-driven rendering
|
|
742
|
+
4. add field-level or schema-level validation
|
|
743
|
+
5. introduce `formMeta` for Student `view`, `create`, and `edit`
|
|
744
|
+
6. introduce `formProvider` only when page-level layout or render behavior needs customization
|
|
745
|
+
7. study one Student resource CRUD form and trace how `ModelResource` feeds `ZForm`
|
|
746
|
+
8. continue with [Zova Form Under the Hood](/frontend/zova-form-under-the-hood) when you want the runtime explanation behind the public authoring surface
|
|
747
|
+
9. continue with [Zova Form Source Reading Map](/frontend/zova-form-source-reading-map) when you need framework-level source details and targeted file-order guidance
|
|
748
|
+
|
|
749
|
+
That path usually gives the fastest route from first usage to a maintainable business form.
|
|
750
|
+
|
|
751
|
+
## Common mistakes to avoid
|
|
752
|
+
|
|
753
|
+
### Mistake 1: starting with manual fields for everything
|
|
754
|
+
|
|
755
|
+
If the backend Student schema already carries the form truth, start schema-driven or mixed instead of manually wiring every field.
|
|
756
|
+
|
|
757
|
+
### Mistake 2: treating `ZFormFieldBlank` like a normal field
|
|
758
|
+
|
|
759
|
+
Use it for action rows and free layout content, not for real data ownership.
|
|
760
|
+
|
|
761
|
+
### Mistake 3: putting layout policy into every field manually
|
|
762
|
+
|
|
763
|
+
If the concern is page-wide or provider-wide, prefer `formProvider` and behavior-based customization.
|
|
764
|
+
|
|
765
|
+
### Mistake 4: forgetting `formMeta`
|
|
766
|
+
|
|
767
|
+
If the Student page has clear `view`, `create`, or `edit` semantics, encode that through `formMeta` so readonly and edit behavior stay consistent.
|
|
768
|
+
|
|
769
|
+
### Mistake 5: reaching for advanced base classes too early
|
|
770
|
+
|
|
771
|
+
Most Student form pages should start with `ZForm` and field components. Use `BeanControllerFormBase` or `BeanControllerPageFormBase` only when the controller-level ownership is genuinely the clearer architecture.
|
|
772
|
+
|
|
773
|
+
## Edition note
|
|
774
|
+
|
|
775
|
+
This guide describes the shared Zova Form architecture.
|
|
776
|
+
|
|
777
|
+
That architecture applies across Cabloy Basic and Cabloy Start. However, visual styling, preset field components, and provider-level UI details can still differ once the task becomes UI-library-specific.
|
|
778
|
+
|
|
779
|
+
For Cabloy Basic, public examples in this repository currently align with DaisyUI + Tailwind CSS.
|
|
780
|
+
|
|
781
|
+
## Verification checklist
|
|
782
|
+
|
|
783
|
+
When documenting or changing Zova Form usage, verify in this order:
|
|
784
|
+
|
|
785
|
+
1. confirm whether the form should be schema-driven, manual, or mixed
|
|
786
|
+
2. confirm whether `formMeta` should be `view`, `create`, or `edit`
|
|
787
|
+
3. confirm whether validation truth should come from schema, field-level validators, or both
|
|
788
|
+
4. confirm whether layout customization belongs in field props or provider behaviors
|
|
789
|
+
5. build the docs site:
|
|
790
|
+
|
|
791
|
+
```bash
|
|
792
|
+
npm run docs:build
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
6. verify the page is reachable from the frontend sidebar and the frontend introduction page
|