@sapphire-caitlyn/schema-kit-ng 0.0.4 → 0.0.6
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 +2 -0
- package/docs/inputs/date/input-date/README.md +37 -0
- package/docs/inputs/date/input-date-span/README.md +36 -0
- package/docs/inputs/date/input-date-time/README.md +32 -0
- package/docs/inputs/date/input-date-time-span/README.md +32 -0
- package/docs/inputs/date/input-time/README.md +28 -0
- package/docs/inputs/date/input-time-span/README.md +29 -0
- package/docs/inputs/input-base/README.md +42 -0
- package/docs/inputs/input-bool/README.md +30 -0
- package/docs/inputs/input-color/README.md +24 -0
- package/docs/inputs/input-color/color-picker/README.md +25 -0
- package/docs/inputs/input-cpf-cnpj/README.md +34 -0
- package/docs/inputs/input-file/README.md +33 -0
- package/docs/inputs/input-icon/README.md +28 -0
- package/docs/inputs/input-json/README.md +36 -0
- package/docs/inputs/input-mask/README.md +39 -0
- package/docs/inputs/input-number/README.md +44 -0
- package/docs/inputs/input-phone/README.md +25 -0
- package/docs/inputs/input-select/README.md +42 -0
- package/docs/inputs/input-string/README.md +32 -0
- package/docs/inputs/input-text-area/README.md +27 -0
- package/docs/schema-table/README.md +444 -0
- package/docs/schema-view/README.md +439 -0
- package/docs/ui/blurry-text-reveal/README.md +49 -0
- package/docs/ui/breadcrumbs/README.md +62 -0
- package/docs/ui/file-preview/README.md +60 -0
- package/docs/ui/infinte-scroll/README.md +10 -0
- package/docs/ui/loading/README.md +62 -0
- package/docs/ui/save-button/README.md +68 -0
- package/docs/ui/schema-dialog/README.md +154 -0
- package/docs/ui/suspended-slider/README.md +95 -0
- package/docs/ui/tree-drag-drop/README.md +202 -0
- package/fesm2022/sapphire-caitlyn-schema-kit-ng-helm.mjs.map +1 -1
- package/fesm2022/sapphire-caitlyn-schema-kit-ng.mjs +106 -103
- package/fesm2022/sapphire-caitlyn-schema-kit-ng.mjs.map +1 -1
- package/package.json +1 -1
- package/types/sapphire-caitlyn-schema-kit-ng.d.ts +93 -17
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# schema-view
|
|
2
|
+
|
|
3
|
+
Tela de criar/editar gerada a partir de um schema: um input por campo, carregamento do registro pela rota, validação e
|
|
4
|
+
salvamento. Na criação envia todos os valores (`POST`) e depois abre a edição do registro criado; na edição envia só os
|
|
5
|
+
campos alterados (`PATCH`).
|
|
6
|
+
|
|
7
|
+
Feita para trabalhar junto com o [schema-table](../schema-table/README.md) (a listagem).
|
|
8
|
+
|
|
9
|
+
## Uso
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { SchemaViewComponent, SchemaViewContentDirective } from '@schema-kit';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### 1. O service
|
|
16
|
+
|
|
17
|
+
O jeito mais comum é passar um `SchemaService`: ele dá os campos, carrega o registro e salva. Os exemplos deste README
|
|
18
|
+
usam este service:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { inject, Injectable } from '@angular/core';
|
|
22
|
+
import { min, required } from '@angular/forms/signals';
|
|
23
|
+
import { FieldType, IView, SchemaService } from '@schema-kit';
|
|
24
|
+
|
|
25
|
+
export interface IProduto extends IView {
|
|
26
|
+
nome: string;
|
|
27
|
+
preco: number;
|
|
28
|
+
ativo: boolean;
|
|
29
|
+
uuidCategoria: string | null;
|
|
30
|
+
foto: string | File | null;
|
|
31
|
+
descricao: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Injectable({ providedIn: 'root' })
|
|
35
|
+
export class ProdutoService extends SchemaService<IProduto> {
|
|
36
|
+
constructor() {
|
|
37
|
+
// `this` não existe antes do super(): injete as dependências em variáveis locais
|
|
38
|
+
const categoriaService = inject(CategoriaService);
|
|
39
|
+
|
|
40
|
+
super({
|
|
41
|
+
endpoint: '/produtos',
|
|
42
|
+
title: 'Produtos',
|
|
43
|
+
bindLabel: 'nome',
|
|
44
|
+
bindValue: 'uuid',
|
|
45
|
+
schema: {
|
|
46
|
+
uuid: { type: FieldType.UUID, label: 'UUID', hidden: true },
|
|
47
|
+
nome: { type: FieldType.String, label: 'Nome', validators: required },
|
|
48
|
+
preco: {
|
|
49
|
+
type: FieldType.Currency,
|
|
50
|
+
label: 'Preço',
|
|
51
|
+
validators: [required, (path) => min(path, 0, { message: 'O preço não pode ser negativo' })],
|
|
52
|
+
},
|
|
53
|
+
ativo: { type: FieldType.Bool, label: 'Ativo', defaultValue: true },
|
|
54
|
+
uuidCategoria: {
|
|
55
|
+
type: FieldType.Select,
|
|
56
|
+
label: 'Categoria',
|
|
57
|
+
tooltipInfo: 'Define em qual seção da loja o produto aparece',
|
|
58
|
+
options: { service: () => categoriaService, clearable: true },
|
|
59
|
+
},
|
|
60
|
+
foto: { type: FieldType.File, label: 'Foto', options: { clearable: true } },
|
|
61
|
+
descricao: { type: FieldType.TextArea, label: 'Descrição' },
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. As rotas
|
|
69
|
+
|
|
70
|
+
O componente lê o uuid do parâmetro da rota. Sem uuid, é criação; com uuid, é edição. Use a mesma estrutura que o
|
|
71
|
+
schema-table navega (`./view` e `./view/:uuid`):
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const routes: Routes = [
|
|
75
|
+
{ path: '', component: ProdutoListComponent },
|
|
76
|
+
{ path: 'view', component: ProdutoViewComponent }, // criar
|
|
77
|
+
{ path: 'view/:uuid', component: ProdutoViewComponent }, // editar
|
|
78
|
+
];
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Se o parâmetro tiver outro nome (ex.: `view/:id`), informe em `uuidIdentifier="id"`.
|
|
82
|
+
|
|
83
|
+
#### Sem rota: input `uuid`
|
|
84
|
+
|
|
85
|
+
Fora dessa estrutura (ex.: um dialog), informe o registro pelo input `uuid`. Ele tem prioridade sobre a rota, e cada
|
|
86
|
+
troca de valor carrega o novo registro. Sem valor (`null`), volta para a criação com os valores iniciais.
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<schema-view
|
|
90
|
+
[schemaService]="produtoService"
|
|
91
|
+
[uuid]="uuidProduto()"
|
|
92
|
+
[redirectOnCreate]="false"
|
|
93
|
+
[noBack]="true"
|
|
94
|
+
(onDidSave)="fechar()"
|
|
95
|
+
/>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 3. O componente
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
@Component({
|
|
102
|
+
selector: 'produto-view',
|
|
103
|
+
imports: [SchemaViewComponent],
|
|
104
|
+
template: `<schema-view [schemaService]="produtoService" />`,
|
|
105
|
+
})
|
|
106
|
+
export class ProdutoViewComponent {
|
|
107
|
+
protected produtoService = inject(ProdutoService);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Só com isso a tela tem: breadcrumb (`Produtos > Novo` ou `Produtos > Edição`), botões **Voltar** e **Novo**, um input
|
|
112
|
+
por campo e o botão **Salvar**.
|
|
113
|
+
|
|
114
|
+
## Inputs
|
|
115
|
+
|
|
116
|
+
| Input | Tipo | Padrão | Descrição |
|
|
117
|
+
| ---------------- | ------------------------------------------ | -------- | ---------------------------------------------------------------------- |
|
|
118
|
+
| `schemaService` | `SchemaService<T, IP> \| null` | `null` | Campos, breadcrumb, carregamento (`show`) e salvamento (`create`/`patch`) |
|
|
119
|
+
| `schema` | `SchemaFieldOptionsRecord<T> \| null` | `null` | Campos da tela (inputs e form), no lugar do schema do service. Ver [Personalizando os campos](#personalizando-os-campos) |
|
|
120
|
+
| `saveFn` | `(values: Partial<T>) => Promise<R>` | `null` | Substitui o `create`/`patch` do service. Ver [Salvando sem service](#salvando-sem-service) |
|
|
121
|
+
| `redirectOnCreate` | `string \| false` | `'../view'` | Para onde ir depois de **criar**: `[redirectOnCreate, uuid]`, relativo à rota atual. `false`: fica na tela e vira edição do registro criado |
|
|
122
|
+
| `uuid` | `string \| null` | — | Registro a editar, no lugar do parâmetro da rota. Ver [Sem rota](#sem-rota-input-uuid) |
|
|
123
|
+
| `uuidIdentifier` | `string` | `'uuid'` | Nome do parâmetro da rota com o uuid |
|
|
124
|
+
| `lstBreadCrumbs` | `IBreadcrumbItem[]` | `[]` | Substitui o breadcrumb |
|
|
125
|
+
| `noBack` | `boolean` | `false` | Esconde Voltar |
|
|
126
|
+
| `noNew` | `boolean` | `false` | Esconde Novo |
|
|
127
|
+
| `noSave` | `boolean` | `false` | Esconde Salvar (tela só de leitura) |
|
|
128
|
+
|
|
129
|
+
É preciso informar `schemaService` ou `schema`.
|
|
130
|
+
|
|
131
|
+
## Outputs
|
|
132
|
+
|
|
133
|
+
| Output | Tipo | Descrição |
|
|
134
|
+
| ----------- | ------- | ------------------------------------------------------------------------------------------------------ |
|
|
135
|
+
| `onDidSave` | `R` | Depois de salvar (criação ou edição), com o que o salvamento devolveu. Vem vazio se a resposta for vazia (ex.: `204`). Não emite sem alterações, em erro nem com form inválido |
|
|
136
|
+
| `novo` | `Event` | Clique em Novo, antes de navegar. `event.preventDefault()` cancela |
|
|
137
|
+
|
|
138
|
+
`R` é o tipo que o salvamento devolve: o retorno do `saveFn`, ou `T` quando quem salva é o service.
|
|
139
|
+
|
|
140
|
+
Voltar para a listagem depois de salvar (desligue o redirect para a navegação ficar só com você):
|
|
141
|
+
|
|
142
|
+
```html
|
|
143
|
+
<schema-view [schemaService]="produtoService" [redirectOnCreate]="false" (onDidSave)="aoSalvar($event)" />
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
private router = inject(Router);
|
|
148
|
+
|
|
149
|
+
protected aoSalvar(produto: IProduto) {
|
|
150
|
+
console.log('Salvo:', produto.uuid);
|
|
151
|
+
this.router.navigate(['/produtos']);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Membros públicos
|
|
156
|
+
|
|
157
|
+
Acessíveis por uma referência de template (`#view`) ou `viewChild(SchemaViewComponent)`:
|
|
158
|
+
|
|
159
|
+
| Membro | Tipo | Descrição |
|
|
160
|
+
| ----------- | --------------------- | ----------------------------------------------- |
|
|
161
|
+
| `form()` | `FieldTree<T>` | O form (signal forms). Recriado quando o `schema` ou o `schemaService` muda |
|
|
162
|
+
| `uuid` | `WritableSignal<string>` | uuid em edição (input `uuid` ou rota). Vazio na criação. `uuid.set()` troca o registro sem navegar |
|
|
163
|
+
| `loading()` | `boolean` | Carregando ou salvando (inputs em skeleton) |
|
|
164
|
+
| `saving()` | `boolean` | Salvando |
|
|
165
|
+
| `save()` | `Promise<void>` | O mesmo que clicar em Salvar |
|
|
166
|
+
| `load(uuid?)` | `Promise<void>` | Recarrega o registro (padrão: o `uuid()` atual). Precisa de `schemaService` |
|
|
167
|
+
|
|
168
|
+
## Template: `schemaViewContent`
|
|
169
|
+
|
|
170
|
+
Conteúdo extra dentro do card. Não tem contexto: para ler o form, use a referência do componente.
|
|
171
|
+
|
|
172
|
+
| Input | Tipo | Padrão | Descrição |
|
|
173
|
+
| ---------- | ------------------ | ------- | ------------------------------------------------------------------------- |
|
|
174
|
+
| `position` | `'start' \| 'end'` | `'end'` | `start`: entre os botões do topo e os campos. `end`: depois dos campos, antes de Salvar |
|
|
175
|
+
|
|
176
|
+
```html
|
|
177
|
+
<schema-view #view [schemaService]="produtoService">
|
|
178
|
+
<ng-template schemaViewContent position="start">
|
|
179
|
+
@if (!view.form().ativo().value()) {
|
|
180
|
+
<div class="rounded-lg bg-muted p-2 text-sm">
|
|
181
|
+
Produto inativo: não aparece na loja.
|
|
182
|
+
</div>
|
|
183
|
+
}
|
|
184
|
+
</ng-template>
|
|
185
|
+
</schema-view>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Campos
|
|
189
|
+
|
|
190
|
+
Cada campo do schema vira um input, na ordem em que foi declarado:
|
|
191
|
+
|
|
192
|
+
| Tipo | Input |
|
|
193
|
+
| ---------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
194
|
+
| `String`, `UUID` | [input-string](../inputs/input-string/README.md). `options.type` define o `type` do `<input>` (ex.: `'email'`) |
|
|
195
|
+
| `TextArea` | [input-text-area](../inputs/input-text-area/README.md) |
|
|
196
|
+
| `Number` / `Decimal` / `Currency` | [input-number / input-decimal / input-currency](../inputs/input-number/README.md) |
|
|
197
|
+
| `Bool` | [input-bool](../inputs/input-bool/README.md) |
|
|
198
|
+
| `Date` / `DateTime` / `Time` | [input-date](../inputs/date/input-date/README.md) / [input-date-time](../inputs/date/input-date-time/README.md) / [input-time](../inputs/date/input-time/README.md) |
|
|
199
|
+
| `DateSpan` / `DateTimeSpan` / `TimeSpan` | [input-date-span](../inputs/date/input-date-span/README.md) / [input-date-time-span](../inputs/date/input-date-time-span/README.md) / [input-time-span](../inputs/date/input-time-span/README.md) |
|
|
200
|
+
| `Phone` | [input-phone](../inputs/input-phone/README.md) |
|
|
201
|
+
| `Cpf` / `Cnpj` / `CpfCnpj` | [input-cpf / input-cnpj / input-cpf-cnpj](../inputs/input-cpf-cnpj/README.md) |
|
|
202
|
+
| `Mask` | [input-mask](../inputs/input-mask/README.md), com `options.maskitoOptions` e `options.unmaskHandler` |
|
|
203
|
+
| `Select` | [input-select](../inputs/input-select/README.md), com as `options` abaixo |
|
|
204
|
+
| `Icon` | [input-icon](../inputs/input-icon/README.md) |
|
|
205
|
+
| `File` | [input-file](../inputs/input-file/README.md), com `options.clearable` |
|
|
206
|
+
| `JSON` | [input-json](../inputs/input-json/README.md) |
|
|
207
|
+
|
|
208
|
+
Do schema, cada input recebe `label`, `placeholder`, `tooltipInfo` e `class`.
|
|
209
|
+
|
|
210
|
+
### Select
|
|
211
|
+
|
|
212
|
+
Duas formas de `options`:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// Busca na API de outro service (paginada, com busca no servidor)
|
|
216
|
+
uuidCategoria: {
|
|
217
|
+
type: FieldType.Select,
|
|
218
|
+
label: 'Categoria',
|
|
219
|
+
options: {
|
|
220
|
+
service: () => categoriaService,
|
|
221
|
+
lstFilter: [{ index: 'ativa', value: true }], // filtros enviados no index do outro service
|
|
222
|
+
clearable: true,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// Lista fixa: bindValue precisa produzir o tipo do campo
|
|
227
|
+
unidade: {
|
|
228
|
+
type: FieldType.Select,
|
|
229
|
+
label: 'Unidade',
|
|
230
|
+
options: {
|
|
231
|
+
lstOptions: [
|
|
232
|
+
{ id: 'UN', nome: 'Unidade' },
|
|
233
|
+
{ id: 'KG', nome: 'Quilo' },
|
|
234
|
+
],
|
|
235
|
+
bindValue: 'id',
|
|
236
|
+
bindLabel: 'nome',
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Layout
|
|
242
|
+
|
|
243
|
+
Os campos ficam num grid de 12 colunas. O `class` do campo define a largura:
|
|
244
|
+
|
|
245
|
+
| Tipo | `class` padrão | Resultado |
|
|
246
|
+
| ----------- | ----------------------------------------- | ----------------------------------------- |
|
|
247
|
+
| `TextArea` | `col-span-full` | Linha inteira |
|
|
248
|
+
| os demais | `xl:col-span-3 md:col-span-6 col-span-full` | 4 por linha no `xl`, 2 no `md`, 1 no celular |
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
nome: { type: FieldType.String, label: 'Nome', class: 'md:col-span-8 col-span-full' },
|
|
252
|
+
preco: { type: FieldType.Currency, label: 'Preço', class: 'md:col-span-4 col-span-full' },
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`hidden: true` ou `hidden: { hiddenForm: true }` esconde o input, mas o campo continua no form (o valor é enviado na
|
|
256
|
+
criação).
|
|
257
|
+
|
|
258
|
+
### Valores iniciais
|
|
259
|
+
|
|
260
|
+
Na criação, cada campo começa com o `defaultValue` do schema ou, sem ele, com o padrão do tipo:
|
|
261
|
+
|
|
262
|
+
| Tipo | Padrão |
|
|
263
|
+
| --------------------------------- | ------- |
|
|
264
|
+
| `Number`, `Decimal`, `Currency` | `0` |
|
|
265
|
+
| `Bool` | `false` |
|
|
266
|
+
| os demais | `null` |
|
|
267
|
+
|
|
268
|
+
### Validação e somente leitura
|
|
269
|
+
|
|
270
|
+
`validators` recebe uma regra ou uma lista de regras do signal forms (`(path) => void`):
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { email, maxLength, required } from '@angular/forms/signals';
|
|
274
|
+
import { cpfValidator, urlValidator } from '@schema-kit';
|
|
275
|
+
|
|
276
|
+
email: { type: FieldType.String, label: 'E-mail', options: { type: 'email' }, validators: [required, email] },
|
|
277
|
+
sku: { type: FieldType.String, label: 'SKU', validators: (path) => maxLength(path, 20) },
|
|
278
|
+
cpf: { type: FieldType.Cpf, label: 'CPF', validators: cpfValidator },
|
|
279
|
+
site: { type: FieldType.String, label: 'Site', validators: urlValidator },
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Validators prontos do `@schema-kit`: `cpfValidator`, `cnpjValidator`, `cpfCnpjValidator`, `urlValidator`,
|
|
283
|
+
`partialUrlValidator` e `passwordValidator`.
|
|
284
|
+
|
|
285
|
+
Ao salvar com o form inválido, todos os campos são marcados como tocados (as mensagens aparecem embaixo de cada input) e
|
|
286
|
+
um toast lista os labels dos campos com erro.
|
|
287
|
+
|
|
288
|
+
Para um campo só de leitura, use `readonly: true` (aplica a regra `readonly` do signal forms):
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
criadoPor: { type: FieldType.String, label: 'Criado por', readonly: true },
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
O valor continua no form: vai no `POST` da criação e, como não pode ser alterado, nunca entra no `PATCH`. Para uma tela
|
|
295
|
+
inteira só de leitura, combine `readonly` nos campos com `[noSave]="true"`.
|
|
296
|
+
|
|
297
|
+
## Salvando
|
|
298
|
+
|
|
299
|
+
| Modo | Ao abrir | Ao salvar | Depois |
|
|
300
|
+
| ---------------------- | ------------------------------ | ----------------------------------------------------------- | ------------------------------ |
|
|
301
|
+
| Criação (sem uuid) | Form com os valores iniciais | `POST ${endpoint}` com **todos** os valores | Vai para `../view/<uuid>` |
|
|
302
|
+
| Edição (com uuid) | `GET ${endpoint}/${uuid}` | `PATCH ${endpoint}/${uuid}` só com os campos **alterados** | Fica na tela |
|
|
303
|
+
|
|
304
|
+
```jsonc
|
|
305
|
+
// Na edição de /produtos/view/3f2a…, mudando só o preço:
|
|
306
|
+
// PATCH /produtos/3f2a…
|
|
307
|
+
{ "preco": 19.9 }
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
- Sem alterações, nada é enviado e aparece o toast "Nenhum dado foi alterado".
|
|
311
|
+
- Se algum valor for arquivo (`File`, `File[]`, `FileList`), o corpo vai como `FormData`: o arquivo na chave do campo,
|
|
312
|
+
listas em `campo[]` e os demais valores serializados em JSON.
|
|
313
|
+
- Sucesso e erro mostram um toast. Os inputs ficam em skeleton enquanto salva.
|
|
314
|
+
- Depois de criar, a tela navega para `[redirectOnCreate, uuid]` (padrão `../view/<uuid>`), substituindo a URL: ela passa
|
|
315
|
+
para edição, recarrega o registro, e o próximo Salvar envia um `PATCH`. Voltar no navegador não retorna para o form de
|
|
316
|
+
criação.
|
|
317
|
+
- Com `[redirectOnCreate]="false"`, a tela não navega: o uuid do registro criado vira o `uuid()`, o form é recarregado
|
|
318
|
+
(sem `schemaService`, fica com os valores digitados) e o próximo Salvar envia um `PATCH`.
|
|
319
|
+
- Nos dois casos, é preciso que a resposta da criação tenha `uuid`. Sem ele (ex.: um `saveFn` que devolve outro tipo,
|
|
320
|
+
ou resposta vazia), não há registro para editar: o form volta para os valores iniciais, pronto para outra criação.
|
|
321
|
+
|
|
322
|
+
### Salvando sem service
|
|
323
|
+
|
|
324
|
+
Com `saveFn`, o componente chama a função no lugar do `create`/`patch`. Ela recebe os mesmos valores (todos na criação,
|
|
325
|
+
só os alterados na edição) e devolve uma `Promise` com o resultado:
|
|
326
|
+
|
|
327
|
+
- o valor resolvido é o `$event` do `onDidSave` (e define o tipo `R`; pode ser diferente do tipo do form);
|
|
328
|
+
- se o valor tiver `uuid`, a criação faz o redirect de `redirectOnCreate` (ou, com `false`, vira edição na mesma tela);
|
|
329
|
+
- se vier vazio (ex.: resposta `204`), o toast de sucesso aparece e o `onDidSave` emite vazio;
|
|
330
|
+
- se rejeitar, aparece o toast de erro.
|
|
331
|
+
|
|
332
|
+
Junto com `schema`, dá para montar um formulário sem service:
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import { email, required } from '@angular/forms/signals';
|
|
336
|
+
import { FieldType, IView, SchemaFieldOptionsRecord, SchemaViewComponent } from '@schema-kit';
|
|
337
|
+
|
|
338
|
+
// O componente exige `T extends IView`
|
|
339
|
+
interface IContato extends IView {
|
|
340
|
+
nome: string;
|
|
341
|
+
email: string;
|
|
342
|
+
mensagem: string;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
@Component({
|
|
346
|
+
selector: 'contato',
|
|
347
|
+
imports: [SchemaViewComponent],
|
|
348
|
+
template: `
|
|
349
|
+
<schema-view
|
|
350
|
+
[schema]="schema"
|
|
351
|
+
[saveFn]="enviar"
|
|
352
|
+
[redirectOnCreate]="false"
|
|
353
|
+
[lstBreadCrumbs]="[{ label: 'Contato' }]"
|
|
354
|
+
[noBack]="true"
|
|
355
|
+
[noNew]="true"
|
|
356
|
+
(onDidSave)="enviado($event)"
|
|
357
|
+
/>
|
|
358
|
+
`,
|
|
359
|
+
})
|
|
360
|
+
export class ContatoComponent {
|
|
361
|
+
private http = inject(HttpClient);
|
|
362
|
+
private router = inject(Router);
|
|
363
|
+
|
|
364
|
+
protected schema: SchemaFieldOptionsRecord<IContato> = {
|
|
365
|
+
nome: { type: FieldType.String, label: 'Nome', validators: required },
|
|
366
|
+
email: { type: FieldType.String, label: 'E-mail', options: { type: 'email' }, validators: [required, email] },
|
|
367
|
+
mensagem: { type: FieldType.TextArea, label: 'Mensagem', validators: required },
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// Arrow function: mantém o `this` quando o schema-view chama a função.
|
|
371
|
+
// A API devolve um protocolo, não um IContato: `R` vira IProtocolo
|
|
372
|
+
protected enviar = (values: Partial<IContato>) =>
|
|
373
|
+
lastValueFrom(this.http.post<IProtocolo>('/api/contato', values));
|
|
374
|
+
|
|
375
|
+
protected enviado(protocolo: IProtocolo) {
|
|
376
|
+
this.router.navigate(['/contato/enviado', protocolo.numero]);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
interface IProtocolo {
|
|
381
|
+
numero: string;
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Sem service não há como carregar o registro: com um uuid (rota ou input), o form não é preenchido. Na edição, o
|
|
386
|
+
`saveFn` recebe só os campos alterados; o uuid do registro fica em `uuid()` (via referência `#view`). Fora da estrutura
|
|
387
|
+
`view`/`view/:uuid` (rota própria, dialog), passe `[redirectOnCreate]="false"`.
|
|
388
|
+
|
|
389
|
+
## Personalizando os campos
|
|
390
|
+
|
|
391
|
+
Com `schemaService` **e** `schema`, o `schema` substitui o do service na tela: define os inputs e o form (valores
|
|
392
|
+
iniciais, `validators` e `readonly`). O service continua carregando e salvando. Útil para ajustar uma tela sem mexer no
|
|
393
|
+
service.
|
|
394
|
+
|
|
395
|
+
O `schema` **substitui**, não completa: um campo do service que não estiver nele não aparece nem vai no form. Para
|
|
396
|
+
manter os outros campos, parta do schema do service:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
export class ProdutoViewComponent {
|
|
400
|
+
protected produtoService = inject(ProdutoService);
|
|
401
|
+
uuidCategoria = input<string>(); // parâmetro da rota, com withComponentInputBinding()
|
|
402
|
+
|
|
403
|
+
// Parte do schema do service: esconde a foto e já cria o produto na categoria da rota
|
|
404
|
+
protected schema = computed<SchemaFieldOptionsRecord<IProduto>>(() => ({
|
|
405
|
+
...this.produtoService.schema,
|
|
406
|
+
foto: { ...this.produtoService.schema.foto!, hidden: { hiddenForm: true } },
|
|
407
|
+
uuidCategoria: { ...this.produtoService.schema.uuidCategoria!, defaultValue: this.uuidCategoria() ?? null },
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
```html
|
|
413
|
+
<schema-view [schemaService]="produtoService" [schema]="schema()" />
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
O `schema` é reativo (pode ser um `computed`). Mudar o valor recria o form:
|
|
417
|
+
|
|
418
|
+
- o que o usuário já alterou continua, nos campos que ainda existem;
|
|
419
|
+
- os outros campos voltam ao valor inicial do schema novo (ex.: um `defaultValue` que mudou);
|
|
420
|
+
- na edição, o registro é recarregado, e o que foi alterado se perde.
|
|
421
|
+
|
|
422
|
+
## Navegação e breadcrumb
|
|
423
|
+
|
|
424
|
+
| Botão | Aparece | Vai para |
|
|
425
|
+
| ------ | --------------------------------- | ----------------------------------------------------------- |
|
|
426
|
+
| Voltar | Sempre (exceto com `noBack`) | A listagem: `../` em `view`, `../../` em `view/:uuid`. Depende só da rota, não do input `uuid` |
|
|
427
|
+
| Novo | Só na edição (exceto com `noNew`) | `../view` (criação). Se o uuid não veio da rota, volta para a criação na mesma tela. Cancelável com `(novo)` |
|
|
428
|
+
| Salvar | Sempre (exceto com `noSave`) | Na criação, `[redirectOnCreate, uuid]` (padrão `../view/<uuid>`); com `false`, fica e vira edição. Na edição, não navega |
|
|
429
|
+
|
|
430
|
+
O breadcrumb vem, nesta ordem, de: `lstBreadCrumbs`; `breadcrumbs` das opções do service; ou o padrão
|
|
431
|
+
`[{ label: title, routerLink: endpoint }, { label: 'Novo' | 'Edição' }]`.
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
interface IBreadcrumbItem {
|
|
435
|
+
label: string;
|
|
436
|
+
routerLink?: string;
|
|
437
|
+
icon?: string;
|
|
438
|
+
}
|
|
439
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# blurry-text-reveal
|
|
2
|
+
|
|
3
|
+
Exibe um texto com animação de entrada letra a letra: cada caractere surge desfocado, transparente e um pouco abaixo da
|
|
4
|
+
posição final, e assenta no lugar 30 ms depois do anterior.
|
|
5
|
+
|
|
6
|
+
Exemplo real de uso: [login](../../../../../../../src/app/pages/auth/login/login.component.html) (título "Backoffice").
|
|
7
|
+
|
|
8
|
+
## Uso
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { BlurryTextRevealComponent } from '@schema-kit';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
imports: [BlurryTextRevealComponent],
|
|
15
|
+
// ...
|
|
16
|
+
})
|
|
17
|
+
export class ExemploComponent {}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<blurry-text-reveal class="font-garamond text-5xl font-semibold text-primary" text="Backoffice" />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Coloque as classes de tipografia e cor no próprio `<blurry-text-reveal>`: as letras herdam fonte, tamanho e cor do host.
|
|
25
|
+
|
|
26
|
+
## API
|
|
27
|
+
|
|
28
|
+
### Inputs
|
|
29
|
+
|
|
30
|
+
| Input | Tipo | Padrão | Descrição |
|
|
31
|
+
| ------ | -------- | -------- | ------------------------------------------------------------------- |
|
|
32
|
+
| `text` | `string` | `'Text'` | Texto animado. É dividido em caracteres, com um `<span>` para cada um |
|
|
33
|
+
|
|
34
|
+
## Comportamentos a saber
|
|
35
|
+
|
|
36
|
+
- **Estilos:** a animação vem do utilitário `letter-reveal`
|
|
37
|
+
([letter-reveal.css](../../../styles/directives/letter-reveal.css)), carregado pelo `styles.scss` da lib. O `.scss` do
|
|
38
|
+
componente é vazio, então o app precisa importar os estilos da lib.
|
|
39
|
+
- **Duração:** cada letra anima por 500 ms, com atraso de `(posição - 1) × 30 ms`. Um texto de 10 letras termina em
|
|
40
|
+
cerca de 770 ms.
|
|
41
|
+
- **`sibling-index()`:** o atraso é calculado com essa função CSS, que é recente. Em navegadores sem suporte, a
|
|
42
|
+
declaração do `animation-delay` é descartada e todas as letras animam ao mesmo tempo.
|
|
43
|
+
- **Espaços:** cada caractere vira um `<span>` `inline-block`, e um `inline-block` que contém só um espaço fica com
|
|
44
|
+
largura zero. Em textos com mais de uma palavra, os espaços somem ("Novo Backoffice" aparece como "NovoBackoffice").
|
|
45
|
+
Os usos atuais são de uma palavra só.
|
|
46
|
+
- **Troca de texto:** os `<span>` são rastreados pela posição (`track $index`). Ao mudar `text`, as letras nas posições
|
|
47
|
+
que já existiam só trocam de conteúdo, sem animar de novo. Só as posições novas (quando o texto fica mais longo)
|
|
48
|
+
animam.
|
|
49
|
+
- **Movimento reduzido:** a animação não respeita `prefers-reduced-motion`.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# breadcrumbs
|
|
2
|
+
|
|
3
|
+
Trilha de navegação (breadcrumb) montada a partir de uma lista de itens, com os primitivos `hlmBreadcrumb*` do Spartan.
|
|
4
|
+
|
|
5
|
+
Usado no topo do [schema-table](../../schema-table/schema-table.component.html) e do
|
|
6
|
+
[schema-view](../../schema-view/schema-view.component.html).
|
|
7
|
+
|
|
8
|
+
## Uso
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { BreadcrumbsComponent, IBreadcrumbItem } from '@schema-kit';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
imports: [BreadcrumbsComponent],
|
|
15
|
+
// ...
|
|
16
|
+
})
|
|
17
|
+
export class ExemploComponent {
|
|
18
|
+
items: IBreadcrumbItem[] = [
|
|
19
|
+
{ label: 'Módulos', routerLink: '/modulo' },
|
|
20
|
+
{ label: 'Edição' },
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<breadcrumbs [items]="items" />
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Nas telas de schema
|
|
30
|
+
|
|
31
|
+
Com `schema-table` e `schema-view`, o componente não é usado diretamente: os itens vêm do `SchemaService`.
|
|
32
|
+
|
|
33
|
+
- Se as opções do service têm `breadcrumbs`, essa lista é usada nas duas telas.
|
|
34
|
+
- Senão, a listagem mostra só o `title`, e a edição mostra o `title` (com link para o `endpoint` do service) seguido de
|
|
35
|
+
"Edição" ou "Novo".
|
|
36
|
+
- O `schema-view` também aceita o input `lstBreadCrumbs`, que tem prioridade sobre o service.
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
### `IBreadcrumbItem`
|
|
41
|
+
|
|
42
|
+
| Campo | Tipo | Descrição |
|
|
43
|
+
| ------------ | --------- | --------------------------------------------------------------- |
|
|
44
|
+
| `label` | `string` | Texto exibido |
|
|
45
|
+
| `routerLink` | `string?` | Rota do item, passada para o `routerLink` do Angular |
|
|
46
|
+
| `icon` | `string?` | Ainda não é exibido pelo componente |
|
|
47
|
+
|
|
48
|
+
### Inputs
|
|
49
|
+
|
|
50
|
+
| Input | Tipo | Padrão | Descrição |
|
|
51
|
+
| ------- | ------------------- | ------ | ----------------------------------------- |
|
|
52
|
+
| `items` | `IBreadcrumbItem[]` | `[]` | Itens, do mais geral até a página atual |
|
|
53
|
+
|
|
54
|
+
## Comportamentos a saber
|
|
55
|
+
|
|
56
|
+
- **Último item:** é sempre a página atual e aparece como texto (`hlmBreadcrumbPage`), mesmo que tenha `routerLink`.
|
|
57
|
+
- **Itens sem link:** itens intermediários sem `routerLink` também usam `hlmBreadcrumbPage`, que marca o elemento com
|
|
58
|
+
`aria-current="page"`. Com mais de um item assim, leitores de tela anunciam mais de uma "página atual".
|
|
59
|
+
- **Rotas relativas:** um `routerLink` sem `/` no início é resolvido a partir da rota em que o componente está. Os
|
|
60
|
+
`endpoint`s dos services começam com `/`, então os links gerados por eles são absolutos.
|
|
61
|
+
- **Separador:** o ícone (`lucideChevronRight`) já é registrado pelo `hlmBreadcrumbSeparator`.
|
|
62
|
+
- **Trilhas longas:** não há colapso com reticências (`hlmBreadcrumbEllipsis`): todos os itens são exibidos.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# file-preview
|
|
2
|
+
|
|
3
|
+
Botão pequeno (`icon-xs`) de um arquivo. Imagens abrem uma pré-visualização num popover, com o [loading](../loading/README.md)
|
|
4
|
+
por cima enquanto a imagem carrega; os demais arquivos abrem/baixam pelo link.
|
|
5
|
+
|
|
6
|
+
Exemplos reais de uso: [input-file](../../inputs/input-file/input-file.component.html) (arquivo selecionado ou já salvo) e
|
|
7
|
+
[schema-table](../../schema-table/schema-table.component.html) (template das colunas `FieldType.File`).
|
|
8
|
+
|
|
9
|
+
## Uso
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { FilePreviewComponent, fileItemsFromValue } from '@schema-kit';
|
|
13
|
+
|
|
14
|
+
@Component({
|
|
15
|
+
imports: [FilePreviewComponent],
|
|
16
|
+
// ...
|
|
17
|
+
})
|
|
18
|
+
export class ExemploComponent {
|
|
19
|
+
arquivos = computed(() => fileItemsFromValue(this.registro().arquivo));
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
@for (arquivo of arquivos(); track arquivo.url) {
|
|
25
|
+
<file-preview [item]="arquivo" />
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
### Inputs
|
|
32
|
+
|
|
33
|
+
| Input | Tipo | Padrão | Descrição |
|
|
34
|
+
| ------ | ---------- | ------------- | ------------------ |
|
|
35
|
+
| `item` | `FileItem` | (obrigatório) | Arquivo a exibir |
|
|
36
|
+
|
|
37
|
+
### `FileItem`
|
|
38
|
+
|
|
39
|
+
| Campo | Tipo | Descrição |
|
|
40
|
+
| ------- | --------- | ------------------------------------------------------------------------- |
|
|
41
|
+
| `name` | `string` | Nome exibido nos rótulos de acessibilidade e usado no download de `file` |
|
|
42
|
+
| `url` | `string` | URL da imagem ou do link |
|
|
43
|
+
| `image` | `boolean` | Abre a pré-visualização em vez do link |
|
|
44
|
+
| `file` | `File?` | Arquivo local (a `url` é uma object URL); o link baixa com o nome `name` |
|
|
45
|
+
|
|
46
|
+
### Funções
|
|
47
|
+
|
|
48
|
+
- **`fileItemFromUrl(url)`:** item de um arquivo já existente. O nome é o último segmento do caminho da URL (o nome
|
|
49
|
+
original, nas URLs assinadas do bucket) e `image` vem da extensão.
|
|
50
|
+
- **`fileItemsFromValue(value)`:** itens de um valor vindo da API (`string`, `string[]` ou vazio). Ignora o que não for
|
|
51
|
+
string.
|
|
52
|
+
|
|
53
|
+
## Comportamentos a saber
|
|
54
|
+
|
|
55
|
+
- **Loading só na primeira abertura:** o popover recria a imagem a cada abertura, mas o navegador reaproveita a já
|
|
56
|
+
carregada. O loading volta a aparecer quando a `url` muda (ex.: nova URL assinada após recarregar o registro).
|
|
57
|
+
- **Erro ao carregar:** o loading some e fica o texto alternativo da imagem.
|
|
58
|
+
- **Cliques:** o componente não para a propagação do clique. Dentro de algo clicável (ex.: linha da tabela), envolva com
|
|
59
|
+
`(click)="$event.stopPropagation()"`.
|
|
60
|
+
- **Object URLs:** quem cria a object URL de um `File` é responsável por revogá-la (o `input-file` já faz isso).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# infinte-scroll
|
|
2
|
+
|
|
3
|
+
> **Não implementado.** A pasta contém só o esqueleto gerado pelo Angular CLI.
|
|
4
|
+
|
|
5
|
+
- O template só exibe `infinte-scroll works!`. Não há inputs, outputs nem lógica.
|
|
6
|
+
- O componente (`InfinteScrollComponent`, seletor `lib-infinte-scroll`) **não é exportado** no
|
|
7
|
+
[index.ts](../../../index.ts), então não está disponível via `@schema-kit`.
|
|
8
|
+
- O nome da pasta, dos arquivos e da classe tem um erro de digitação ("infinte" em vez de "infinite"). Vale corrigir
|
|
9
|
+
ao implementar, antes que alguém passe a usar.
|
|
10
|
+
- O seletor usa o prefixo `lib-`, diferente dos outros componentes de `ui/`, que não têm prefixo.
|