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