@praxisui/manual-form 0.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/LICENSE +6 -0
- package/README.md +303 -0
- package/fesm2022/praxisui-manual-form.mjs +1592 -0
- package/fesm2022/praxisui-manual-form.mjs.map +1 -0
- package/index.d.ts +298 -0
- package/package.json +39 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Manual Form Toolkit
|
|
2
|
+
|
|
3
|
+
## Instalação
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install praxis-manual-form
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Peers necessários (instale no app host):
|
|
10
|
+
- `@angular/core` `^20.1.0`, `@angular/common` `^20.1.0`
|
|
11
|
+
- `@praxisui/core` (ConfigStorage, GenericCrudService, ícones)
|
|
12
|
+
- `@praxisui/dynamic-fields` (componentes de campo `pdx-*`)
|
|
13
|
+
- `@praxisui/settings-panel` (integração com painel de configurações)
|
|
14
|
+
- `@praxisui/metadata-editor` (edição visual de campos, opcional)
|
|
15
|
+
|
|
16
|
+
## Problema
|
|
17
|
+
|
|
18
|
+
Montar formulários Praxis manualmente (usando `<pdx-*>`) exigia montar `FormConfig`, `FormGroup`, persistência e metadados manualmente. Isso gerava atrito para hosts e dificultava a evolução.
|
|
19
|
+
|
|
20
|
+
## Solução
|
|
21
|
+
|
|
22
|
+
Criamos a biblioteca `@praxisui/manual-form` com:
|
|
23
|
+
|
|
24
|
+
- **ManualFormComponent**: container que detecta campos, infere `FieldMetadata`, cria o `FormGroup`, mantém hot update e persiste via `ManualFormInstance`. Aceita inputs `formId`, `formTitle`, `actions`, `enableAutoSave` etc.
|
|
25
|
+
- **ManualFormInstanceFactory**: runtime que cuida da persistência (`createManualFormSeed`, `saveDraft`, `resetToSeed`).
|
|
26
|
+
- **ManualFormHeader/Actions**: componentes auxiliares para título/descritivo e ações (`submit/cancel/reset/custom`).
|
|
27
|
+
- **ManualFieldMetadataBridgeService**: integra com o `@praxisui/metadata-editor`, aplica patches e garante persistência via `ManualFormInstance`.
|
|
28
|
+
|
|
29
|
+
O exemplo `/cargos/manual-form` demonstra uso declarativo:
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<praxis-manual-form formId="cargo">
|
|
33
|
+
<pdx-text-input formControlName="nome" label="Nome" required></pdx-text-input>
|
|
34
|
+
<pdx-material-currency formControlName="salario" label="Salário"></pdx-material-currency>
|
|
35
|
+
</praxis-manual-form>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Editor de metadados integrado
|
|
39
|
+
|
|
40
|
+
Use o `ManualFieldMetadataBridgeService` para abrir o editor visual a partir de qualquer campo detectado pelo container. O serviço carrega o módulo sob demanda, aplica o patch retornado e chama `saveDraft()` para persistir as alterações no runtime.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
@Component({
|
|
44
|
+
/* ... */
|
|
45
|
+
})
|
|
46
|
+
export class CargoManualFormComponent {
|
|
47
|
+
@ViewChild(ManualFormComponent) manualForm?: ManualFormComponent;
|
|
48
|
+
|
|
49
|
+
constructor(private readonly metadataBridge: ManualFieldMetadataBridgeService) {}
|
|
50
|
+
|
|
51
|
+
openFieldEditor(fieldName: string): void {
|
|
52
|
+
const instance = this.manualForm?.instance;
|
|
53
|
+
if (!instance) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
void this.metadataBridge.openEditor(instance, fieldName);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Motivações
|
|
62
|
+
|
|
63
|
+
- DX coerente para formulários manuais (sem JSON).
|
|
64
|
+
- Compatibilidade com o ecossistema (metadados, persistência, metadata editor).
|
|
65
|
+
- Facilita adoção por hosts sem precisar do builder completo.
|
|
66
|
+
|
|
67
|
+
## Estado atual
|
|
68
|
+
|
|
69
|
+
- Container autodetecta campos (`selector`/`constructor → FieldControlType`), infere `label`, `validators.required`.
|
|
70
|
+
- Persistência automática (`ManualFormInstance`/`ConfigStorage`).
|
|
71
|
+
- Header/Ações padrões com `FormActionsLayout` (normalizado por `DEFAULT_ACTIONS`).
|
|
72
|
+
- Seeds usam `ensureIds`.
|
|
73
|
+
- Autosave opcional com debounce configurável (`enableAutoSave` + `autoSaveDebounceMs`).
|
|
74
|
+
|
|
75
|
+
## Próximos passos
|
|
76
|
+
|
|
77
|
+
1. Enriquecer inferência (opções, limites numéricos, etc.).
|
|
78
|
+
2. Permitir layout personalizado via `@Input() sections` ou `layoutConfig`.
|
|
79
|
+
3. Expandir testes unitários (detecção de campos, persistência, normalização de ações, nested paths).
|
|
80
|
+
4. Documentar API pública com exemplos (README + docs).
|
|
81
|
+
5. Adicionar feedbacks visuais pós-edição (notificações, toasts).
|
|
82
|
+
|
|
83
|
+
## Notas para evolução
|
|
84
|
+
|
|
85
|
+
- Continue a trabalhar em `projects/praxis-manual-form` e atualize demos (ex. `/src/app/features/cargo-manual-form`).
|
|
86
|
+
- Ao rodar `ng-packagr`, confirme que libs referenciadas (`@praxisui/core`, `@praxisui/dynamic-fields`) estão construídas ou apontam para `dist/`.
|
|
87
|
+
- Respeite contratos de `FieldMetadata` e use helpers (`ensureIds`, `deepClone`).
|
|
88
|
+
- Atenção a inputs obrigatórios (alguns componentes exigem `metadata`, `readonlyMode`, etc.).
|
|
89
|
+
- Prefira inline styles para evitar warnings de arquivo ausente.
|
|
90
|
+
|
|
91
|
+
Essa base permite que hosts manualmente criem formulários com componentes Praxis, mantendo compatibilidade com o restante do ecossistema (persistência, metadados e editor).
|
|
92
|
+
|
|
93
|
+
## Padrão corporativo recomendado
|
|
94
|
+
|
|
95
|
+
- Host tipado: o host deve criar um `FormGroup` tipado e passá‑lo via `[formGroup]`. O container adota esse grupo, aplica metadados/validators e evita interceptações internas. Isso habilita o Angular Language Service a validar `formControlName` e melhora a DX.
|
|
96
|
+
- Modo dinâmico: se nenhum `[formGroup]` for informado, o container cria e gerencia um `FormGroup` interno (útil para PoCs), sem garantia de validação estática de nomes.
|
|
97
|
+
|
|
98
|
+
## Configurações avançadas
|
|
99
|
+
|
|
100
|
+
- `@Input() usePathNames: boolean` (padrão: `false`)
|
|
101
|
+
- Quando habilitado, usa `FormControlName.path` (segmentos unidos por `.`) como `FieldMetadata.name`. Requer suporte a grupos aninhados — já implementado em `DynamicFormService`.
|
|
102
|
+
- Mapeamentos de inferência via DI
|
|
103
|
+
- `MANUAL_FORM_SELECTOR_TO_CONTROL_TYPE` e `MANUAL_FORM_CONSTRUCTOR_TO_CONTROL_TYPE` permitem sobrescrever o mapa padrão de seletores/constructors → `FieldControlType`.
|
|
104
|
+
- Autosave
|
|
105
|
+
- `@Input() enableAutoSave: boolean` (padrão: `true`) e `@Input() autoSaveDebounceMs: number` (padrão: `800`).
|
|
106
|
+
|
|
107
|
+
## SSR (ConfigStorage sem localStorage)
|
|
108
|
+
|
|
109
|
+
Em SSR, `localStorage` não existe. Como `praxis-manual-form` apenas depende do token `CONFIG_STORAGE`, você pode prover uma implementação segura no servidor (ex.: in‑memory) e manter `LocalStorageConfigService` no browser.
|
|
110
|
+
|
|
111
|
+
Exemplo (Angular com `app.config.server.ts`):
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// app.config.server.ts
|
|
115
|
+
import { ApplicationConfig, PLATFORM_ID, inject, isPlatformServer } from '@angular/core';
|
|
116
|
+
import { CONFIG_STORAGE, LocalStorageConfigService, type ConfigStorage } from '@praxisui/core';
|
|
117
|
+
|
|
118
|
+
class MemoryConfigStorage implements ConfigStorage {
|
|
119
|
+
private readonly map = new Map<string, any>();
|
|
120
|
+
loadConfig<T>(key: string): T | null { return (this.map.get(key) as T) ?? null; }
|
|
121
|
+
saveConfig<T>(key: string, config: T): void { this.map.set(key, config); }
|
|
122
|
+
clearConfig(key: string): void { this.map.delete(key); }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const appConfig: ApplicationConfig = {
|
|
126
|
+
providers: [
|
|
127
|
+
{
|
|
128
|
+
provide: CONFIG_STORAGE,
|
|
129
|
+
useFactory: () => isPlatformServer(inject(PLATFORM_ID))
|
|
130
|
+
? new MemoryConfigStorage()
|
|
131
|
+
: inject(LocalStorageConfigService),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Alternativas no servidor: usar cookies, cache distribuído (ex.: Redis) ou persistir por sessão do usuário. Basta implementar a interface `ConfigStorage`.
|
|
138
|
+
|
|
139
|
+
### Exemplo (nested form)
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<form [formGroup]="form">
|
|
143
|
+
<praxis-manual-form formId="nested" [usePathNames]="true">
|
|
144
|
+
<div formGroupName="endereco">
|
|
145
|
+
<pdx-text-input formControlName="logradouro" label="Logradouro"></pdx-text-input>
|
|
146
|
+
<pdx-text-input formControlName="numero" label="Número"></pdx-text-input>
|
|
147
|
+
</div>
|
|
148
|
+
</praxis-manual-form>
|
|
149
|
+
</form>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Com `usePathNames=true`, os metadados serão gerados com nomes `endereco.logradouro` e `endereco.numero`, mantendo o `FormGroup` aninhado do host.
|
|
153
|
+
|
|
154
|
+
## Quando usar (e quando não)
|
|
155
|
+
|
|
156
|
+
Use esta biblioteca quando:
|
|
157
|
+
- Você quer controlar o layout via HTML, reaproveitando os componentes Praxis.
|
|
158
|
+
- Prefere começar simples e evoluir metadados gradualmente (inferência + editor visual opcional).
|
|
159
|
+
- Precisa integrar com persistência de rascunhos e editar propriedades de campos em runtime.
|
|
160
|
+
|
|
161
|
+
Prefira o formulário totalmente dinâmico (JSON com `@praxisui/dynamic-form`) quando:
|
|
162
|
+
- O layout deve ser 100% dirigido por metadados/JSON (sem HTML manual).
|
|
163
|
+
- É necessário trocar telas apenas alterando seeds/backends.
|
|
164
|
+
- Precisa de recursos avançados do motor dinâmico (regras, seções condicionais, layouts declarativos complexos).
|
|
165
|
+
|
|
166
|
+
## Guia rápido (host tipado)
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// component.ts
|
|
170
|
+
@Component({ /* … */ })
|
|
171
|
+
export class MinhaPagina {
|
|
172
|
+
private readonly fb = inject(FormBuilder);
|
|
173
|
+
readonly form = this.fb.group({
|
|
174
|
+
nome: this.fb.control('', { nonNullable: true, validators: [Validators.required] }),
|
|
175
|
+
endereco: this.fb.group({ logradouro: this.fb.control('', { nonNullable: true }) }),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```html
|
|
181
|
+
<praxis-manual-form
|
|
182
|
+
[formGroup]="form"
|
|
183
|
+
formId="meu-form"
|
|
184
|
+
[usePathNames]="true"
|
|
185
|
+
formTitle="Cadastro"
|
|
186
|
+
(submitted)="onSubmit($event)"
|
|
187
|
+
>
|
|
188
|
+
<pdx-text-input formControlName="nome" label="Nome"></pdx-text-input>
|
|
189
|
+
<div formGroupName="endereco">
|
|
190
|
+
<pdx-text-input formControlName="logradouro" label="Logradouro"></pdx-text-input>
|
|
191
|
+
</div>
|
|
192
|
+
</praxis-manual-form>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## API do ManualFormComponent (resumo)
|
|
196
|
+
|
|
197
|
+
- Inputs (sinais):
|
|
198
|
+
- `formId: string` (obrigatório)
|
|
199
|
+
- `formTitle?: string`, `formDescription?: string`
|
|
200
|
+
- `actions?: FormActionsLayout | null`
|
|
201
|
+
- `showHeader = true`, `showActions = true`
|
|
202
|
+
- `enableAutoSave = true`, `autoSaveDebounceMs = 800`
|
|
203
|
+
- `editModeEnabled = false` (habilita ações de customização — ex.: abrir editor por duplo clique)
|
|
204
|
+
- `persistenceOptions?: { namespace?, tenantId?, profileId?, locale? }`
|
|
205
|
+
- `usePathNames = false` (usa `FormControlName.path` como nome do campo)
|
|
206
|
+
- Outputs (sinais):
|
|
207
|
+
- `submitted({ value, instance })`, `saved(instance)`, `reset(instance)`
|
|
208
|
+
- `metadataChange(FormConfig)`
|
|
209
|
+
|
|
210
|
+
- Métodos públicos:
|
|
211
|
+
- `tryOpenFieldEditor(fieldName: string)`: tenta abrir o editor do campo, respeitando `editModeEnabled` (no‑op quando `false`).
|
|
212
|
+
|
|
213
|
+
## Header e Actions
|
|
214
|
+
|
|
215
|
+
- `<praxis-manual-form-header>`: recebe `instance`, `title`, `description`, `saveLabel`, `resetLabel` e emite `save/reset`.
|
|
216
|
+
- Quando `editModeEnabled` está ativo no container, o header exibe um botão “Editar formulário” que emite `editForm`; o container trata esse evento chamando `openFormEditor()`.
|
|
217
|
+
- `<praxis-manual-form-actions>`: recebe `actions: FormActionsLayout`, emite `actionClick` e aceita `trackByFn` opcional para listas grandes.
|
|
218
|
+
|
|
219
|
+
## Editor de Metadados (visual)
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
@ViewChild(ManualFormComponent) manual?: ManualFormComponent;
|
|
223
|
+
constructor(private readonly bridge: ManualFieldMetadataBridgeService) {}
|
|
224
|
+
openEditor(fieldName: string) {
|
|
225
|
+
const inst = this.manual?.instance; if (!inst) return;
|
|
226
|
+
this.bridge.openEditor(inst, fieldName).catch(console.error);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Integração com Settings Panel: o `ManualFormComponent` utiliza `SettingsPanelService` para abrir o editor do formulário (lista de campos e flags) quando `editModeEnabled` está ativo. É possível abrir programaticamente via `manualForm.openFormEditor()`.
|
|
231
|
+
|
|
232
|
+
### Duplo clique com modo de edição
|
|
233
|
+
|
|
234
|
+
Para espelhar o comportamento das demais libs (permitir edição apenas quando o modo de edição está ativo), use o input `editModeEnabled` no container e a diretiva `pdxManualEdit` nos campos:
|
|
235
|
+
|
|
236
|
+
```html
|
|
237
|
+
<praxis-manual-form formId="cargo" [editModeEnabled]="custom.enabled()">
|
|
238
|
+
<pdx-text-input formControlName="nome" label="Nome" pdxManualEdit="nome"></pdx-text-input>
|
|
239
|
+
<pdx-material-currency formControlName="salario" label="Salário" pdxManualEdit="salario"></pdx-material-currency>
|
|
240
|
+
</praxis-manual-form>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
A diretiva chama `manualForm.tryOpenFieldEditor(fieldName)` e respeita `editModeEnabled`, de modo que o duplo clique só abre o editor quando a edição está ligada.
|
|
244
|
+
|
|
245
|
+
### Editor do Formulário (lista de campos)
|
|
246
|
+
|
|
247
|
+
O `ManualFormComponent` expõe `openFormEditor()` para abrir um editor simples do formulário que lista todos os campos e permite alternar: visibilidade (mostrar/ocultar), obrigatório, somente leitura e desabilitado — facilitando encontrar campos ocultos e reexibi‑los, além de ajustes rápidos. O editor respeita `editModeEnabled` e usa o `SettingsPanelService`.
|
|
248
|
+
|
|
249
|
+
Exemplo de uso no host:
|
|
250
|
+
|
|
251
|
+
```html
|
|
252
|
+
<button *ngIf="custom.enabled()" type="button" (click)="manualForm?.openFormEditor()">Editar formulário</button>
|
|
253
|
+
<praxis-manual-form #manualForm [formGroup]="form" formId="'cargo'" [editModeEnabled]="custom.enabled()">
|
|
254
|
+
<!-- campos -->
|
|
255
|
+
</praxis-manual-form>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Extensibilidade por DI
|
|
259
|
+
|
|
260
|
+
- Customize a inferência de `FieldControlType` via tokens:
|
|
261
|
+
- `MANUAL_FORM_SELECTOR_TO_CONTROL_TYPE`
|
|
262
|
+
- `MANUAL_FORM_CONSTRUCTOR_TO_CONTROL_TYPE`
|
|
263
|
+
|
|
264
|
+
## ManualFieldDirective
|
|
265
|
+
|
|
266
|
+
```html
|
|
267
|
+
<ng-container *praxisManualField="'nome'; praxisManualFieldInstance: manual.instance as ctx">
|
|
268
|
+
Label atual: {{ ctx?.label }}
|
|
269
|
+
</ng-container>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## ManualFieldEditorOnDblclickDirective (`pdxManualEdit`)
|
|
273
|
+
|
|
274
|
+
- Usa o duplo clique para abrir o editor do campo, respeitando `editModeEnabled` do `ManualFormComponent`.
|
|
275
|
+
- Apenas funciona quando aplicada a elementos dentro de `<praxis-manual-form>`.
|
|
276
|
+
|
|
277
|
+
```html
|
|
278
|
+
<praxis-manual-form formId="cargo" [editModeEnabled]="custom.enabled()">
|
|
279
|
+
<pdx-text-input formControlName="nome" label="Nome" pdxManualEdit="nome"></pdx-text-input>
|
|
280
|
+
</praxis-manual-form>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Boas práticas
|
|
284
|
+
|
|
285
|
+
- Preferir host tipado e `usePathNames=true` para nested forms.
|
|
286
|
+
- Habilitar autosave com debounce adequado.
|
|
287
|
+
- Evitar lógica pesada em templates; delegar inferências ao container.
|
|
288
|
+
- No SSR, sempre prover `CONFIG_STORAGE` compatível (sem localStorage).
|
|
289
|
+
|
|
290
|
+
## Licença
|
|
291
|
+
|
|
292
|
+
Apache-2.0 — consulte `LICENSE` neste pacote ou no repositório raiz.
|
|
293
|
+
|
|
294
|
+
## Limitações
|
|
295
|
+
|
|
296
|
+
- Inferência automática é propositalmente minimalista (label, required, tipo básico). Propriedades avançadas exigem metadados explícitos/edição visual.
|
|
297
|
+
- Layout é responsabilidade do host; não há reordenação automática por metadados.
|
|
298
|
+
- Modo dinâmico sem `[formGroup]` não habilita validação estática de `formControlName` no IDE.
|
|
299
|
+
|
|
300
|
+
## Comparativo — Manual vs Dinâmico (JSON)
|
|
301
|
+
|
|
302
|
+
- Manual: controle máximo do HTML; evolui por metadados incrementais; ótimo para telas estáveis com design específico.
|
|
303
|
+
- Dinâmico: telas dirigidas por JSON com alto poder de reconfiguração; ideal para múltiplas telas e variações rápidas sem alterar HTML.
|