@ssgoi/angular 7.1.2 → 7.1.3-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,17 @@ npm install @ssgoi/angular
12
12
 
13
13
  Agent setup guide: https://ssgoi.dev/llms.txt
14
14
 
15
+ ## Router helpers
16
+
17
+ Install this framework package once; router helpers are optional subpaths.
18
+ These router helpers are experimental APIs and may change.
19
+
20
+ | Router | Import | API |
21
+ | -------------- | ----------------------- | -------------------- |
22
+ | Angular Router | `@ssgoi/angular/router` | `SsgoiRouteBoundary` |
23
+
24
+ See the [framework guide](https://ssgoi.dev/docs/frameworks/angular) for wiring and limits.
25
+
15
26
  ## Setup
16
27
 
17
28
  Create one SSGOI root above the router outlet.
@@ -45,22 +56,24 @@ export class AppComponent {
45
56
 
46
57
  ## Route boundary
47
58
 
48
- Mark the root element of each routed component.
59
+ Use the experimental standalone directive on the routed page's single DOM root:
49
60
 
50
61
  ```ts
62
+ import { Component } from "@angular/core";
63
+ import { SsgoiRouteBoundary } from "@ssgoi/angular/router";
64
+
51
65
  @Component({
52
- selector: "app-post",
53
66
  standalone: true,
54
- template: `
55
- <article data-ssgoi-transition="/posts/42">
56
- <!-- page -->
57
- </article>
58
- `,
67
+ imports: [SsgoiRouteBoundary],
68
+ template: `<article *ssgoiRouteBoundary="let route">{{ route.id }}</article>`,
59
69
  })
60
70
  export class PostComponent {}
61
71
  ```
62
72
 
63
- The id must match the config route patterns.
73
+ It recreates the embedded view on pathname changes, including parameterized
74
+ routes that reuse the same component. Query-only navigation preserves it. Use
75
+ `key: 'shell'` for a persistent region. Keep RouterOutlet in the application;
76
+ custom reuse caches and auxiliary outlet coordination are not handled.
64
77
 
65
78
  ## Config
66
79
 
package/dist/README.md CHANGED
@@ -12,6 +12,17 @@ npm install @ssgoi/angular
12
12
 
13
13
  Agent setup guide: https://ssgoi.dev/llms.txt
14
14
 
15
+ ## Router helpers
16
+
17
+ Install this framework package once; router helpers are optional subpaths.
18
+ These router helpers are experimental APIs and may change.
19
+
20
+ | Router | Import | API |
21
+ | -------------- | ----------------------- | -------------------- |
22
+ | Angular Router | `@ssgoi/angular/router` | `SsgoiRouteBoundary` |
23
+
24
+ See the [framework guide](https://ssgoi.dev/docs/frameworks/angular) for wiring and limits.
25
+
15
26
  ## Setup
16
27
 
17
28
  Create one SSGOI root above the router outlet.
@@ -45,22 +56,24 @@ export class AppComponent {
45
56
 
46
57
  ## Route boundary
47
58
 
48
- Mark the root element of each routed component.
59
+ Use the experimental standalone directive on the routed page's single DOM root:
49
60
 
50
61
  ```ts
62
+ import { Component } from "@angular/core";
63
+ import { SsgoiRouteBoundary } from "@ssgoi/angular/router";
64
+
51
65
  @Component({
52
- selector: "app-post",
53
66
  standalone: true,
54
- template: `
55
- <article data-ssgoi-transition="/posts/42">
56
- <!-- page -->
57
- </article>
58
- `,
67
+ imports: [SsgoiRouteBoundary],
68
+ template: `<article *ssgoiRouteBoundary="let route">{{ route.id }}</article>`,
59
69
  })
60
70
  export class PostComponent {}
61
71
  ```
62
72
 
63
- The id must match the config route patterns.
73
+ It recreates the embedded view on pathname changes, including parameterized
74
+ routes that reuse the same component. Query-only navigation preserves it. Use
75
+ `key: 'shell'` for a persistent region. Keep RouterOutlet in the application;
76
+ custom reuse caches and auxiliary outlet coordination are not handled.
64
77
 
65
78
  ## Config
66
79
 
@@ -0,0 +1,90 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, TemplateRef, ViewContainerRef, Renderer2, Input, Directive } from '@angular/core';
3
+ import { ActivatedRoute, Router, NavigationEnd, createUrlTreeFromSnapshot } from '@angular/router';
4
+
5
+ /**
6
+ * @experimental Angular Router integration. The API may change.
7
+ * Use on a routed page's single DOM root, e.g. <article *ssgoiRouteBoundary>.
8
+ * Recreates the embedded view when its route key changes, including reused
9
+ * parameterized routes. It does not replace RouterOutlet or a RouteReuseStrategy.
10
+ */
11
+ class SsgoiRouteBoundary {
12
+ ssgoiRouteBoundary;
13
+ ssgoiRouteBoundaryKey;
14
+ route = inject(ActivatedRoute);
15
+ router = inject(Router);
16
+ template = inject(TemplateRef);
17
+ container = inject(ViewContainerRef);
18
+ renderer = inject(Renderer2);
19
+ view;
20
+ element;
21
+ key;
22
+ subscription;
23
+ initialized = false;
24
+ ngOnInit() {
25
+ this.initialized = true;
26
+ this.renderBoundary();
27
+ // Read committed snapshots once per successful navigation. Ancestor URL
28
+ // streams may emit separately while the router advances its route tree.
29
+ this.subscription = this.router.events.subscribe((event) => {
30
+ if (event instanceof NavigationEnd)
31
+ this.renderBoundary();
32
+ });
33
+ }
34
+ ngOnChanges() {
35
+ if (this.initialized)
36
+ this.renderBoundary();
37
+ }
38
+ ngOnDestroy() {
39
+ this.subscription?.unsubscribe();
40
+ }
41
+ renderBoundary() {
42
+ const route = this.route.snapshot;
43
+ const pathname = this.router.serializeUrl(createUrlTreeFromSnapshot(route, [], null, null));
44
+ const boundary = typeof this.ssgoiRouteBoundary === "function"
45
+ ? this.ssgoiRouteBoundary({ pathname, route })
46
+ : { id: pathname };
47
+ const key = this.ssgoiRouteBoundaryKey ?? boundary.key ?? boundary.id;
48
+ if (!this.view || key !== this.key) {
49
+ this.container.clear();
50
+ this.view = this.container.createEmbeddedView(this.template, {
51
+ $implicit: boundary,
52
+ });
53
+ const roots = this.view.rootNodes.filter((node) => node.nodeType === 1);
54
+ if (roots.length !== 1) {
55
+ this.container.clear();
56
+ this.view = undefined;
57
+ throw new Error("SSGOI: *ssgoiRouteBoundary requires exactly one DOM root.");
58
+ }
59
+ this.element = roots[0];
60
+ this.key = key;
61
+ }
62
+ else {
63
+ this.view.context.$implicit = boundary;
64
+ }
65
+ this.renderer.setAttribute(this.element, "data-ssgoi-transition", boundary.id);
66
+ this.view.markForCheck();
67
+ }
68
+ static ngTemplateContextGuard(_directive, context) {
69
+ return true;
70
+ }
71
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: SsgoiRouteBoundary, deps: [], target: i0.ɵɵFactoryTarget.Directive });
72
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.9", type: SsgoiRouteBoundary, isStandalone: true, selector: "[ssgoiRouteBoundary]", inputs: { ssgoiRouteBoundary: "ssgoiRouteBoundary", ssgoiRouteBoundaryKey: "ssgoiRouteBoundaryKey" }, usesOnChanges: true, ngImport: i0 });
73
+ }
74
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: SsgoiRouteBoundary, decorators: [{
75
+ type: Directive,
76
+ args: [{ selector: "[ssgoiRouteBoundary]", standalone: true }]
77
+ }], propDecorators: { ssgoiRouteBoundary: [{
78
+ type: Input
79
+ }], ssgoiRouteBoundaryKey: [{
80
+ type: Input
81
+ }] } });
82
+
83
+ /** @experimental Angular Router integration. The API may change. */
84
+
85
+ /**
86
+ * Generated bundle index. Do not edit.
87
+ */
88
+
89
+ export { SsgoiRouteBoundary };
90
+ //# sourceMappingURL=ssgoi-angular-router.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssgoi-angular-router.mjs","sources":["../../router/src/ssgoi-route-boundary.ts","../../router/public-api.ts","../../router/ssgoi-angular-router.ts"],"sourcesContent":["import {\n Directive,\n Input,\n Renderer2,\n TemplateRef,\n ViewContainerRef,\n inject,\n type EmbeddedViewRef,\n type OnChanges,\n type OnDestroy,\n type OnInit,\n} from \"@angular/core\";\nimport {\n ActivatedRoute,\n NavigationEnd,\n Router,\n createUrlTreeFromSnapshot,\n type ActivatedRouteSnapshot,\n} from \"@angular/router\";\n\nexport interface RouteBoundaryState {\n id: string;\n key?: string | number;\n}\n\nexport interface AngularRouteLocation {\n pathname: string;\n route: ActivatedRouteSnapshot;\n}\n\ntype BoundaryContext = { $implicit: RouteBoundaryState };\ntype ResolveBoundary = (location: AngularRouteLocation) => RouteBoundaryState;\n\n/**\n * @experimental Angular Router integration. The API may change.\n * Use on a routed page's single DOM root, e.g. <article *ssgoiRouteBoundary>.\n * Recreates the embedded view when its route key changes, including reused\n * parameterized routes. It does not replace RouterOutlet or a RouteReuseStrategy.\n */\n@Directive({ selector: \"[ssgoiRouteBoundary]\", standalone: true })\nexport class SsgoiRouteBoundary implements OnInit, OnChanges, OnDestroy {\n @Input() ssgoiRouteBoundary?: ResolveBoundary | \"\";\n @Input() ssgoiRouteBoundaryKey?: string | number;\n\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly template = inject<TemplateRef<BoundaryContext>>(TemplateRef);\n private readonly container = inject(ViewContainerRef);\n private readonly renderer = inject(Renderer2);\n private view?: EmbeddedViewRef<BoundaryContext>;\n private element?: HTMLElement;\n private key?: string | number;\n private subscription?: { unsubscribe(): void };\n private initialized = false;\n\n ngOnInit(): void {\n this.initialized = true;\n this.renderBoundary();\n // Read committed snapshots once per successful navigation. Ancestor URL\n // streams may emit separately while the router advances its route tree.\n this.subscription = this.router.events.subscribe((event) => {\n if (event instanceof NavigationEnd) this.renderBoundary();\n });\n }\n\n ngOnChanges(): void {\n if (this.initialized) this.renderBoundary();\n }\n\n ngOnDestroy(): void {\n this.subscription?.unsubscribe();\n }\n\n private renderBoundary(): void {\n const route = this.route.snapshot;\n const pathname = this.router.serializeUrl(\n createUrlTreeFromSnapshot(route, [], null, null),\n );\n const boundary =\n typeof this.ssgoiRouteBoundary === \"function\"\n ? this.ssgoiRouteBoundary({ pathname, route })\n : { id: pathname };\n const key = this.ssgoiRouteBoundaryKey ?? boundary.key ?? boundary.id;\n\n if (!this.view || key !== this.key) {\n this.container.clear();\n this.view = this.container.createEmbeddedView(this.template, {\n $implicit: boundary,\n });\n const roots = this.view.rootNodes.filter((node) => node.nodeType === 1);\n if (roots.length !== 1) {\n this.container.clear();\n this.view = undefined;\n throw new Error(\n \"SSGOI: *ssgoiRouteBoundary requires exactly one DOM root.\",\n );\n }\n this.element = roots[0];\n this.key = key;\n } else {\n this.view.context.$implicit = boundary;\n }\n this.renderer.setAttribute(\n this.element,\n \"data-ssgoi-transition\",\n boundary.id,\n );\n this.view.markForCheck();\n }\n\n static ngTemplateContextGuard(\n _directive: SsgoiRouteBoundary,\n context: unknown,\n ): context is BoundaryContext {\n return true;\n }\n}\n","/** @experimental Angular Router integration. The API may change. */\nexport { SsgoiRouteBoundary } from \"./src/ssgoi-route-boundary\";\nexport type {\n AngularRouteLocation,\n RouteBoundaryState,\n} from \"./src/ssgoi-route-boundary\";\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import * as i0 from '@angular/core';
2
+ import { OnInit, OnChanges, OnDestroy } from '@angular/core';
3
+ import { ActivatedRouteSnapshot } from '@angular/router';
4
+
5
+ interface RouteBoundaryState {
6
+ id: string;
7
+ key?: string | number;
8
+ }
9
+ interface AngularRouteLocation {
10
+ pathname: string;
11
+ route: ActivatedRouteSnapshot;
12
+ }
13
+ type BoundaryContext = {
14
+ $implicit: RouteBoundaryState;
15
+ };
16
+ type ResolveBoundary = (location: AngularRouteLocation) => RouteBoundaryState;
17
+ /**
18
+ * @experimental Angular Router integration. The API may change.
19
+ * Use on a routed page's single DOM root, e.g. <article *ssgoiRouteBoundary>.
20
+ * Recreates the embedded view when its route key changes, including reused
21
+ * parameterized routes. It does not replace RouterOutlet or a RouteReuseStrategy.
22
+ */
23
+ declare class SsgoiRouteBoundary implements OnInit, OnChanges, OnDestroy {
24
+ ssgoiRouteBoundary?: ResolveBoundary | "";
25
+ ssgoiRouteBoundaryKey?: string | number;
26
+ private readonly route;
27
+ private readonly router;
28
+ private readonly template;
29
+ private readonly container;
30
+ private readonly renderer;
31
+ private view?;
32
+ private element?;
33
+ private key?;
34
+ private subscription?;
35
+ private initialized;
36
+ ngOnInit(): void;
37
+ ngOnChanges(): void;
38
+ ngOnDestroy(): void;
39
+ private renderBoundary;
40
+ static ngTemplateContextGuard(_directive: SsgoiRouteBoundary, context: unknown): context is BoundaryContext;
41
+ static ɵfac: i0.ɵɵFactoryDeclaration<SsgoiRouteBoundary, never>;
42
+ static ɵdir: i0.ɵɵDirectiveDeclaration<SsgoiRouteBoundary, "[ssgoiRouteBoundary]", never, { "ssgoiRouteBoundary": { "alias": "ssgoiRouteBoundary"; "required": false; }; "ssgoiRouteBoundaryKey": { "alias": "ssgoiRouteBoundaryKey"; "required": false; }; }, {}, never, never, true, never>;
43
+ }
44
+
45
+ export { SsgoiRouteBoundary };
46
+ export type { AngularRouteLocation, RouteBoundaryState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssgoi/angular",
3
- "version": "7.1.2",
3
+ "version": "7.1.3-beta.0",
4
4
  "description": "Angular bindings for SSGOI - Native app-like page transitions for Angular applications",
5
5
  "private": false,
6
6
  "type": "module",
@@ -20,14 +20,19 @@
20
20
  },
