@aster-editor/angular 0.0.0-stage → 0.4.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 +21 -0
- package/README.md +395 -2
- package/fesm2022/aster-editor-angular.mjs +624 -0
- package/fesm2022/aster-editor-angular.mjs.map +1 -0
- package/package.json +55 -3
- package/types/aster-editor-angular.d.ts +310 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joris Tremoulet
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,396 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ✳️ @aster-editor/angular
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Composant Angular standalone pour [Aster Editor](../../README.md) : `<aster-editor>` avec
|
|
4
|
+
> reactive forms, signals, zoneless, nom accessible, et import / export Word à la demande.
|
|
5
|
+
|
|
6
|
+
## 📦 Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @aster-editor/angular @aster-editor/ui @aster-editor/core
|
|
10
|
+
# optionnel, pour le collage Word nettoyé et l'import .docx
|
|
11
|
+
npm install @aster-editor/word
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Peer deps : `@angular/core`, `@angular/common`, `@angular/forms` ≥ 20. Angular 22 est la cible,
|
|
15
|
+
l'app peut être zoneless.
|
|
16
|
+
|
|
17
|
+
Importe les thèmes CSS une fois, par exemple dans `styles.css` :
|
|
18
|
+
|
|
19
|
+
```css
|
|
20
|
+
/* structure + thèmes light et night */
|
|
21
|
+
@import '@aster-editor/ui/themes/aster.css';
|
|
22
|
+
|
|
23
|
+
/* ou, pour alléger : structure + un seul thème */
|
|
24
|
+
@import '@aster-editor/ui/themes/base.css';
|
|
25
|
+
@import '@aster-editor/ui/themes/light.css';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 🚀 Exemple minimal
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { Component } from '@angular/core';
|
|
32
|
+
import { AsterEditorComponent } from '@aster-editor/angular';
|
|
33
|
+
|
|
34
|
+
@Component({
|
|
35
|
+
selector: 'app-note',
|
|
36
|
+
imports: [AsterEditorComponent],
|
|
37
|
+
template: `<aster-editor
|
|
38
|
+
toolbar="standard"
|
|
39
|
+
placeholder="Écris ici…"
|
|
40
|
+
(changed)="html = $event"
|
|
41
|
+
/>`,
|
|
42
|
+
})
|
|
43
|
+
export class NoteComponent {
|
|
44
|
+
html = '';
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
L'éditeur est monté dans `afterNextRender` (aucun accès DOM côté serveur) et détruit avec le
|
|
49
|
+
composant.
|
|
50
|
+
|
|
51
|
+
## 📝 Reactive form
|
|
52
|
+
|
|
53
|
+
Le composant implémente `ControlValueAccessor` : la valeur du contrôle est le HTML sémantique de
|
|
54
|
+
`getHTML()`. `dirty` passe à vrai à la frappe, `touched` au blur vers l'extérieur de l'éditeur
|
|
55
|
+
(cliquer la toolbar, un menu ou un dialogue ne compte pas), `disabled` met l'éditeur en lecture
|
|
56
|
+
seule **sans émettre `change`** (`setDisabledState` ne salit jamais le contrôle). `writeValue`
|
|
57
|
+
est idempotent : il n'émet rien, n'entre pas dans l'historique d'annulation, vide l'historique
|
|
58
|
+
antérieur (un `Ctrl+Z` qui suit ne rejoue pas l'ancien document) et ignore une valeur identique
|
|
59
|
+
au contenu courant (la sélection ne bouge pas). Une valeur reçue avant le montage devient le
|
|
60
|
+
contenu initial.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { Component } from '@angular/core';
|
|
64
|
+
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
65
|
+
import { AsterEditorComponent } from '@aster-editor/angular';
|
|
66
|
+
|
|
67
|
+
@Component({
|
|
68
|
+
selector: 'app-article-form',
|
|
69
|
+
imports: [AsterEditorComponent, ReactiveFormsModule],
|
|
70
|
+
template: `
|
|
71
|
+
<form [formGroup]="form" (ngSubmit)="save()">
|
|
72
|
+
<input formControlName="title" />
|
|
73
|
+
<aster-editor formControlName="body" toolbar="full" />
|
|
74
|
+
<button type="submit" [disabled]="form.invalid">Enregistrer</button>
|
|
75
|
+
</form>
|
|
76
|
+
`,
|
|
77
|
+
})
|
|
78
|
+
export class ArticleFormComponent {
|
|
79
|
+
readonly form = new FormGroup({
|
|
80
|
+
title: new FormControl('', { nonNullable: true }),
|
|
81
|
+
body: new FormControl('', { nonNullable: true }),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
save(): void {
|
|
85
|
+
console.log(this.form.getRawValue()); // { title: '…', body: '<p>…</p>' }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> Un document vide vaut `<p></p>`, pas `''` : pour un champ requis, écris un validateur qui
|
|
91
|
+
> retire les balises avant de tester la vacuité (voir `apps/demo/src/app/pages/required-rich-text.ts`).
|
|
92
|
+
|
|
93
|
+
## 🎛️ Inputs et outputs
|
|
94
|
+
|
|
95
|
+
| Input | Type | Défaut effectif | À chaud |
|
|
96
|
+
| ----------------------- | -------------------------- | --------------- | ----------------------------------------------------------- |
|
|
97
|
+
| `toolbar` | `ToolbarConfig` | `'standard'` | Remonte l'éditeur : contenu conservé, historique perdu |
|
|
98
|
+
| `theme` | `AsterTheme` | `'light'` | Oui |
|
|
99
|
+
| `locale` | `AsterLocale` | `'fr'` | Oui, toolbar comprise |
|
|
100
|
+
| `placeholder` | `string \| undefined` | chaîne i18n | Oui |
|
|
101
|
+
| `readonly` | `boolean` | `false` | Oui, cumulé avec le `disabled` du formulaire |
|
|
102
|
+
| `config` | `Partial<AsterUiOptions>` | `{}` | Seules les clés ci-dessus sont relues ; le reste au montage |
|
|
103
|
+
| `wordPaste` | `boolean` | `true` | Oui |
|
|
104
|
+
| `ariaLabel` | `string \| undefined` | aucun | Oui : `aria-label` sur le contenteditable |
|
|
105
|
+
| `ariaLabelledBy` | `string \| undefined` | aucun | Oui : `aria-labelledby` sur le contenteditable |
|
|
106
|
+
| `variables` | `readonly AsterVariable[]` | aucune | Oui : `editor.variables.define(...)`, fusion par nom |
|
|
107
|
+
| `variableValues` | `VariableValues` | aucune | Oui : `editor.variables.setValues(...)`, fusion par nom |
|
|
108
|
+
| `variableResolver` | `VariableResolver` | aucun | Non, lu au montage |
|
|
109
|
+
| `allowUnknownVariables` | `boolean` | `true` | Non, lu au montage |
|
|
110
|
+
| `variableSuggestion` | `boolean` | `true` | Non, lu au montage |
|
|
111
|
+
|
|
112
|
+
Un contenteditable ne s'associe pas à un `<label for>` : nomme la zone de saisie avec
|
|
113
|
+
`ariaLabel`, ou `ariaLabelledBy` pointant sur l'`id` de ton libellé visible. Les deux sont
|
|
114
|
+
appliqués sur `editor.contentElement` (le `.ProseMirror`), retirés quand la valeur est vide,
|
|
115
|
+
et survivent à un remontage.
|
|
116
|
+
|
|
117
|
+
Le remontage déclenché par `toolbar` recrée l'instance `createAster` : le HTML courant est
|
|
118
|
+
réinjecté comme contenu initial, `autofocus` est neutralisé et le focus est restauré en fin de
|
|
119
|
+
document s'il était dans l'éditeur, `ready` est réémis avec la nouvelle instance. Un
|
|
120
|
+
`importDocx` en cours pendant un remontage ou une destruction rend son résultat sans écrire
|
|
121
|
+
dans l'éditeur.
|
|
122
|
+
|
|
123
|
+
| Output | Payload | Quand |
|
|
124
|
+
| ------------------ | ---------------- | ---------------------------------------------------------------------------------------------- |
|
|
125
|
+
| `ready` | `AsterEditor` | Éditeur monté. Réémis après un remontage (`toolbar`) |
|
|
126
|
+
| `changed` | `string` | Document modifié, HTML de `getHTML()`. Pas sur `writeValue` |
|
|
127
|
+
| `focused` | `void` | Focus du contenu |
|
|
128
|
+
| `blurred` | `void` | Focus perdu vers l'extérieur de l'éditeur (pas vers la toolbar) ; marque le contrôle `touched` |
|
|
129
|
+
| `variablesChanged` | `VariableValues` | Valeurs ou déclarations de variables changées (`setValues`, `define`, `resolve`, import) |
|
|
130
|
+
|
|
131
|
+
API publique du composant :
|
|
132
|
+
|
|
133
|
+
- `editor` : `Signal<AsterEditor | null>`, l'éditeur du core (`getHTML`, `getJSON`, `getCounts`,
|
|
134
|
+
`commands.run(...)`, `tiptap`…), `null` avant le montage et après la destruction.
|
|
135
|
+
- `variableNames` : `Signal<readonly string[]>`, noms des variables présentes dans le document,
|
|
136
|
+
relus sur `change` et `variablesChange`.
|
|
137
|
+
- `getHTML(options?)` : HTML du document, `{ variables: 'resolve' }` substitue les valeurs.
|
|
138
|
+
- `resolveVariables()` : appelle le résolveur pour les variables sans valeur, rend les valeurs.
|
|
139
|
+
- `importDocx(file: File): Promise<DocxImportResult>` : voir plus bas.
|
|
140
|
+
- `exportDocx(options?: DocxExportOptions): Promise<Blob>` : voir « Variables et modèles ».
|
|
141
|
+
|
|
142
|
+
La vue source, le plein écran et le menu contextuel de table de `@aster-editor/ui` sont montés
|
|
143
|
+
par `createAster` et donc disponibles dans le composant : via les widgets `source` /
|
|
144
|
+
`fullscreen` d'une toolbar déclarative ou du preset `full`, ou au clavier (`Mod-f` ouvre le
|
|
145
|
+
panneau de recherche si le widget `search` est monté, `Échap` quitte le plein écran). Le composant n'expose pas `AsterInstance` : pour
|
|
146
|
+
piloter ces fonctions par code, passe par les contrôleurs partagés de `ui`
|
|
147
|
+
(`getSourceView(editor)`, `getFullscreen(editor)`) avec l'éditeur du signal.
|
|
148
|
+
|
|
149
|
+
Types réexportés pour ne pas dépendre de `core`, `ui` ou `word` : `AsterEditor`,
|
|
150
|
+
`AsterDocument`, `AsterCounts`, `AsterLocale`, `AsterTheme`, `LinkPolicy`,
|
|
151
|
+
`PasteTransformer`, `ToolbarConfig`, `ToolbarPreset`, `UploadHandler`, `AsterVariable`,
|
|
152
|
+
`VariableValues`, `VariableResolver`, `VariableSource`, `VariableRenderMode`, `GetHtmlOptions`,
|
|
153
|
+
`AsterUiOptions`, `DocxImportResult`, `DocxImportWarning`, `DocxExportOptions`,
|
|
154
|
+
`DocxExportVariableOptions`, `DocxExportWarning`, `DocxVariableMode`.
|
|
155
|
+
|
|
156
|
+
Un input dédié laissé à `undefined` n'écrase pas la valeur venue de `config` ou de
|
|
157
|
+
`provideAster`. Passe des objets **stables** dans `config` (champ de classe, pas un littéral dans
|
|
158
|
+
le template) : une nouvelle référence à chaque rendu relance la fusion, et une `toolbar`
|
|
159
|
+
déclarative recréée à chaque rendu remonterait l'éditeur.
|
|
160
|
+
|
|
161
|
+
## ⚙️ provideAster
|
|
162
|
+
|
|
163
|
+
Configuration globale d'une application ou d'une route, fusionnée sous l'input `config` qui
|
|
164
|
+
est lui-même sous les inputs dédiés :
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { ApplicationConfig } from '@angular/core';
|
|
168
|
+
import { provideAster } from '@aster-editor/angular';
|
|
169
|
+
|
|
170
|
+
export const appConfig: ApplicationConfig = {
|
|
171
|
+
providers: [
|
|
172
|
+
provideAster({
|
|
173
|
+
locale: 'en',
|
|
174
|
+
theme: 'night',
|
|
175
|
+
linkPolicy: { defaultTarget: '_blank' },
|
|
176
|
+
disabledCommands: ['fontFamily'],
|
|
177
|
+
}),
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Le token sous-jacent est `ASTER_CONFIG` (`InjectionToken<Partial<AsterUiOptions>>`).
|
|
183
|
+
|
|
184
|
+
## 🖼️ Upload d'images : ASTER_UPLOAD_HANDLER
|
|
185
|
+
|
|
186
|
+
Sans handler, les images collées, déposées ou importées sont insérées en data URL base64.
|
|
187
|
+
Pour passer par ton API :
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { inject } from '@angular/core';
|
|
191
|
+
import { HttpClient } from '@angular/common/http';
|
|
192
|
+
import { firstValueFrom } from 'rxjs';
|
|
193
|
+
import { ASTER_UPLOAD_HANDLER, UploadHandler } from '@aster-editor/angular';
|
|
194
|
+
|
|
195
|
+
export const appConfig: ApplicationConfig = {
|
|
196
|
+
providers: [
|
|
197
|
+
{
|
|
198
|
+
provide: ASTER_UPLOAD_HANDLER,
|
|
199
|
+
useFactory: (): UploadHandler => {
|
|
200
|
+
const http = inject(HttpClient);
|
|
201
|
+
return async (file) => {
|
|
202
|
+
const body = new FormData();
|
|
203
|
+
body.append('file', file);
|
|
204
|
+
const { url } = await firstValueFrom(http.post<{ url: string }>('/api/upload', body));
|
|
205
|
+
return url;
|
|
206
|
+
};
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Priorité : `config.upload` > `provideAster({ upload })` > `ASTER_UPLOAD_HANDLER`.
|
|
214
|
+
|
|
215
|
+
## 📄 Import .docx
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
@Component({
|
|
219
|
+
imports: [AsterEditorComponent],
|
|
220
|
+
template: `
|
|
221
|
+
<input type="file" accept=".docx" (change)="onFile($event)" />
|
|
222
|
+
<aster-editor />
|
|
223
|
+
`,
|
|
224
|
+
})
|
|
225
|
+
export class ImportComponent {
|
|
226
|
+
private readonly editor = viewChild.required(AsterEditorComponent);
|
|
227
|
+
|
|
228
|
+
async onFile(event: Event): Promise<void> {
|
|
229
|
+
const file = (event.target as HTMLInputElement).files?.[0];
|
|
230
|
+
if (!file) return;
|
|
231
|
+
const { warnings } = await this.editor().importDocx(file);
|
|
232
|
+
console.warn(warnings); // [{ code: 'mammoth', message: '…' }]
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`importDocx` charge `@aster-editor/word` par `import()` : mammoth reste dans un chunk séparé et
|
|
238
|
+
n'est téléchargé qu'au premier import. Le contenu est remplacé avec émission de `change`, un
|
|
239
|
+
formulaire lié voit donc la nouvelle valeur. Le handler d'upload de l'éditeur est réutilisé pour
|
|
240
|
+
les images embarquées. Appelé avant le montage, il rejette avec une erreur explicite.
|
|
241
|
+
|
|
242
|
+
> ⚠️ **Vite à froid** : sur `ng serve`, le tout premier `importDocx` d'une session déclenche
|
|
243
|
+
> l'optimisation de `mammoth` par Vite et **recharge la page** en plein import, sans erreur.
|
|
244
|
+
> C'est un comportement du serveur de dev, pas du build : relance l'import. Le test e2e de la
|
|
245
|
+
> démo fait de même (`toPass` avec nouvelle sélection du fichier).
|
|
246
|
+
|
|
247
|
+
## 📤 Export .docx
|
|
248
|
+
|
|
249
|
+
Même recette que l'import, avec `exportDocx` et `downloadDocx` chargés à la demande (la lib
|
|
250
|
+
`docx` reste dans son chunk) :
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
@Component({
|
|
254
|
+
selector: 'app-export',
|
|
255
|
+
imports: [AsterEditorComponent],
|
|
256
|
+
template: `
|
|
257
|
+
<aster-editor />
|
|
258
|
+
<button type="button" (click)="exportWord()">Exporter .docx</button>
|
|
259
|
+
`,
|
|
260
|
+
})
|
|
261
|
+
export class ExportComponent {
|
|
262
|
+
private readonly editor = viewChild.required(AsterEditorComponent);
|
|
263
|
+
|
|
264
|
+
async exportWord(): Promise<void> {
|
|
265
|
+
const editor = this.editor().editor();
|
|
266
|
+
if (!editor) return;
|
|
267
|
+
const { exportDocx, downloadDocx } = await import('@aster-editor/word');
|
|
268
|
+
const blob = await exportDocx(editor.getHTML(), { title: 'Article' });
|
|
269
|
+
downloadDocx(blob, 'article');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Options, mapping HTML → Word et limites dans le [README word](../word/README.md).
|
|
275
|
+
|
|
276
|
+
## 🧩 Variables et modèles
|
|
277
|
+
|
|
278
|
+
Les variables (champs de fusion `{{client.nom}}`) sont des puces du document, gérées par
|
|
279
|
+
`editor.variables` du core. Le composant les expose sans toucher à `config` :
|
|
280
|
+
|
|
281
|
+
| Input / output | Rôle |
|
|
282
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
283
|
+
| `variables` | Déclarations (`name`, `label`, `description`, `value`) proposées dans le menu d'insertion |
|
|
284
|
+
| `variableValues` | Valeurs par nom. À chaud, fusionnées dans le magasin (`setValues`) |
|
|
285
|
+
| `variableResolver` | `(name, editor) => string \| undefined \| Promise<…>`, appelé par `resolveVariables()` |
|
|
286
|
+
| `allowUnknownVariables` | `false` pour refuser l'insertion d'un nom non déclaré (vrai par défaut : un `.docx` importé peut contenir n'importe quoi) |
|
|
287
|
+
| `variableSuggestion` | Menu déclenché par `{{` dans le contenu (vrai par défaut) |
|
|
288
|
+
| `variablesChanged` | Output `VariableValues`, émis sur `variablesChange` du core |
|
|
289
|
+
| `variableNames` | `Signal<readonly string[]>`, noms présents dans le document |
|
|
290
|
+
|
|
291
|
+
Ces inputs priment sur `config.variables` clé par clé ; un input laissé à `undefined` ne masque
|
|
292
|
+
rien. Un remontage (`toolbar`) conserve déclarations et valeurs.
|
|
293
|
+
|
|
294
|
+
`importDocx` déclare les variables trouvées dans le `.docx` (`{{nom}}`, champs `MERGEFIELD`,
|
|
295
|
+
contrôles de contenu avec leur libellé et leur valeur) via `editor.variables.define`, puis
|
|
296
|
+
réapplique `variableValues` sur les noms que Word avait valorisés : ton état reste la source de
|
|
297
|
+
vérité. `exportDocx` charge `@aster-editor/word` à la demande, exporte `getHTML()` et transmet les
|
|
298
|
+
valeurs du magasin quand `options.variables.values` n'est pas fourni :
|
|
299
|
+
|
|
300
|
+
- `variables.mode: 'resolve'` : valeurs substituées, `{{nom}}` conservé (et avertissement
|
|
301
|
+
`variable-unresolved` via `onWarning`) pour une variable sans valeur ;
|
|
302
|
+
- `'fields'` : champs de fusion Word, la valeur connue comme résultat de champ ;
|
|
303
|
+
- `'placeholders'` (défaut) : le modèle tel quel.
|
|
304
|
+
|
|
305
|
+
Exemple complet, formulaire généré depuis `variableNames` et export :
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
@Component({
|
|
309
|
+
imports: [AsterEditorComponent],
|
|
310
|
+
template: `
|
|
311
|
+
<input type="file" accept=".docx" (change)="onFile($event)" />
|
|
312
|
+
<aster-editor
|
|
313
|
+
[variables]="declared"
|
|
314
|
+
[variableValues]="values()"
|
|
315
|
+
(variablesChanged)="store.set($event)"
|
|
316
|
+
/>
|
|
317
|
+
<form (submit)="apply($event)">
|
|
318
|
+
@for (name of editor().variableNames(); track name) {
|
|
319
|
+
<label>
|
|
320
|
+
{{ name }}
|
|
321
|
+
<input [name]="name" [value]="store()[name] ?? ''" (input)="edit(name, $event)" />
|
|
322
|
+
</label>
|
|
323
|
+
}
|
|
324
|
+
<button type="submit">Appliquer</button>
|
|
325
|
+
</form>
|
|
326
|
+
<p>{{ unresolved() }} variable(s) sans valeur</p>
|
|
327
|
+
<button type="button" (click)="download('resolve')">Télécharger résolu</button>
|
|
328
|
+
<button type="button" (click)="download('fields')">Avec champs Word</button>
|
|
329
|
+
`,
|
|
330
|
+
})
|
|
331
|
+
export class TemplateComponent {
|
|
332
|
+
protected readonly editor = viewChild.required(AsterEditorComponent);
|
|
333
|
+
protected readonly declared: readonly AsterVariable[] = [{ name: 'client.nom', label: 'Client' }];
|
|
334
|
+
protected readonly values = signal<VariableValues | undefined>(undefined);
|
|
335
|
+
protected readonly store = signal<VariableValues>({});
|
|
336
|
+
protected readonly unresolved = computed(
|
|
337
|
+
() =>
|
|
338
|
+
this.editor()
|
|
339
|
+
.variableNames()
|
|
340
|
+
.filter((name) => !this.store()[name]).length,
|
|
341
|
+
);
|
|
342
|
+
private draft: Record<string, string> = {};
|
|
343
|
+
|
|
344
|
+
async onFile(event: Event): Promise<void> {
|
|
345
|
+
const file = (event.target as HTMLInputElement).files?.[0];
|
|
346
|
+
if (file) await this.editor().importDocx(file); // déclare les variables trouvées
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
edit(name: string, event: Event): void {
|
|
350
|
+
this.draft = { ...this.draft, [name]: (event.target as HTMLInputElement).value };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
apply(event: Event): void {
|
|
354
|
+
event.preventDefault();
|
|
355
|
+
this.values.set({ ...this.draft }); // → editor.variables.setValues, puis variablesChanged
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async download(mode: DocxVariableMode): Promise<void> {
|
|
359
|
+
const blob = await this.editor().exportDocx({ variables: { mode } });
|
|
360
|
+
const { downloadDocx } = await import('@aster-editor/word');
|
|
361
|
+
downloadDocx(blob, mode === 'resolve' ? 'contrat' : 'modele');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Pour un aperçu résolu sans export, `editor().getHTML({ variables: 'resolve' })` rend le HTML
|
|
367
|
+
avec les valeurs substituées en texte brut ; `resolveVariables()` appelle d'abord le résolveur
|
|
368
|
+
pour les variables manquantes.
|
|
369
|
+
|
|
370
|
+
## 📋 Collage depuis Word
|
|
371
|
+
|
|
372
|
+
Avec `wordPaste` (vrai par défaut), le composant charge `@aster-editor/word` au montage (jamais
|
|
373
|
+
côté serveur) et branche `wordPasteTransformer()` en tête des `pasteTransformers` : le HTML de
|
|
374
|
+
Word et de Google Docs est nettoyé avant d'atteindre l'éditeur, tes propres transformateurs
|
|
375
|
+
reçoivent du HTML propre. Si le paquet `word` n'est pas installé, un avertissement est journalisé
|
|
376
|
+
et le collage reste brut. Passer `wordPaste` à `false` laisse le module chargé mais inactif.
|
|
377
|
+
|
|
378
|
+
## 🧭 App démo
|
|
379
|
+
|
|
380
|
+
`apps/demo` (`npm run demo`, http://localhost:4200) montre cinq pages chargées à la demande,
|
|
381
|
+
chacune avec son titre de document : `/` playground (preset, thème, langue, lecture seule,
|
|
382
|
+
compteurs, sortie HTML / JSON), `/form` reactive form avec validateur « requis » sur le contenu
|
|
383
|
+
riche, `/import` dépôt de `.docx` avec avertissements, `/readonly` rendu figé en thème `night`,
|
|
384
|
+
`/template` modèle à variables (exemple `08-variables-braces.docx` servi depuis
|
|
385
|
+
`public/samples/`, formulaire généré, aperçu puces / résolu, export dans les trois modes).
|
|
386
|
+
|
|
387
|
+
## 🧪 Tests
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
npx ng test aster-angular --watch=false # 39 tests du composant et de provideAster
|
|
391
|
+
npx ng test demo --watch=false # 23 tests des pages de la démo
|
|
392
|
+
npm run e2e -- --project=demo # tests Playwright sur la démo, page Modèle comprise (ng serve lancé par Playwright)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
TestBed + Vitest, environnement jsdom. Monte le composant, appelle `fixture.detectChanges()`
|
|
396
|
+
puis `await fixture.whenStable()` pour laisser `afterNextRender` créer l'éditeur.
|