weg-shared-layout 0.0.24 → 0.0.26
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 -21
- package/dist/cjs/weg-footer_2.cjs.entry.js +218 -193
- package/dist/collection/components/weg-footer/icons/instagram.svg +5 -5
- package/dist/collection/components/weg-footer/icons/linkedin.svg +3 -3
- package/dist/collection/components/weg-footer/icons/tiktok.svg +3 -3
- package/dist/collection/components/weg-footer/icons/youtube.svg +4 -4
- package/dist/collection/components/weg-footer/weg-footer.css +270 -218
- package/dist/collection/components/weg-footer/weg-footer.js +22 -26
- package/dist/collection/components/weg-header/weg-header.css +474 -473
- package/dist/collection/components/weg-header/weg-header.js +143 -139
- package/dist/collection/styles/shared.css +72 -72
- package/dist/collection/utils/layout.js +57 -9
- package/dist/components/p-BnMIy-A5.js +1 -0
- package/dist/components/weg-footer.js +1 -1
- package/dist/components/weg-header.js +1 -1
- package/dist/esm/weg-footer_2.entry.js +218 -193
- package/dist/types/components/weg-footer/weg-footer.d.ts +2 -2
- package/dist/types/components/weg-header/weg-header.d.ts +14 -11
- package/dist/types/components.d.ts +6 -6
- package/dist/types/types/layout-data.d.ts +13 -27
- package/dist/types/utils/layout.d.ts +9 -1
- package/dist/weg-shared-layout/p-e6eadfa1.entry.js +1 -0
- package/dist/weg-shared-layout/weg-shared-layout.esm.js +1 -1
- package/docs/angular-16-compatibility.md +235 -0
- package/docs/angular-integration-guide.md +1008 -1026
- package/docs/angular.md +232 -247
- package/docs/nextjs.md +306 -305
- package/docs/publishing.md +48 -48
- package/docs/react.md +261 -261
- package/docs/vanilla.md +138 -140
- package/package.json +5 -2
- package/readme.md +83 -108
- package/src/assets/dummy-data.json +221 -147
- package/src/constants/breakpoints.ts +7 -7
- package/dist/collection/components/weg-header/signed-in-layout.js +0 -13
- package/dist/collection/constants/weg-urls.js +0 -18
- package/dist/components/p-DbIEJ3IT.js +0 -1
- package/dist/types/components/weg-header/signed-in-layout.d.ts +0 -18
- package/dist/types/constants/weg-urls.d.ts +0 -18
- package/dist/weg-shared-layout/p-c6ec3888.entry.js +0 -1
- package/src/constants/weg-urls.ts +0 -23
- /package/dist/types/Users/{rakin/Documents/WEG/repos → tan/Documents/code}/weg-shared-layout/.stencil/test-utils/axe.d.ts +0 -0
- /package/dist/types/Users/{rakin/Documents/WEG/repos → tan/Documents/code}/weg-shared-layout/.stencil/test-utils/viewport.d.ts +0 -0
|
@@ -1,1026 +1,1008 @@
|
|
|
1
|
-
# Angular integration guide
|
|
2
|
-
|
|
3
|
-
Step-by-step guide for **Angular 16+** (standalone or NgModule). Covers static fixtures, HTTP layout, auth, and common pitfalls.
|
|
4
|
-
|
|
5
|
-
| Doc | When to use it |
|
|
6
|
-
| --- | --- |
|
|
7
|
-
| **This guide** | First integration, production setup, API mapping, testing, SSR |
|
|
8
|
-
| **[angular.md](./angular.md)** | Short copy-paste quick start (same steps, fewer words) |
|
|
9
|
-
| **[
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
| **
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- [ ] **Step
|
|
37
|
-
- [ ] **Step
|
|
38
|
-
- [ ] **Step
|
|
39
|
-
- [ ] **Step
|
|
40
|
-
- [ ] **Step
|
|
41
|
-
- [ ] **Step
|
|
42
|
-
- [ ] **Step
|
|
43
|
-
- [ ] **Step
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
| `<weg-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
header.
|
|
63
|
-
header.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
| **
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
import {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
import {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
import 'weg-shared-layout/weg-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
import {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
import {
|
|
210
|
-
import
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
import {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
"
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
|
977
|
-
|
|
|
978
|
-
|
|
|
979
|
-
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
[layout]="layoutData"
|
|
1010
|
-
[signedIn]="signedIn"
|
|
1011
|
-
[userName]="userName"
|
|
1012
|
-
(wegAuthClick)="onAuthClick($event)"
|
|
1013
|
-
></weg-header>
|
|
1014
|
-
|
|
1015
|
-
<router-outlet></router-outlet>
|
|
1016
|
-
|
|
1017
|
-
<weg-footer [layout]="layoutData"></weg-footer>
|
|
1018
|
-
```
|
|
1019
|
-
|
|
1020
|
-
### Related docs
|
|
1021
|
-
|
|
1022
|
-
- [Quick start (angular.md)](./angular.md)
|
|
1023
|
-
- [Package readme](../readme.md)
|
|
1024
|
-
- [Plain HTML / vanilla JS](./vanilla.md)
|
|
1025
|
-
- [React SPA](./react.md)
|
|
1026
|
-
- [Reference demo app](https://github.com/jobsac/weg-angular-demo)
|
|
1
|
+
# Angular integration guide
|
|
2
|
+
|
|
3
|
+
Step-by-step guide for **Angular 16+** (standalone or NgModule). Covers static fixtures, HTTP layout, auth, and common pitfalls.
|
|
4
|
+
|
|
5
|
+
| Doc | When to use it |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| **This guide** | First integration, production setup, API mapping, testing, SSR |
|
|
8
|
+
| **[angular.md](./angular.md)** | Short copy-paste quick start (same steps, fewer words) |
|
|
9
|
+
| **[angular-16-compatibility.md](./angular-16-compatibility.md)** | TS 4.9 / `stencil-public-runtime` errors, subpath import / `moduleResolution` |
|
|
10
|
+
| **[weg-angular-demo](https://github.com/jobsac/weg-angular-demo)** | Runnable reference app |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## How to read this guide
|
|
15
|
+
|
|
16
|
+
1. Follow **[Step-by-step integration](#step-by-step-integration)** in order — that is the complete happy path.
|
|
17
|
+
2. Skim **[Layout data reference](#layout-data-reference)** when shaping CMS/API JSON.
|
|
18
|
+
3. Use **[Optional sections](#optional-sections)** only when they match your app (separate API arrays, signals, SSR, tests, etc.).
|
|
19
|
+
4. If something breaks, go to **[Troubleshooting](#troubleshooting)**.
|
|
20
|
+
|
|
21
|
+
**Steps 6 and 7 include two variants each** — pick the one that matches your app:
|
|
22
|
+
|
|
23
|
+
| Variant | Angular | State style |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| **6A / 7A** | 16+ | Plain fields + RxJS `async` pipe (HTTP) |
|
|
26
|
+
| **6B / 7B** | 16.1+ (signals) | `signal()` + `toSignal()` for HTTP |
|
|
27
|
+
|
|
28
|
+
The web components are identical; only Angular template syntax differs (`layoutData` vs `layoutData()`).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Integration checklist
|
|
33
|
+
|
|
34
|
+
Copy this list while integrating:
|
|
35
|
+
|
|
36
|
+
- [ ] **Step 1** — `npm i weg-shared-layout`
|
|
37
|
+
- [ ] **Step 2** — `defineCustomElements()` in `main.ts` **before** bootstrap
|
|
38
|
+
- [ ] **Step 3** — `resolveJsonModule` in `tsconfig.app.json` (if importing JSON)
|
|
39
|
+
- [ ] **Step 4** — `src/app/auth.ts` with sign-in URL constants (host app, not from package)
|
|
40
|
+
- [ ] **Step 5** — `CUSTOM_ELEMENTS_SCHEMA` on the shell `@Component` (or `@NgModule`) that owns `<weg-header>` / `<weg-footer>`
|
|
41
|
+
- [ ] **Step 6** — Shell template with **`[layout]`** property binding (not `layout="..."`)
|
|
42
|
+
- [ ] **Step 7** — Replace fixture with HTTP/CMS when ready; keep `{ header, footer }` object shape
|
|
43
|
+
- [ ] **Step 8** — Header: `[signedIn]`, `[userName]`, `(wegAuthClick)` + `preventDefault()` if you handle auth
|
|
44
|
+
- [ ] **Step 9** — Confirm in DevTools: `$0.layout` is an object on `<weg-header>`
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## What you are integrating
|
|
49
|
+
|
|
50
|
+
`weg-shared-layout` ships two **Stencil Web Components**:
|
|
51
|
+
|
|
52
|
+
| Tag | Purpose |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `<weg-header>` | Header — bundled logo, CMS nav (signed out), built-in nav (signed in), Sign in / Manage Account / Sign out |
|
|
55
|
+
| `<weg-footer>` | Footer — social links, menu, credits, copyright |
|
|
56
|
+
|
|
57
|
+
They are **presentational**: they **do not** fetch data. Your Angular app loads layout JSON and passes it in.
|
|
58
|
+
|
|
59
|
+
Angular treats them as **custom elements**. Bind JavaScript **properties** (square brackets in templates):
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
header.layout = { header: { ... }, footer: { ... } };
|
|
63
|
+
header.signedIn = true;
|
|
64
|
+
header.userName = 'Alex';
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The components do not depend on signals, RxJS, or standalone vs NgModule — only that values reach the element properties.
|
|
68
|
+
|
|
69
|
+
### Requirements
|
|
70
|
+
|
|
71
|
+
| Requirement | Notes |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| **Angular 16+** | Standalone and NgModule both work |
|
|
74
|
+
| **Property binding** | `[layout]="obj"` — not `layout="{{ obj }}"` |
|
|
75
|
+
| **`CUSTOM_ELEMENTS_SCHEMA`** | On every component template that uses the tags; **does not** cascade to routed children |
|
|
76
|
+
| **One-time registration** | `defineCustomElements()` once per browser load (recommended) |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Step-by-step integration
|
|
81
|
+
|
|
82
|
+
These steps assume a **standalone** Angular app with `src/app/app.ts` + `src/app/app.html`. For **NgModule** bootstrap, see [Optional: NgModule bootstrap](#optional-ngmodule-bootstrap).
|
|
83
|
+
|
|
84
|
+
### Step 1 — Install the package
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm i weg-shared-layout
|
|
88
|
+
# or
|
|
89
|
+
pnpm add weg-shared-layout
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### Step 2 — Register custom elements (once, before bootstrap)
|
|
95
|
+
|
|
96
|
+
**Recommended:** call the Stencil loader in `src/main.ts` **before** `bootstrapApplication`:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// src/main.ts
|
|
100
|
+
import { bootstrapApplication } from '@angular/platform-browser';
|
|
101
|
+
import { defineCustomElements } from 'weg-shared-layout/loader';
|
|
102
|
+
|
|
103
|
+
import { appConfig } from './app/app.config';
|
|
104
|
+
import { App } from './app/app';
|
|
105
|
+
|
|
106
|
+
defineCustomElements();
|
|
107
|
+
|
|
108
|
+
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Alternative:** side-effect import only the tags you use (also once, before bootstrap). Pick one approach, not both:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import 'weg-shared-layout/weg-header';
|
|
115
|
+
import 'weg-shared-layout/weg-footer';
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
If header/footer are empty but tags appear in the DOM, registration did not run or ran too late.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### Step 3 — TypeScript: import layout JSON
|
|
123
|
+
|
|
124
|
+
In `tsconfig.app.json` (or `tsconfig.json`):
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"compilerOptions": {
|
|
129
|
+
"resolveJsonModule": true
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Create a shared layout type in `src/app/layout.types.ts`:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// src/app/layout.types.ts
|
|
138
|
+
import layoutFixture from 'weg-shared-layout/dummy-data.json';
|
|
139
|
+
|
|
140
|
+
/** Aligns with weg-shared-layout / CMS GET /api/layout shape */
|
|
141
|
+
export type LayoutData = typeof layoutFixture;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
During development you can import the package fixture:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import layoutFixture from 'weg-shared-layout/dummy-data.json';
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Production: same shape from your API (e.g. [`GET /api/layout`](https://weg-payload-test.vercel.app/api/layout)).
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### Step 4 — Auth URLs in your app (not from the package)
|
|
155
|
+
|
|
156
|
+
Create `src/app/auth.ts`. Host apps own sign-in/out URLs — do not import auth constants from `weg-shared-layout`.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// src/app/auth.ts
|
|
160
|
+
export const HEADER_SIGN_IN = {
|
|
161
|
+
label: 'Sign in',
|
|
162
|
+
href: 'https://account.warwickemploymentgroup.com/account/login',
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Where to send the user after sign-out (often same as login) */
|
|
166
|
+
export const ACCOUNT_LOGIN_HREF = HEADER_SIGN_IN.href;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
If your API omits a Sign in entry in `header.menu`, append `HEADER_SIGN_IN` when building layout — see Step 7.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### Step 5 — Allow custom elements in templates
|
|
174
|
+
|
|
175
|
+
Angular's compiler does not recognize `<weg-header>` / `<weg-footer>` unless you opt in.
|
|
176
|
+
|
|
177
|
+
Add `schemas: [CUSTOM_ELEMENTS_SCHEMA]` to the **shell** component whose template contains the tags (usually `App`):
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// src/app/app.ts
|
|
181
|
+
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
182
|
+
import { RouterOutlet } from '@angular/router';
|
|
183
|
+
|
|
184
|
+
@Component({
|
|
185
|
+
selector: 'app-root',
|
|
186
|
+
imports: [RouterOutlet],
|
|
187
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
188
|
+
templateUrl: './app.html',
|
|
189
|
+
})
|
|
190
|
+
export class App {
|
|
191
|
+
// layout + auth — Step 6
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Important:** If only `App` has the header/footer, only `App` needs the schema. Routed child components do **not** inherit it from a parent.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### Step 6 — Shell component with layout (static fixture)
|
|
200
|
+
|
|
201
|
+
Complete minimal shell (no HTTP yet). Use **6A** or **6B** — not both.
|
|
202
|
+
|
|
203
|
+
#### Step 6A — Plain properties (Angular 16+)
|
|
204
|
+
|
|
205
|
+
**`src/app/app.ts`:**
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
209
|
+
import { RouterOutlet } from '@angular/router';
|
|
210
|
+
import layoutFixture from 'weg-shared-layout/dummy-data.json';
|
|
211
|
+
|
|
212
|
+
import { ACCOUNT_LOGIN_HREF, HEADER_SIGN_IN } from './auth';
|
|
213
|
+
import type { LayoutData } from './layout.types';
|
|
214
|
+
|
|
215
|
+
@Component({
|
|
216
|
+
selector: 'app-root',
|
|
217
|
+
imports: [RouterOutlet],
|
|
218
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
219
|
+
templateUrl: './app.html',
|
|
220
|
+
})
|
|
221
|
+
export class App {
|
|
222
|
+
/** Same object for header and footer */
|
|
223
|
+
layoutData: LayoutData = layoutFixture;
|
|
224
|
+
|
|
225
|
+
signedIn = false;
|
|
226
|
+
userName?: string;
|
|
227
|
+
|
|
228
|
+
onAuthClick(event: Event): void {
|
|
229
|
+
const customEvent = event as CustomEvent<{ action: 'sign-in' | 'sign-out' }>;
|
|
230
|
+
customEvent.preventDefault();
|
|
231
|
+
|
|
232
|
+
if (customEvent.detail.action === 'sign-out') {
|
|
233
|
+
this.signedIn = false;
|
|
234
|
+
window.location.href = ACCOUNT_LOGIN_HREF;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
window.location.href = HEADER_SIGN_IN.href;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**`src/app/app.html`:**
|
|
244
|
+
|
|
245
|
+
```html
|
|
246
|
+
<weg-header
|
|
247
|
+
[layout]="layoutData"
|
|
248
|
+
[signedIn]="signedIn"
|
|
249
|
+
[userName]="userName"
|
|
250
|
+
(wegAuthClick)="onAuthClick($event)"
|
|
251
|
+
></weg-header>
|
|
252
|
+
|
|
253
|
+
<main>
|
|
254
|
+
<router-outlet />
|
|
255
|
+
</main>
|
|
256
|
+
|
|
257
|
+
<weg-footer [layout]="layoutData"></weg-footer>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Binding rules (6A):
|
|
261
|
+
|
|
262
|
+
| Correct | Wrong |
|
|
263
|
+
| --- | --- |
|
|
264
|
+
| `[layout]="layoutData"` | `layout="{{ layoutData }}"` (string attribute) |
|
|
265
|
+
| `[signedIn]="signedIn"` | Omitting `signedIn` when you need signed-in nav |
|
|
266
|
+
| No `()` on bindings | `layoutData()` — that is for **6B** only |
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
#### Step 6B — Signals (Angular 16.1+)
|
|
271
|
+
|
|
272
|
+
Same shell as 6A; bindings use `()` because values are signals.
|
|
273
|
+
|
|
274
|
+
**`src/app/app.ts`:**
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';
|
|
278
|
+
import { RouterOutlet } from '@angular/router';
|
|
279
|
+
import layoutFixture from 'weg-shared-layout/dummy-data.json';
|
|
280
|
+
|
|
281
|
+
import { ACCOUNT_LOGIN_HREF, HEADER_SIGN_IN } from './auth';
|
|
282
|
+
import type { LayoutData } from './layout.types';
|
|
283
|
+
|
|
284
|
+
@Component({
|
|
285
|
+
selector: 'app-root',
|
|
286
|
+
imports: [RouterOutlet],
|
|
287
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
288
|
+
templateUrl: './app.html',
|
|
289
|
+
})
|
|
290
|
+
export class App {
|
|
291
|
+
readonly layoutData = signal<LayoutData>(layoutFixture);
|
|
292
|
+
|
|
293
|
+
readonly signedIn = signal(false);
|
|
294
|
+
readonly userName = signal<string | undefined>(undefined);
|
|
295
|
+
|
|
296
|
+
onAuthClick(event: Event): void {
|
|
297
|
+
const customEvent = event as CustomEvent<{ action: 'sign-in' | 'sign-out' }>;
|
|
298
|
+
customEvent.preventDefault();
|
|
299
|
+
|
|
300
|
+
if (customEvent.detail.action === 'sign-out') {
|
|
301
|
+
this.signedIn.set(false);
|
|
302
|
+
this.userName.set(undefined);
|
|
303
|
+
window.location.href = ACCOUNT_LOGIN_HREF;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
window.location.href = HEADER_SIGN_IN.href;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**`src/app/app.html`:**
|
|
313
|
+
|
|
314
|
+
```html
|
|
315
|
+
<weg-header
|
|
316
|
+
[layout]="layoutData()"
|
|
317
|
+
[signedIn]="signedIn()"
|
|
318
|
+
[userName]="userName()"
|
|
319
|
+
(wegAuthClick)="onAuthClick($event)"
|
|
320
|
+
></weg-header>
|
|
321
|
+
|
|
322
|
+
<main>
|
|
323
|
+
<router-outlet />
|
|
324
|
+
</main>
|
|
325
|
+
|
|
326
|
+
<weg-footer [layout]="layoutData()"></weg-footer>
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Run the app — you should see header nav and footer. Continue to **Step 7A** or **7B** for HTTP.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
### Step 7 — Load layout from your API
|
|
334
|
+
|
|
335
|
+
Use **7A** (RxJS `async` pipe) or **7B** (`toSignal`) — same `LayoutService` as below. Pick the pair that matches Step 6 (6A → 7A, 6B → 7B).
|
|
336
|
+
|
|
337
|
+
**`src/app/layout.service.ts`** (shared by 7A and 7B):
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { Injectable } from '@angular/core';
|
|
341
|
+
import { HttpClient } from '@angular/common/http';
|
|
342
|
+
import { map, Observable } from 'rxjs';
|
|
343
|
+
|
|
344
|
+
import { HEADER_SIGN_IN } from './auth';
|
|
345
|
+
import type { LayoutData } from './layout.types';
|
|
346
|
+
|
|
347
|
+
@Injectable({ providedIn: 'root' })
|
|
348
|
+
export class LayoutService {
|
|
349
|
+
constructor(private http: HttpClient) {}
|
|
350
|
+
|
|
351
|
+
/** CMS returns full { header, footer } object */
|
|
352
|
+
loadLayout(): Observable<LayoutData> {
|
|
353
|
+
return this.http.get<LayoutData>('/api/layout').pipe(
|
|
354
|
+
map((data) => this.ensureSignInMenu(data)),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private ensureSignInMenu(data: LayoutData): LayoutData {
|
|
359
|
+
const menu = data.header?.menu ?? [];
|
|
360
|
+
const hasSignIn = menu.some(
|
|
361
|
+
(item) =>
|
|
362
|
+
!item.items?.length &&
|
|
363
|
+
item.label?.trim().toLowerCase() === 'sign in',
|
|
364
|
+
);
|
|
365
|
+
if (hasSignIn) return data;
|
|
366
|
+
return {
|
|
367
|
+
...data,
|
|
368
|
+
header: { ...data.header, menu: [...menu, HEADER_SIGN_IN] },
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Register `provideHttpClient()` in `app.config.ts` if not already present.
|
|
375
|
+
|
|
376
|
+
#### Step 7A — HTTP with RxJS + `async` pipe (pairs with 6A)
|
|
377
|
+
|
|
378
|
+
**`src/app/app.ts`:**
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
import { AsyncPipe } from '@angular/common';
|
|
382
|
+
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
383
|
+
import { RouterOutlet } from '@angular/router';
|
|
384
|
+
import { Observable } from 'rxjs';
|
|
385
|
+
|
|
386
|
+
import { ACCOUNT_LOGIN_HREF, HEADER_SIGN_IN } from './auth';
|
|
387
|
+
import type { LayoutData } from './layout.types';
|
|
388
|
+
import { LayoutService } from './layout.service';
|
|
389
|
+
|
|
390
|
+
@Component({
|
|
391
|
+
selector: 'app-root',
|
|
392
|
+
imports: [AsyncPipe, RouterOutlet],
|
|
393
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
394
|
+
templateUrl: './app.html',
|
|
395
|
+
})
|
|
396
|
+
export class App {
|
|
397
|
+
layout$: Observable<LayoutData>;
|
|
398
|
+
signedIn = false;
|
|
399
|
+
userName?: string;
|
|
400
|
+
|
|
401
|
+
constructor(layoutService: LayoutService) {
|
|
402
|
+
this.layout$ = layoutService.loadLayout();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
onAuthClick(event: Event): void {
|
|
406
|
+
const customEvent = event as CustomEvent<{ action: 'sign-in' | 'sign-out' }>;
|
|
407
|
+
customEvent.preventDefault();
|
|
408
|
+
|
|
409
|
+
if (customEvent.detail.action === 'sign-out') {
|
|
410
|
+
this.signedIn = false;
|
|
411
|
+
window.location.href = ACCOUNT_LOGIN_HREF;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
window.location.href = HEADER_SIGN_IN.href;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**`src/app/app.html`:**
|
|
421
|
+
|
|
422
|
+
```html
|
|
423
|
+
@if (layout$ | async; as layout) {
|
|
424
|
+
<weg-header
|
|
425
|
+
[layout]="layout"
|
|
426
|
+
[signedIn]="signedIn"
|
|
427
|
+
[userName]="userName"
|
|
428
|
+
(wegAuthClick)="onAuthClick($event)"
|
|
429
|
+
></weg-header>
|
|
430
|
+
|
|
431
|
+
<main>
|
|
432
|
+
<router-outlet />
|
|
433
|
+
</main>
|
|
434
|
+
|
|
435
|
+
<weg-footer [layout]="layout"></weg-footer>
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
`@if (layout$ | async; as layout)` avoids a flash of empty header while the request is in flight. On older Angular without `@if`, use `*ngIf="layout$ | async as layout"`.
|
|
440
|
+
|
|
441
|
+
Use `shareReplay(1)` on `layout$` if multiple subscribers need the same payload.
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
#### Step 7B — HTTP with signals + `toSignal` (pairs with 6B)
|
|
446
|
+
|
|
447
|
+
Requires **Angular 16.1+** (`@angular/core/rxjs-interop`).
|
|
448
|
+
|
|
449
|
+
**`src/app/app.ts`:**
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { Component, CUSTOM_ELEMENTS_SCHEMA, inject, signal } from '@angular/core';
|
|
453
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
454
|
+
import { RouterOutlet } from '@angular/router';
|
|
455
|
+
|
|
456
|
+
import { ACCOUNT_LOGIN_HREF, HEADER_SIGN_IN } from './auth';
|
|
457
|
+
import { LayoutService } from './layout.service';
|
|
458
|
+
|
|
459
|
+
@Component({
|
|
460
|
+
selector: 'app-root',
|
|
461
|
+
imports: [RouterOutlet],
|
|
462
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
463
|
+
templateUrl: './app.html',
|
|
464
|
+
})
|
|
465
|
+
export class App {
|
|
466
|
+
private readonly layoutService = inject(LayoutService);
|
|
467
|
+
|
|
468
|
+
readonly layout = toSignal(this.layoutService.loadLayout());
|
|
469
|
+
readonly signedIn = signal(false);
|
|
470
|
+
readonly userName = signal<string | undefined>(undefined);
|
|
471
|
+
|
|
472
|
+
onAuthClick(event: Event): void {
|
|
473
|
+
const customEvent = event as CustomEvent<{ action: 'sign-in' | 'sign-out' }>;
|
|
474
|
+
customEvent.preventDefault();
|
|
475
|
+
|
|
476
|
+
if (customEvent.detail.action === 'sign-out') {
|
|
477
|
+
this.signedIn.set(false);
|
|
478
|
+
this.userName.set(undefined);
|
|
479
|
+
window.location.href = ACCOUNT_LOGIN_HREF;
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
window.location.href = HEADER_SIGN_IN.href;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**`src/app/app.html`:**
|
|
489
|
+
|
|
490
|
+
```html
|
|
491
|
+
@if (layout(); as layout) {
|
|
492
|
+
<weg-header
|
|
493
|
+
[layout]="layout"
|
|
494
|
+
[signedIn]="signedIn()"
|
|
495
|
+
[userName]="userName()"
|
|
496
|
+
(wegAuthClick)="onAuthClick($event)"
|
|
497
|
+
></weg-header>
|
|
498
|
+
|
|
499
|
+
<main>
|
|
500
|
+
<router-outlet />
|
|
501
|
+
</main>
|
|
502
|
+
|
|
503
|
+
<weg-footer [layout]="layout"></weg-footer>
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
`toSignal` bridges the HTTP `Observable` from `LayoutService` into a signal for the template. Session fields (`signedIn`, `userName`) stay as signals, same as Step 6B.
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
### Step 8 — Header auth behaviour
|
|
512
|
+
|
|
513
|
+
| Binding | Web component | Purpose |
|
|
514
|
+
| --- | --- | --- |
|
|
515
|
+
| `[layout]="..."` | `layout` | CMS nav when **signed out** |
|
|
516
|
+
| `[signedIn]="..."` | `signed-in` | `true` → built-in signed-in nav (ignores CMS `header.menu`) |
|
|
517
|
+
| `[userName]="..."` | `user-name` | First name on Manage Account when signed in |
|
|
518
|
+
| `(wegAuthClick)="..."` | `wegAuthClick` | `detail.action`: `'sign-in'` \| `'sign-out'` |
|
|
519
|
+
|
|
520
|
+
**Signed out** (`signedIn === false`):
|
|
521
|
+
|
|
522
|
+
- Uses `layout.header.menu` — dropdown groups (`items`) and flat links (`href`), including Sign in
|
|
523
|
+
- Logo **image** from `layout.header.logoSrc` (bundled if omitted); logo **link** always goes to WEG home
|
|
524
|
+
|
|
525
|
+
**Signed in** (`signedIn === true`):
|
|
526
|
+
|
|
527
|
+
- Built-in links: Find a job, Dashboard, Manage Account, Sign out
|
|
528
|
+
- `userName` customizes Manage Account label
|
|
529
|
+
|
|
530
|
+
Call `event.preventDefault()` in `onAuthClick` when **your** app handles navigation. Without `preventDefault()`, the component uses default redirects.
|
|
531
|
+
|
|
532
|
+
Wire `signedIn` / `userName` from your session service (set `signedIn = true` and `userName` when the user logs in).
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
### Step 9 — Verify in the browser
|
|
537
|
+
|
|
538
|
+
1. Open DevTools → Elements → select `<weg-header>`.
|
|
539
|
+
2. In the console:
|
|
540
|
+
|
|
541
|
+
```js
|
|
542
|
+
$0.layout // must be a plain object, not undefined
|
|
543
|
+
$0.signedIn // boolean
|
|
544
|
+
$0.userName // string or undefined
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
3. If `layout` is missing or looks like `"[object Object]"`, you used attribute binding instead of `[layout]`.
|
|
548
|
+
4. Footer: same `layout` object; only `layout.footer` is read.
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
## Layout data reference
|
|
553
|
+
|
|
554
|
+
Both tags accept one **`layout`** object with optional `header` and `footer` keys — **not** a bare array at the top level.
|
|
555
|
+
|
|
556
|
+
### TypeScript shape
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
type LayoutLink = {
|
|
560
|
+
label: string;
|
|
561
|
+
href?: string;
|
|
562
|
+
target?: '_blank' | '_self' | '_parent' | '_top';
|
|
563
|
+
items?: LayoutLink[];
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
type LayoutHeaderData = {
|
|
567
|
+
logoSrc?: string;
|
|
568
|
+
menu?: LayoutLink[];
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
type LayoutFooterSocialLink = {
|
|
572
|
+
platform: 'LinkedIn' | 'Instagram' | 'TikTok' | 'YouTube';
|
|
573
|
+
href: string;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
type LayoutFooterData = {
|
|
577
|
+
social?: LayoutFooterSocialLink[];
|
|
578
|
+
menu?: LayoutLink[][];
|
|
579
|
+
credits?: string;
|
|
580
|
+
copyright?: string;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
type LayoutData = {
|
|
584
|
+
header?: Partial<LayoutHeaderData>;
|
|
585
|
+
footer?: Partial<LayoutFooterData>;
|
|
586
|
+
};
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Full example JSON:
|
|
590
|
+
|
|
591
|
+
- **From npm:** `import layout from 'weg-shared-layout/dummy-data.json'`
|
|
592
|
+
- **In this repo:** [`src/assets/dummy-data.json`](../src/assets/dummy-data.json)
|
|
593
|
+
- **Live CMS sample:** [`GET /api/layout`](https://weg-payload-test.vercel.app/api/layout)
|
|
594
|
+
|
|
595
|
+
### Abbreviated JSON
|
|
596
|
+
|
|
597
|
+
```json
|
|
598
|
+
{
|
|
599
|
+
"header": {
|
|
600
|
+
"logoSrc": "https://www.warwickemploymentgroup.com/assets/weg-logo.svg",
|
|
601
|
+
"menu": [
|
|
602
|
+
{
|
|
603
|
+
"label": "Find a job",
|
|
604
|
+
"items": [
|
|
605
|
+
{ "label": "Graduates", "href": "https://example.com/jobs/graduates" }
|
|
606
|
+
]
|
|
607
|
+
},
|
|
608
|
+
{ "label": "Career advice", "href": "https://example.com/career-advice" },
|
|
609
|
+
{ "label": "Register", "href": "https://example.com/register" },
|
|
610
|
+
{ "label": "Sign in", "href": "https://example.com/login" }
|
|
611
|
+
]
|
|
612
|
+
},
|
|
613
|
+
"footer": {
|
|
614
|
+
"social": [
|
|
615
|
+
{ "platform": "LinkedIn", "href": "https://linkedin.com/company/example" }
|
|
616
|
+
],
|
|
617
|
+
"menu": [
|
|
618
|
+
[{ "label": "About WEG", "href": "/about" }]
|
|
619
|
+
],
|
|
620
|
+
"credits": "Warwick Employment Group …",
|
|
621
|
+
"copyright": "Copyright © Warwick Employment Group."
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Which fields are used when
|
|
627
|
+
|
|
628
|
+
| Field | Used when | Notes |
|
|
629
|
+
| --- | --- | --- |
|
|
630
|
+
| `header.logoSrc` | Signed **out** | Logo image URL; bundled if omitted |
|
|
631
|
+
| `header.menu` | Signed **out** | Unified nav — groups use `items[]`, flat links use `href` |
|
|
632
|
+
| `footer.social` | Always | `platform` must be `LinkedIn`, `Instagram`, `TikTok`, or `YouTube` exactly |
|
|
633
|
+
| `footer.menu` | Always | `LayoutLink[][]` — one link array per column |
|
|
634
|
+
| `footer.credits` | Always | Text below menu |
|
|
635
|
+
| `footer.copyright` | Always | Copyright line |
|
|
636
|
+
|
|
637
|
+
When **`signedIn` is true**, `<weg-header>` **ignores** `header.menu` from layout.
|
|
638
|
+
|
|
639
|
+
### Property binding vs attributes
|
|
640
|
+
|
|
641
|
+
```html
|
|
642
|
+
<!-- Correct -->
|
|
643
|
+
<weg-header [layout]="layoutData"></weg-header>
|
|
644
|
+
|
|
645
|
+
<!-- Wrong for objects -->
|
|
646
|
+
<weg-header layout="{{ layoutData }}"></weg-header>
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
The component can parse a JSON **string** for `layout`, but `[layout]="object"` is the Angular norm.
|
|
650
|
+
|
|
651
|
+
### Invalid entries are silently dropped
|
|
652
|
+
|
|
653
|
+
- Links without `label` or `href` are skipped
|
|
654
|
+
- Menu groups with empty `items` are skipped
|
|
655
|
+
- Unknown social `platform` values are skipped
|
|
656
|
+
- Malformed input → empty sections (no throw) — inspect `$0.layout` in DevTools
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## Optional sections
|
|
661
|
+
|
|
662
|
+
Read only what applies to your app.
|
|
663
|
+
|
|
664
|
+
### Optional: API returns separate arrays (not one layout object)
|
|
665
|
+
|
|
666
|
+
**Skip this section** if your API already returns `{ header: { ... }, footer: { ... } }`.
|
|
667
|
+
|
|
668
|
+
The top-level `[layout]` binding must be an **object**, not an array:
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
// ❌ Will NOT work — nav stays empty
|
|
672
|
+
const navLinks = [
|
|
673
|
+
{ label: 'Career advice', href: '/career-advice' },
|
|
674
|
+
];
|
|
675
|
+
// template: [layout]="navLinks"
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
**✅** Put arrays **inside** the layout object:
|
|
679
|
+
|
|
680
|
+
| Field | Array element shape |
|
|
681
|
+
| --- | --- |
|
|
682
|
+
| `header.menu` | `LayoutLink` — group with `items[]` or flat link with `href` |
|
|
683
|
+
| `footer.social` | `{ platform, href }` |
|
|
684
|
+
| `footer.menu` | `{ label, href }[]` per column |
|
|
685
|
+
|
|
686
|
+
**`src/app/layout.service.ts` — merge multiple endpoints into one menu:**
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
import { Injectable } from '@angular/core';
|
|
690
|
+
import { HttpClient } from '@angular/common/http';
|
|
691
|
+
import { forkJoin, map, Observable } from 'rxjs';
|
|
692
|
+
|
|
693
|
+
import { HEADER_SIGN_IN } from './auth';
|
|
694
|
+
import type { LayoutData } from './layout.types';
|
|
695
|
+
|
|
696
|
+
@Injectable({ providedIn: 'root' })
|
|
697
|
+
export class LayoutService {
|
|
698
|
+
constructor(private http: HttpClient) {}
|
|
699
|
+
|
|
700
|
+
loadLayoutFromParts(): Observable<LayoutData> {
|
|
701
|
+
return forkJoin({
|
|
702
|
+
menuGroups: this.http.get<NonNullable<LayoutData['header']>['menu']>(
|
|
703
|
+
'/api/nav/menu',
|
|
704
|
+
),
|
|
705
|
+
flatLinks: this.http.get<NonNullable<LayoutData['header']>['menu']>(
|
|
706
|
+
'/api/nav/links',
|
|
707
|
+
),
|
|
708
|
+
footer: this.http.get<LayoutData['footer']>('/api/footer'),
|
|
709
|
+
}).pipe(
|
|
710
|
+
map(({ menuGroups, flatLinks, footer }) => ({
|
|
711
|
+
header: {
|
|
712
|
+
menu: [...menuGroups, ...flatLinks, HEADER_SIGN_IN],
|
|
713
|
+
},
|
|
714
|
+
footer,
|
|
715
|
+
})),
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
Bind the result the same way as Step 7.
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
### Optional: Plain vs signals (cheat sheet)
|
|
726
|
+
|
|
727
|
+
| | Step 6A / 7A (plain + RxJS) | Step 6B / 7B (signals) |
|
|
728
|
+
| --- | --- | --- |
|
|
729
|
+
| Angular | 16+ | 16.1+ |
|
|
730
|
+
| Static layout | `layoutData = { ... }` | `layoutData = signal({ ... })` |
|
|
731
|
+
| Template | `[layout]="layoutData"` | `[layout]="layoutData()"` |
|
|
732
|
+
| HTTP layout | `layout$` + `\| async` | `layout = toSignal(loadLayout())` |
|
|
733
|
+
| Update signed in | `this.signedIn = true` | `this.signedIn.set(true)` |
|
|
734
|
+
|
|
735
|
+
Full code for both variants: **Step 6** and **Step 7** above, and [angular.md §5](./angular.md#5-layout-shell--pick-one).
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
### Optional: Central layout service (BehaviorSubject)
|
|
740
|
+
|
|
741
|
+
Use when multiple components need the same layout or you update it imperatively:
|
|
742
|
+
|
|
743
|
+
```ts
|
|
744
|
+
import { Injectable } from '@angular/core';
|
|
745
|
+
import { BehaviorSubject } from 'rxjs';
|
|
746
|
+
import layoutFixture from 'weg-shared-layout/dummy-data.json';
|
|
747
|
+
|
|
748
|
+
import { HEADER_SIGN_IN } from './auth';
|
|
749
|
+
import type { LayoutData } from './layout.types';
|
|
750
|
+
|
|
751
|
+
@Injectable({ providedIn: 'root' })
|
|
752
|
+
export class LayoutService {
|
|
753
|
+
private readonly layoutSubject = new BehaviorSubject<LayoutData>(layoutFixture);
|
|
754
|
+
|
|
755
|
+
readonly layout$ = this.layoutSubject.asObservable();
|
|
756
|
+
|
|
757
|
+
setLayout(data: LayoutData): void {
|
|
758
|
+
this.layoutSubject.next(this.ensureSignInMenu(data));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private ensureSignInMenu(data: LayoutData): LayoutData {
|
|
762
|
+
const menu = data.header?.menu ?? [];
|
|
763
|
+
const hasSignIn = menu.some(
|
|
764
|
+
(item) =>
|
|
765
|
+
!item.items?.length &&
|
|
766
|
+
item.label?.trim().toLowerCase() === 'sign in',
|
|
767
|
+
);
|
|
768
|
+
if (hasSignIn) return data;
|
|
769
|
+
return {
|
|
770
|
+
...data,
|
|
771
|
+
header: { ...data.header, menu: [...menu, HEADER_SIGN_IN] },
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
---
|
|
778
|
+
|
|
779
|
+
### Optional: Update layout at runtime
|
|
780
|
+
|
|
781
|
+
Stencil watches the `layout` **property**. Assign a **new object reference** when data changes:
|
|
782
|
+
|
|
783
|
+
```ts
|
|
784
|
+
// Good
|
|
785
|
+
this.layoutData = {
|
|
786
|
+
...this.layoutData,
|
|
787
|
+
header: { ...this.layoutData.header, menu: newMenu },
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// Risky — may not re-render
|
|
791
|
+
this.layoutData.header!.menu!.push(newLink);
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
With `OnPush` and plain fields, call `ChangeDetectorRef.markForCheck()` after updates.
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
### Optional: Imperative assignment (`@ViewChild`)
|
|
799
|
+
|
|
800
|
+
Rare — use when property binding is blocked (legacy wrapper):
|
|
801
|
+
|
|
802
|
+
```ts
|
|
803
|
+
import {
|
|
804
|
+
AfterViewInit,
|
|
805
|
+
Component,
|
|
806
|
+
CUSTOM_ELEMENTS_SCHEMA,
|
|
807
|
+
ElementRef,
|
|
808
|
+
ViewChild,
|
|
809
|
+
} from '@angular/core';
|
|
810
|
+
import layoutFixture from 'weg-shared-layout/dummy-data.json';
|
|
811
|
+
import type { LayoutData } from './layout.types';
|
|
812
|
+
|
|
813
|
+
@Component({
|
|
814
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
815
|
+
template: `
|
|
816
|
+
<weg-header #header></weg-header>
|
|
817
|
+
<weg-footer #footer></weg-footer>
|
|
818
|
+
`,
|
|
819
|
+
})
|
|
820
|
+
export class ShellComponent implements AfterViewInit {
|
|
821
|
+
@ViewChild('header', { read: ElementRef }) headerRef!: ElementRef<HTMLElement>;
|
|
822
|
+
@ViewChild('footer', { read: ElementRef }) footerRef!: ElementRef<HTMLElement>;
|
|
823
|
+
|
|
824
|
+
layoutData: LayoutData = layoutFixture;
|
|
825
|
+
|
|
826
|
+
ngAfterViewInit(): void {
|
|
827
|
+
const header = this.headerRef.nativeElement as HTMLElement & {
|
|
828
|
+
layout?: LayoutData;
|
|
829
|
+
};
|
|
830
|
+
header.layout = this.layoutData;
|
|
831
|
+
(this.footerRef.nativeElement as HTMLElement & { layout?: LayoutData }).layout =
|
|
832
|
+
this.layoutData;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
---
|
|
838
|
+
|
|
839
|
+
### Optional: NgModule bootstrap
|
|
840
|
+
|
|
841
|
+
**`src/main.ts`:**
|
|
842
|
+
|
|
843
|
+
```ts
|
|
844
|
+
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
|
845
|
+
import { defineCustomElements } from 'weg-shared-layout/loader';
|
|
846
|
+
|
|
847
|
+
import { AppModule } from './app/app.module';
|
|
848
|
+
|
|
849
|
+
defineCustomElements();
|
|
850
|
+
|
|
851
|
+
platformBrowserDynamic()
|
|
852
|
+
.bootstrapModule(AppModule)
|
|
853
|
+
.catch((err) => console.error(err));
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
**`src/app/app.module.ts`:**
|
|
857
|
+
|
|
858
|
+
```ts
|
|
859
|
+
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
860
|
+
import { BrowserModule } from '@angular/platform-browser';
|
|
861
|
+
|
|
862
|
+
import { App } from './app';
|
|
863
|
+
|
|
864
|
+
@NgModule({
|
|
865
|
+
declarations: [App],
|
|
866
|
+
imports: [BrowserModule],
|
|
867
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
868
|
+
bootstrap: [App],
|
|
869
|
+
})
|
|
870
|
+
export class AppModule {}
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
Steps 4–9 are the same; declare the shell component in `declarations` instead of `imports: [App]`.
|
|
874
|
+
|
|
875
|
+
---
|
|
876
|
+
|
|
877
|
+
### Optional: Testing
|
|
878
|
+
|
|
879
|
+
In test setup:
|
|
880
|
+
|
|
881
|
+
```ts
|
|
882
|
+
import { defineCustomElements } from 'weg-shared-layout/loader';
|
|
883
|
+
|
|
884
|
+
beforeAll(() => {
|
|
885
|
+
defineCustomElements();
|
|
886
|
+
});
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
Before assertions:
|
|
890
|
+
|
|
891
|
+
```ts
|
|
892
|
+
await Promise.all([
|
|
893
|
+
customElements.whenDefined('weg-header'),
|
|
894
|
+
customElements.whenDefined('weg-footer'),
|
|
895
|
+
]);
|
|
896
|
+
|
|
897
|
+
fixture.detectChanges();
|
|
898
|
+
await fixture.whenStable();
|
|
899
|
+
|
|
900
|
+
const header = fixture.nativeElement.querySelector('weg-header') as HTMLElement & {
|
|
901
|
+
layout?: LayoutData;
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
expect(
|
|
905
|
+
header.layout?.header?.menu?.some(
|
|
906
|
+
(item) => item.label === 'Sign in' && item.href === HEADER_SIGN_IN.href,
|
|
907
|
+
),
|
|
908
|
+
).toBe(true);
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
Full example: [weg-angular-demo `app.spec.ts`](https://github.com/jobsac/weg-angular-demo/blob/main/src/app/app.spec.ts).
|
|
912
|
+
|
|
913
|
+
---
|
|
914
|
+
|
|
915
|
+
### Optional: Server-side rendering (SSR)
|
|
916
|
+
|
|
917
|
+
Guard registration in `main.ts`:
|
|
918
|
+
|
|
919
|
+
```ts
|
|
920
|
+
import { defineCustomElements } from 'weg-shared-layout/loader';
|
|
921
|
+
|
|
922
|
+
if (typeof window !== 'undefined') {
|
|
923
|
+
defineCustomElements();
|
|
924
|
+
}
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
Layout JSON can be fetched on the server; custom elements render after client hydration.
|
|
928
|
+
|
|
929
|
+
---
|
|
930
|
+
|
|
931
|
+
## Footer
|
|
932
|
+
|
|
933
|
+
`<weg-footer>` only needs `[layout]` with the same object as the header:
|
|
934
|
+
|
|
935
|
+
```html
|
|
936
|
+
<weg-footer [layout]="layoutData"></weg-footer>
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
External `https://` links open in a new tab automatically.
|
|
940
|
+
|
|
941
|
+
---
|
|
942
|
+
|
|
943
|
+
## Troubleshooting
|
|
944
|
+
|
|
945
|
+
| Symptom | Likely cause | Fix |
|
|
946
|
+
| --- | --- | --- |
|
|
947
|
+
| `'weg-header' is not a known element` | Missing schema | `CUSTOM_ELEMENTS_SCHEMA` on the component that owns the template |
|
|
948
|
+
| Header/footer completely empty | Not registered | `defineCustomElements()` before bootstrap, or tag side-effect imports |
|
|
949
|
+
| Tags render, nav empty | Wrong `layout` | `[layout]="..."` with object `{ header?, footer? }` |
|
|
950
|
+
| Passed array to `[layout]` | Top-level must be object | `{ header: { menu: myArray } }` — see [Optional: API returns separate arrays](#optional-api-returns-separate-arrays-not-one-layout-object) |
|
|
951
|
+
| Auth always Sign in | `signedIn` not bound / false | Bind session; set `true` when logged in |
|
|
952
|
+
| Generic Manage Account | `userName` missing | Pass first name when `signedIn` is true |
|
|
953
|
+
| Layout changes ignored | In-place mutation | New object reference |
|
|
954
|
+
| Brief empty header with `async` | Observable not emitted yet | `@if (layout$ \| async; as layout)` or `BehaviorSubject` default |
|
|
955
|
+
| Tests fail | Elements undefined | `defineCustomElements()` in `beforeAll` |
|
|
956
|
+
| SSR `document is not defined` | Loader on server | `typeof window !== 'undefined'` guard |
|
|
957
|
+
| Missing social icons | Bad `platform` | Exactly `LinkedIn`, `Instagram`, `TikTok`, `YouTube` |
|
|
958
|
+
| Missing menu item | Normalization | Non-empty `label` and valid `items` or `href` |
|
|
959
|
+
| `Mixin<const …>` / TS1139 in `stencil-public-runtime.d.ts` | TS 4.9 parsing Stencil 5 syntax | `skipLibCheck: true` and/or TypeScript 5.0+ — see **[angular-16-compatibility](./angular-16-compatibility.md)** |
|
|
960
|
+
| Cannot resolve `weg-shared-layout/weg-header` | `moduleResolution: "node"` | Use `defineCustomElements` from `weg-shared-layout/loader`, or set `bundler` / `node16` — see **[angular-16-compatibility](./angular-16-compatibility.md)** |
|
|
961
|
+
|
|
962
|
+
### DevTools
|
|
963
|
+
|
|
964
|
+
```js
|
|
965
|
+
$0.layout
|
|
966
|
+
$0.signedIn
|
|
967
|
+
$0.userName
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
---
|
|
971
|
+
|
|
972
|
+
## Quick reference
|
|
973
|
+
|
|
974
|
+
### File map (standalone app)
|
|
975
|
+
|
|
976
|
+
| File | Role |
|
|
977
|
+
| --- | --- |
|
|
978
|
+
| `src/main.ts` | `defineCustomElements()` then bootstrap |
|
|
979
|
+
| `tsconfig.app.json` | `resolveJsonModule: true` |
|
|
980
|
+
| `src/app/auth.ts` | `HEADER_SIGN_IN`, `ACCOUNT_LOGIN_HREF` |
|
|
981
|
+
| `src/app/layout.types.ts` | `export type LayoutData = typeof layoutFixture` |
|
|
982
|
+
| `src/app/layout.service.ts` | HTTP / API mapping (Step 7, optional arrays) |
|
|
983
|
+
| `src/app/app.ts` | Shell: schema, layout, auth handler |
|
|
984
|
+
| `src/app/app.html` | `<weg-header>`, `<router-outlet>`, `<weg-footer>` |
|
|
985
|
+
|
|
986
|
+
### Minimal template
|
|
987
|
+
|
|
988
|
+
```html
|
|
989
|
+
<weg-header
|
|
990
|
+
[layout]="layoutData"
|
|
991
|
+
[signedIn]="signedIn"
|
|
992
|
+
[userName]="userName"
|
|
993
|
+
(wegAuthClick)="onAuthClick($event)"
|
|
994
|
+
></weg-header>
|
|
995
|
+
|
|
996
|
+
<router-outlet></router-outlet>
|
|
997
|
+
|
|
998
|
+
<weg-footer [layout]="layoutData"></weg-footer>
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
### Related docs
|
|
1002
|
+
|
|
1003
|
+
- [Angular 16 / TypeScript 4.9 compatibility](./angular-16-compatibility.md)
|
|
1004
|
+
- [Quick start (angular.md)](./angular.md)
|
|
1005
|
+
- [Package readme](../readme.md)
|
|
1006
|
+
- [Plain HTML / vanilla JS](./vanilla.md)
|
|
1007
|
+
- [React SPA](./react.md)
|
|
1008
|
+
- [Reference demo app](https://github.com/jobsac/weg-angular-demo)
|