weg-shared-layout 0.0.32 → 0.0.34

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