21
21
  "./package.json": {
22
22
  "default": "./package.json"
23
+ },
24
+ "./router": {
25
+ "types": "./dist/router/index.d.ts",
26
+ "default": "./dist/fesm2022/ssgoi-angular-router.mjs"
23
27
  }
24
28
  },
25
29
  "dependencies": {
26
- "@ssgoi/core": "^7.1.2"
30
+ "@ssgoi/core": "^7.1.3-beta.0"
27
31
  },
28
32
  "peerDependencies": {
29
33
  "@angular/common": "^20.0.0",
30
- "@angular/core": "^20.0.0"
34
+ "@angular/core": "^20.0.0",
35
+ "@angular/router": "*"
31
36
  },
32
37
  "devDependencies": {
33
38
  "@angular-eslint/builder": "^20.3.0",
@@ -35,15 +40,20 @@
35
40
  "@angular-eslint/eslint-plugin-template": "^20.3.0",
36
41
  "@angular-eslint/schematics": "^20.3.0",
37
42
  "@angular-eslint/template-parser": "^20.3.0",
38
- "@angular/common": "^20.3.4",
39
- "@angular/compiler": "^20.3.4",
40
- "@angular/compiler-cli": "^20.3.4",
41
- "@angular/core": "^20.3.4",
43
+ "@angular/common": "20.3.9",
44
+ "@angular/compiler": "20.3.9",
45
+ "@angular/compiler-cli": "20.3.9",
46
+ "@angular/core": "20.3.9",
42
47
  "@typescript-eslint/eslint-plugin": "^8.46.0",
43
48
  "@typescript-eslint/parser": "^8.46.0",
44
49
  "eslint": "^9.37.0",
45
50
  "ng-packagr": "^20.3.0",
46
- "typescript": "~5.8.3"
51
+ "typescript": "~5.8.3",
52
+ "@angular/router": "20.3.9",
53
+ "@angular/platform-browser": "20.3.9",
54
+ "@angular/platform-browser-dynamic": "20.3.9",
55
+ "vitest": "^3.2.4",
56
+ "jsdom": "^26.1.0"
47
57
  },
48
58
  "keywords": [
49
59
  "angular",
@@ -71,10 +81,16 @@
71
81
  "node": ">=18.0.0"
72
82
  },
73
83
  "sideEffects": false,
84
+ "peerDependenciesMeta": {
85
+ "@angular/router": {
86
+ "optional": true
87
+ }
88
+ },
74
89
  "scripts": {
75
90
  "build": "ng-packagr -p ng-package.json",
76
91
  "watch": "ng-packagr -p ng-package.json --watch",
77
92
  "lint": "eslint .",
78
- "lint:fix": "eslint . --fix"
93
+ "lint:fix": "eslint . --fix",
94
+ "test:run": "vitest run"
79
95
  }
80
96
  }