@nutin/cli 2.1.0 → 3.0.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/CHANGELOG.md +322 -0
- package/README.md +14 -6
- package/bin/add-feature.mjs +1 -1
- package/bin/cli.mjs +1 -1
- package/bin/create-app.mjs +1 -1
- package/bin/update-app.mjs +1 -1
- package/lib/index.mjs +6 -2
- package/lib/src/add-feature/base-template-patcher.mjs +139 -0
- package/lib/src/add-feature/feature-adder.mjs +20 -9
- package/lib/src/add-feature/feature-context-builder.mjs +1 -0
- package/lib/src/add-feature/nutin-config-updater.mjs +34 -8
- package/lib/src/add-feature/package-json-updater.mjs +7 -9
- package/lib/src/common/feature-registry.mjs +4 -0
- package/lib/src/common/file-generator.mjs +25 -3
- package/lib/src/common/git.mjs +31 -0
- package/lib/src/common/meta-bootstrap-prompt.mjs +28 -2
- package/lib/src/common/package-json-helper.mjs +14 -4
- package/lib/src/common/project-meta.mjs +2 -2
- package/lib/src/common/template-compiler.mjs +2 -0
- package/lib/src/common/transpile-ts.mjs +94 -0
- package/lib/src/create-app/context-builder.mjs +1 -0
- package/lib/src/create-app/git-manager.mjs +12 -2
- package/lib/src/create-app/json-manager.mjs +21 -18
- package/lib/src/create-app/new-app-prompt.mjs +4 -2
- package/lib/src/create-app/success-message.mjs +1 -1
- package/lib/src/update-app/conflict-report-writer.mjs +33 -21
- package/lib/src/update-app/json-drift.mjs +54 -0
- package/lib/src/update-app/project-updater.mjs +123 -14
- package/lib/src/update-app/template-differ.mjs +5 -5
- package/lib/src/update-app/three-way-merge.mjs +42 -0
- package/lib/src/update-app/update-context-builder.mjs +1 -0
- package/lib/src/update-app/update-summary-printer.mjs +10 -2
- package/package.json +12 -11
- package/templates/base/.gitignore.hbs +1 -0
- package/templates/base/AGENTS.md.hbs +45 -43
- package/templates/base/GETTING_STARTED.md.hbs +32 -23
- package/templates/base/README.md.hbs +7 -4
- package/templates/base/nutin.config.js.hbs +1 -1
- package/templates/base/src/app/components/.gitkeep +0 -0
- package/templates/base/src/app/globals.d.ts.hbs +6 -3
- package/templates/base/src/app/guards.ts.hbs +1 -3
- package/templates/base/src/app/main.ts.hbs +2 -9
- package/templates/base/src/app/routes.ts.hbs +9 -3
- package/templates/base/src/app/services/.gitkeep +0 -0
- package/templates/base/src/app/views/home/home.view.html.hbs +1 -1
- package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
- package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +2 -2
- package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +461 -1
- package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +39 -48
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +36 -1
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +47 -77
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +307 -21
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +144 -26
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +105 -4
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +136 -62
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +85 -4
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +101 -51
- package/templates/base/src/core/base-classes/base-component/helpers/html.helper.test.js.hbs +404 -0
- package/templates/base/src/core/base-classes/base-component/helpers/html.helper.ts.hbs +242 -0
- package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +70 -1
- package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +54 -15
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +12 -1
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +39 -36
- package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.test.js.hbs +179 -0
- package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.ts.hbs +93 -0
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +135 -17
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +199 -81
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +40 -23
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +67 -65
- package/templates/base/src/core/base-classes/component/component.test.js.hbs +17 -17
- package/templates/base/src/core/base-classes/component/component.ts.hbs +8 -41
- package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +1 -1
- package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +37 -39
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +14 -1
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +33 -31
- package/templates/base/src/core/base-classes/service/service.test.js.hbs +32 -0
- package/templates/base/src/core/base-classes/service/service.ts.hbs +21 -13
- package/templates/base/src/core/base-classes/view/view.test.js.hbs +4 -0
- package/templates/base/src/core/base-classes/view/view.ts.hbs +11 -8
- package/templates/base/src/core/index.ts.hbs +4 -2
- package/templates/base/src/core/internals.d.ts.hbs +199 -20
- package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +18 -0
- package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +26 -25
- package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +3 -3
- package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +3 -3
- package/templates/base/src/core/services/globals/globals.ts.hbs +7 -32
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +133 -50
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +147 -39
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +75 -19
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +73 -47
- package/templates/base/src/core/services/http-client/http-client.test.js.hbs +339 -232
- package/templates/base/src/core/services/http-client/http-client.ts.hbs +37 -25
- package/templates/base/src/core/services/i18n/i18n.test.js.hbs +57 -16
- package/templates/base/src/core/services/i18n/i18n.ts.hbs +42 -42
- package/templates/base/src/core/services/i18n/languages.ts.hbs +0 -2
- package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +4 -6
- package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +10 -0
- package/templates/base/src/core/services/pipe-registry/pipes.ts.hbs +4 -2
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +217 -4
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +210 -65
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +26 -27
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +38 -60
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +4 -4
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +30 -30
- package/templates/base/src/core/services/router/router.test.js.hbs +321 -0
- package/templates/base/src/core/services/router/router.ts.hbs +122 -54
- package/templates/base/src/index.html.hbs +1 -0
- package/templates/base/src/styles/_styles.scss.hbs +1 -113
- package/templates/base/src/styles/main.scss.hbs +1 -1
- package/templates/base/tools/builder/builder.config.js.hbs +6 -0
- package/templates/base/tools/builder/builder.js.hbs +26 -8
- package/templates/base/tools/builder/core/app/compile-ts.js.hbs +81 -6
- package/templates/base/tools/builder/core/app/copy-static.js.hbs +12 -1
- package/templates/base/tools/builder/core/app/handler-refs.js.hbs +114 -0
- package/templates/base/tools/builder/core/app/handler-refs.test.js.hbs +70 -0
- package/templates/base/tools/builder/core/app/paths.js.hbs +17 -0
- package/templates/base/tools/builder/core/app/route-keys.js.hbs +47 -0
- package/templates/base/tools/builder/core/app/route-keys.test.js.hbs +97 -0
- package/templates/base/tools/builder/core/app/ts-errors.js.hbs +58 -0
- package/templates/base/tools/builder/core/app/ts-errors.test.js.hbs +84 -0
- package/templates/base/tools/builder/core/app/validate-routes.js.hbs +21 -55
- package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -0
- package/templates/base/tools/builder/core/html-index/add-tags.js.hbs +1 -4
- package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +3 -3
- package/templates/base/tools/builder/core/html-index/validate-html.js.hbs +10 -2
- package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +111 -23
- package/templates/base/tools/builder/core/html-templates/minify-templates.js.hbs +8 -0
- package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +5 -3
- package/templates/base/tools/builder/core/html-templates/template-literal.js.hbs +193 -0
- package/templates/base/tools/builder/core/html-templates/template-literal.test.js.hbs +69 -0
- package/templates/base/tools/builder/core/i18n/build-i18n.js.hbs +14 -6
- package/templates/base/tools/builder/core/prod-bundle/bundle-options.js.hbs +82 -0
- package/templates/base/tools/builder/core/prod-bundle/bundle-options.test.js.hbs +65 -0
- package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +6 -6
- package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +25 -8
- package/templates/base/tools/builder/core/prod-bundle/hash-files.js.hbs +4 -4
- package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +31 -13
- package/templates/base/tools/builder/core/seo/generate-robots-txt.test.js.hbs +48 -0
- package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +3 -3
- package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +61 -15
- package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +22 -7
- package/templates/base/tools/builder/core/seo/generate-sitemap-xml.test.js.hbs +30 -0
- package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +48 -18
- package/templates/base/tools/builder/core/seo/html-substitutions.test.js.hbs +45 -0
- package/templates/base/tools/builder/core/seo/route-output.js.hbs +26 -8
- package/templates/base/tools/builder/core/seo/route-output.test.js.hbs +31 -0
- package/templates/base/tools/builder/core/seo/seo-routes.js.hbs +29 -0
- package/templates/base/tools/builder/core/seo/seo-routes.test.js.hbs +21 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +6 -7
- package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +34 -5
- package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.test.js.hbs +48 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +26 -9
- package/templates/base/tools/builder/core/styles/sass.js.hbs +7 -5
- package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
- package/templates/base/tools/dev/dev-serve.js.hbs +71 -7
- package/templates/base/tools/dev/serve.js.hbs +152 -22
- package/templates/base/tools/dev/watcher.js.hbs +75 -28
- package/templates/base/tools/generator/generator.js.hbs +23 -18
- package/templates/base/tools/generator/handle-file.js.hbs +4 -24
- package/templates/base/tools/generator/handle-file.test.js.hbs +4 -23
- package/templates/base/tools/generator/templates/component/component.template.js.hbs +3 -3
- package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +3 -3
- package/templates/base/tools/generator/templates/service/service.template.js.hbs +1 -1
- package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +1 -1
- package/templates/base/tools/generator/templates/view/view.template.js.hbs +2 -2
- package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
- package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +1 -1
- package/templates/base/tools/testin-nutin/core/globals/assertion-lib.js.hbs +2 -2
- package/templates/base/tools/testin-nutin/core/{tests/assertions.test.js.hbs → globals/assertion-lib.test.js.hbs} +6 -0
- package/templates/base/tools/testin-nutin/core/globals/register-test-globals.js.hbs +6 -7
- package/templates/base/tools/testin-nutin/core/{tests/globals-each-all.test.js.hbs → globals/register-test-globals.test.js.hbs} +12 -0
- package/templates/base/tools/testin-nutin/core/printer.js.hbs +10 -4
- package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +8 -8
- package/templates/base/tools/testin-nutin/mocks/mock-event-bus.js.hbs +27 -25
- package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +30 -22
- package/templates/base/tools/testin-nutin/mocks/mock-http-client.js.hbs +7 -10
- package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +12 -0
- package/templates/base/tools/testin-nutin/mocks/mock-i18n.js.hbs +47 -22
- package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +47 -26
- package/templates/base/tools/testin-nutin/mocks/mock-router.js.hbs +19 -11
- package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +24 -7
- package/templates/base/tools/testin-nutin/runner.js.hbs +12 -3
- package/templates/base/tools/testin-nutin/watch-tests.js.hbs +45 -20
- package/templates/base/tools/utils/build-lock.js.hbs +97 -0
- package/templates/base/tools/utils/ensure-deps.js.hbs +72 -0
- package/templates/base/tools/utils/ensure-deps.test.js.hbs +17 -0
- package/templates/base/tools/utils/format.js.hbs +1 -1
- package/templates/base/tools/utils/index.js.hbs +3 -1
- package/templates/base/tools/utils/print.js.hbs +2 -2
- package/templates/base/tools/utils/resolve-port.js.hbs +30 -0
- package/templates/base/tools/utils/resolve-port.test.js.hbs +47 -0
- package/templates/base/tools/utils/run-command.js.hbs +30 -1
- package/templates/base/tools/utils/run-command.test.js.hbs +12 -1
- package/templates/base/tools/utils/run-script.js.hbs +2 -2
- package/templates/features/docker/tools/docker/Dockerfile.template.hbs +5 -2
- package/templates/features/docker/tools/docker/nginx.conf.template +19 -2
- package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +1 -1
- package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +2 -2
- package/templates/features/docker/tools/docker/scripts/locale-redirects.js.hbs +28 -0
- package/templates/features/docker/tools/docker/scripts/locale-redirects.test.js.hbs +34 -0
- package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.js.hbs +25 -0
- package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.test.js.hbs +34 -0
- package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +3 -1
- package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +12 -0
- package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +29 -0
- package/templates/features/markdown/markdown-content/getting-started.md.hbs +28 -0
- package/templates/features/markdown/markdown-content/writing-pages.md.hbs +19 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.scss.hbs +75 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.test.js.hbs +40 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.ts.hbs +65 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.test.js.hbs +67 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.ts.hbs +87 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.scss.hbs +42 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.test.js.hbs +59 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.ts.hbs +72 -0
- package/templates/features/markdown/src/app/markdown/locales/en.json.hbs +6 -0
- package/templates/features/markdown/src/app/markdown/markdown-guards.test.js.hbs +63 -0
- package/templates/features/markdown/src/app/markdown/markdown-guards.ts.hbs +31 -0
- package/templates/features/markdown/src/app/markdown/markdown-i18n.test.js.hbs +78 -0
- package/templates/features/markdown/src/app/markdown/markdown-i18n.ts.hbs +31 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifest.test.js.hbs +98 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifest.ts.hbs +157 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifests.service.test.js.hbs +87 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifests.service.ts.hbs +50 -0
- package/templates/features/markdown/src/app/markdown/markdown-routes.test.js.hbs +74 -0
- package/templates/features/markdown/src/app/markdown/markdown-routes.ts.hbs +26 -0
- package/templates/features/markdown/src/app/markdown/markdown-sources.test.js.hbs +22 -0
- package/templates/features/markdown/src/app/markdown/markdown-sources.ts.hbs +42 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.html.hbs +4 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.scss.hbs +19 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.test.js.hbs +125 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.ts.hbs +146 -0
- package/templates/features/markdown/tools/builder/core/markdown/generate-markdown-pages.js.hbs +71 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.js.hbs +582 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.test.js.hbs +390 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.js.hbs +155 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.test.js.hbs +112 -0
- package/templates/base/src/app/components/index.ts.hbs +0 -1
- package/templates/base/src/app/services/index.ts.hbs +0 -1
- package/templates/base/src/app/views/index.ts.hbs +0 -3
- /package/templates/base/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
// Tokens resolve to raw values: escaping happens at output, in html`` templates.
|
|
2
|
+
function readInputValue(el: HTMLElement): string {
|
|
3
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) return el.value;
|
|
4
|
+
if (el.hasAttribute('contenteditable')) return el.innerText;
|
|
5
|
+
return '';
|
|
6
|
+
}
|
|
2
7
|
|
|
3
8
|
// Exact token resolvers
|
|
4
9
|
const EXACT_TOKEN_RESOLVERS: Record<string, (el: HTMLElement, ev: Event) => any> = {
|
|
5
|
-
'@id': (el) =>
|
|
6
|
-
'@class': (el) =>
|
|
7
|
-
'@name': (el) =>
|
|
8
|
-
'@tag': (el) =>
|
|
10
|
+
'@id': (el) => el.id,
|
|
11
|
+
'@class': (el) => el.className,
|
|
12
|
+
'@name': (el) => (el as any).name ?? '',
|
|
13
|
+
'@tag': (el) => el.tagName,
|
|
9
14
|
|
|
10
|
-
'@value': (el) =>
|
|
15
|
+
'@value': (el) => readInputValue(el),
|
|
11
16
|
'@checked': (el) => (el as HTMLInputElement).checked,
|
|
12
17
|
'@selected': (el) => (el as HTMLOptionElement).selected,
|
|
13
18
|
|
|
14
|
-
'@textContent': (el) =>
|
|
15
|
-
'@innerText': (el) =>
|
|
16
|
-
'@html': (el) =>
|
|
19
|
+
'@textContent': (el) => el.textContent ?? '',
|
|
20
|
+
'@innerText': (el) => el.innerText ?? '',
|
|
21
|
+
'@html': (el) => el.innerHTML ?? '',
|
|
17
22
|
|
|
18
23
|
'@event': (_el, ev) => ev,
|
|
19
24
|
'@target': (_el, ev) => ev.target,
|
|
@@ -21,79 +26,76 @@ const EXACT_TOKEN_RESOLVERS: Record<string, (el: HTMLElement, ev: Event) => any>
|
|
|
21
26
|
'@x': (_el, ev) => (ev as MouseEvent).clientX ?? 0,
|
|
22
27
|
'@y': (_el, ev) => (ev as MouseEvent).clientY ?? 0,
|
|
23
28
|
|
|
24
|
-
'@key': (_el, ev) =>
|
|
25
|
-
'@code': (_el, ev) =>
|
|
29
|
+
'@key': (_el, ev) => (ev as KeyboardEvent).key ?? '',
|
|
30
|
+
'@code': (_el, ev) => (ev as KeyboardEvent).code ?? '',
|
|
26
31
|
};
|
|
27
32
|
|
|
28
33
|
// Prefix-based token resolvers
|
|
29
34
|
const PREFIXED_TOKEN_RESOLVERS: Record<string, (suffix: string, el: HTMLElement) => any> = {
|
|
30
|
-
'@attr:': (suffix, el) =>
|
|
31
|
-
'@dataset:': (suffix, el) =>
|
|
35
|
+
'@attr:': (suffix, el) => el.getAttribute(suffix) ?? '',
|
|
36
|
+
'@dataset:': (suffix, el) => (el.dataset as any)[suffix] ?? '',
|
|
32
37
|
};
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
private static customResolvers: Record<string, (el: HTMLElement, ev: Event) => any> = {};
|
|
39
|
+
const customResolvers: Record<string, (el: HTMLElement, ev: Event) => any> = {};
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
event.preventDefault();
|
|
41
|
+
export function resolve(token: string, el: Element, event: Event): any {
|
|
42
|
+
const htmlEl = el as HTMLElement;
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
return (
|
|
45
|
+
resolveExact(token, htmlEl, event) ??
|
|
46
|
+
resolvePrefixed(token, htmlEl) ??
|
|
47
|
+
resolveCustom(token, htmlEl, event) ??
|
|
48
|
+
resolveLiteral(token) ??
|
|
49
|
+
token
|
|
50
|
+
);
|
|
51
|
+
}
|
|
49
52
|
|
|
50
53
|
/** Register a new exact token like "@foo" */
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
export function registerCustomToken(
|
|
55
|
+
name: string,
|
|
56
|
+
resolver: (el: HTMLElement, ev: Event) => any
|
|
57
|
+
): void {
|
|
58
|
+
customResolvers[name] = resolver;
|
|
59
|
+
}
|
|
57
60
|
|
|
58
61
|
/** Register a new prefixed token like "@style:" */
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
export function registerPrefixedToken(
|
|
63
|
+
prefix: string,
|
|
64
|
+
resolver: (suffix: string, el: HTMLElement) => any
|
|
65
|
+
): void {
|
|
66
|
+
PREFIXED_TOKEN_RESOLVERS[prefix] = resolver;
|
|
67
|
+
}
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
function resolveExact(token: string, el: HTMLElement, ev: Event): any | null {
|
|
70
|
+
const resolver = EXACT_TOKEN_RESOLVERS[token];
|
|
71
|
+
return resolver ? resolver(el, ev) : null;
|
|
72
|
+
}
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
74
|
+
function resolvePrefixed(token: string, el: HTMLElement): any | null {
|
|
75
|
+
for (const [prefix, resolver] of Object.entries(PREFIXED_TOKEN_RESOLVERS)) {
|
|
76
|
+
if (token.startsWith(prefix)) {
|
|
77
|
+
return resolver(token.slice(prefix.length), el);
|
|
76
78
|
}
|
|
77
|
-
return null;
|
|
78
79
|
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
function resolveCustom(token: string, el: HTMLElement, ev: Event): any | null {
|
|
84
|
+
const resolver = customResolvers[token];
|
|
85
|
+
return resolver ? resolver(el, ev) : null;
|
|
86
|
+
}
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
88
|
+
function resolveLiteral(token: string): any | null {
|
|
89
|
+
// string literal
|
|
90
|
+
if (
|
|
91
|
+
(token.startsWith('"') && token.endsWith('"')) ||
|
|
92
|
+
(token.startsWith("'") && token.endsWith("'"))
|
|
93
|
+
) {
|
|
94
|
+
return token.slice(1, -1);
|
|
95
|
+
}
|
|
96
|
+
// number literal
|
|
97
|
+
if (!isNaN(Number(token))) {
|
|
98
|
+
return Number(token);
|
|
98
99
|
}
|
|
100
|
+
return null;
|
|
99
101
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Component } from '#root/dist/src/core/index.js';
|
|
2
|
-
import { DataBindingHelper } from '#root/dist/src/core/base-classes/component/helpers/data-binding.helper.js';
|
|
3
2
|
|
|
4
3
|
class TestComponent extends Component {
|
|
5
4
|
constructor(options = {}) {
|
|
@@ -44,6 +43,13 @@ describe('Component', () => {
|
|
|
44
43
|
expect(component.getElement().classList.contains('highlight')).toBe(true);
|
|
45
44
|
});
|
|
46
45
|
|
|
46
|
+
it('onBeforeRender applies a space-separated props.className as several classes', () => {
|
|
47
|
+
const component = new TestComponent({ props: { className: ' btn primary ' } });
|
|
48
|
+
component.render();
|
|
49
|
+
expect(component.getElement().classList.contains('btn')).toBe(true);
|
|
50
|
+
expect(component.getElement().classList.contains('primary')).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
47
53
|
it('onBeforeRender applies props.style as cssText on render', () => {
|
|
48
54
|
const component = new TestComponent({ props: { style: 'color: red;' } });
|
|
49
55
|
component.render();
|
|
@@ -61,25 +67,19 @@ describe('Component', () => {
|
|
|
61
67
|
expect(component.getElement().querySelector('span').textContent).toBe('Ada');
|
|
62
68
|
});
|
|
63
69
|
|
|
64
|
-
it('
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const component = new TestComponent({
|
|
70
|
+
it('applies data-bindings in onAfterRender, so a subclass sees them once it calls super', () => {
|
|
71
|
+
let seen = null;
|
|
72
|
+
class BoundComponent extends Component {
|
|
73
|
+
onAfterRender() {
|
|
74
|
+
super.onAfterRender();
|
|
75
|
+
seen = this.getElement().querySelector('span').textContent;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const component = new BoundComponent({
|
|
74
79
|
templateFn: () => '<span data-bind="name"></span>',
|
|
75
80
|
props: { name: 'Ada' },
|
|
76
81
|
});
|
|
77
82
|
component.render();
|
|
78
|
-
|
|
79
|
-
expect(calls.length).toBe(1);
|
|
80
|
-
expect(calls[0].props).toEqual({ name: 'Ada' });
|
|
81
|
-
expect(calls[0].htmlAtCallTime).toBe('<span data-bind="name"></span>');
|
|
82
|
-
|
|
83
|
-
spy.restore();
|
|
83
|
+
expect(seen).toBe('Ada');
|
|
84
84
|
});
|
|
85
85
|
});
|
|
@@ -1,44 +1,11 @@
|
|
|
1
|
-
import { BaseComponent
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { BaseComponent } from '../base-component/base-component.js';
|
|
2
|
+
import * as ConfigHelper from './helpers/config.helper.js';
|
|
3
|
+
import * as DataBindingHelper from './helpers/data-binding.helper.js';
|
|
4
4
|
|
|
5
|
-
export interface ComponentProps {
|
|
6
|
-
// Common HTML attributes - extend as needed
|
|
7
|
-
className?: string;
|
|
8
|
-
style?: string;
|
|
9
|
-
textContent?: string;
|
|
10
|
-
|
|
11
|
-
// Form field bindings - extend as needed
|
|
12
|
-
name?: string;
|
|
13
|
-
email?: string;
|
|
14
|
-
|
|
15
|
-
// Allow for additional data-bind attributes
|
|
16
|
-
[key: string]: any;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ComponentOptions<K = any> extends BaseComponentOptions {
|
|
20
|
-
props?: ComponentProps;
|
|
21
|
-
config?: K;
|
|
22
|
-
defaults?: Partial<K>;
|
|
23
|
-
templateFn?: (config?: K) => string;
|
|
24
|
-
normalizeKeys?: (keyof K)[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* ```typescript
|
|
29
|
-
interface ComponentOptions<K = any> extends BaseComponentOptions {
|
|
30
|
-
props?: ComponentProps;
|
|
31
|
-
config?: K;
|
|
32
|
-
defaults?: Partial<K>;
|
|
33
|
-
templateFn?: (config?: K) => string;
|
|
34
|
-
normalizeKeys?: (keyof K)[];
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
*/
|
|
38
5
|
export abstract class Component<T extends HTMLElement = HTMLElement, K = any> extends BaseComponent<T> {
|
|
39
6
|
protected config: K;
|
|
40
|
-
protected props: ComponentProps;
|
|
41
|
-
private _templateFn: (config?: K) =>
|
|
7
|
+
protected props: Nutin.ComponentProps;
|
|
8
|
+
private _templateFn: (config?: K) => Nutin.Template;
|
|
42
9
|
private _normalizeKeys: (keyof K)[];
|
|
43
10
|
private _defaults: Partial<K>;
|
|
44
11
|
|
|
@@ -51,7 +18,7 @@ export abstract class Component<T extends HTMLElement = HTMLElement, K = any> ex
|
|
|
51
18
|
normalizeKeys = [],
|
|
52
19
|
defaults = {},
|
|
53
20
|
trustLevel,
|
|
54
|
-
}: ComponentOptions) {
|
|
21
|
+
}: Nutin.ComponentOptions) {
|
|
55
22
|
super({ mountTarget, tagName, trustLevel });
|
|
56
23
|
|
|
57
24
|
this.config = ConfigHelper.setConfigValue(config, normalizeKeys);
|
|
@@ -65,7 +32,7 @@ export abstract class Component<T extends HTMLElement = HTMLElement, K = any> ex
|
|
|
65
32
|
return DataBindingHelper.getDataBindingValues(this.element);
|
|
66
33
|
}
|
|
67
34
|
|
|
68
|
-
protected override generateTemplate():
|
|
35
|
+
protected override generateTemplate(): Nutin.Template {
|
|
69
36
|
return ConfigHelper.createNormalizedTemplate({
|
|
70
37
|
config: this.config,
|
|
71
38
|
defaults: this._defaults,
|
|
@@ -84,7 +51,7 @@ export abstract class Component<T extends HTMLElement = HTMLElement, K = any> ex
|
|
|
84
51
|
|
|
85
52
|
private applyProps(): void {
|
|
86
53
|
if (this.props.className) {
|
|
87
|
-
this.element.classList.add(this.props.className);
|
|
54
|
+
this.element.classList.add(...this.props.className.split(/\s+/).filter(Boolean));
|
|
88
55
|
}
|
|
89
56
|
if (this.props.style) {
|
|
90
57
|
this.element.style.cssText = this.props.style;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as ConfigHelper from '#root/dist/src/core/base-classes/component/helpers/config.helper.js';
|
|
2
2
|
|
|
3
3
|
describe('ConfigHelper', () => {
|
|
4
4
|
it('normalize shallow-merges defaults under config, letting config win', () => {
|
|
@@ -1,43 +1,41 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
1
|
+
export function normalize<T extends Record<string, any>>(
|
|
2
|
+
config: T,
|
|
3
|
+
defaults: Partial<T>
|
|
4
|
+
): T {
|
|
5
|
+
return {
|
|
6
|
+
...defaults,
|
|
7
|
+
...config
|
|
8
|
+
} as T;
|
|
9
|
+
}
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return normalized;
|
|
25
|
-
}
|
|
11
|
+
export function normalizeStrings<T extends Record<string, any>>(
|
|
12
|
+
config: T,
|
|
13
|
+
stringKeys: (keyof T)[]
|
|
14
|
+
): T {
|
|
15
|
+
const normalized = { ...config };
|
|
16
|
+
|
|
17
|
+
stringKeys.forEach(key => {
|
|
18
|
+
if (normalized[key] === undefined || normalized[key] === null) {
|
|
19
|
+
normalized[key] = '' as T[keyof T];
|
|
20
|
+
}
|
|
21
|
+
});
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createNormalizedTemplate<T extends Record<string, any>>({
|
|
27
|
+
config,
|
|
28
|
+
defaults = {},
|
|
29
|
+
normalizeKeys = [],
|
|
30
|
+
templateFn = () => ''
|
|
31
|
+
}: T) {
|
|
32
|
+
let normalizedConfig = normalize(config, defaults);
|
|
33
|
+
normalizedConfig = normalizeStrings(normalizedConfig, normalizeKeys);
|
|
34
|
+
return templateFn(normalizedConfig);
|
|
35
|
+
}
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
37
|
+
export function setConfigValue<T extends Record<string, any>>(config: T, normalizeKeys: (keyof T)[]): T {
|
|
38
|
+
if (normalizeKeys) return normalizeStrings(config, normalizeKeys) as T;
|
|
39
|
+
else if (config) return config as T
|
|
40
|
+
else return ({} as T);
|
|
43
41
|
}
|
package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as DataBindingHelper from '#root/dist/src/core/base-classes/component/helpers/data-binding.helper.js';
|
|
2
|
+
import * as DomHelper from '#root/dist/src/core/base-classes/base-component/helpers/dom.helper.js';
|
|
2
3
|
|
|
3
4
|
describe('DataBindingHelper', () => {
|
|
4
5
|
let container;
|
|
@@ -72,4 +73,16 @@ describe('DataBindingHelper', () => {
|
|
|
72
73
|
const values = DataBindingHelper.getDataBindingValues(container);
|
|
73
74
|
expect(values).toEqual({});
|
|
74
75
|
});
|
|
76
|
+
|
|
77
|
+
it('skips fields inside a nested component root in both directions', () => {
|
|
78
|
+
container.innerHTML = '<input class="own" data-bind="name"><div class="child"><input class="nested" data-bind="name" value="child"></div>';
|
|
79
|
+
DomHelper.markComponentRoot(container.querySelector('.child'));
|
|
80
|
+
|
|
81
|
+
DataBindingHelper.applyDataBindings(container, { name: 'Ada' });
|
|
82
|
+
|
|
83
|
+
expect(container.querySelector('.own').value).toBe('Ada');
|
|
84
|
+
expect(container.querySelector('.nested').value).toBe('child');
|
|
85
|
+
container.querySelector('.own').value = 'parent';
|
|
86
|
+
expect(DataBindingHelper.getDataBindingValues(container)).toEqual({ name: 'parent' });
|
|
87
|
+
});
|
|
75
88
|
});
|
|
@@ -1,35 +1,37 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as DomHelper from '../../base-component/helpers/dom.helper.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
// A nested component's data-bind fields belong to that component: they are neither
|
|
4
|
+
// filled from this one's props nor returned by its getValues().
|
|
5
|
+
export function applyDataBindings(element: HTMLElement, props: Nutin.ComponentProps): void {
|
|
6
|
+
const bindEls = element.querySelectorAll('[data-bind]');
|
|
7
|
+
bindEls.forEach(el => {
|
|
8
|
+
if (DomHelper.isInsideNestedComponent(el, element)) return;
|
|
9
|
+
const key = el.getAttribute('data-bind');
|
|
10
|
+
if (!key) return;
|
|
11
|
+
|
|
12
|
+
const value = props[key];
|
|
13
|
+
if (value === undefined) return;
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
16
|
+
el.value = String(value);
|
|
17
|
+
} else {
|
|
18
|
+
el.textContent = String(value);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
export function getDataBindingValues(element: HTMLElement): Record<string, string> {
|
|
24
|
+
const values: Record<string, string> = {};
|
|
25
|
+
element.querySelectorAll('[data-bind]').forEach(el => {
|
|
26
|
+
if (DomHelper.isInsideNestedComponent(el, element)) return;
|
|
27
|
+
const key = el.getAttribute('data-bind');
|
|
28
|
+
if (!key) return;
|
|
29
|
+
|
|
30
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
31
|
+
values[key] = el.value;
|
|
32
|
+
} else {
|
|
33
|
+
values[key] = el.textContent || '';
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return values;
|
|
35
37
|
}
|
|
@@ -99,6 +99,38 @@ describe('Service', () => {
|
|
|
99
99
|
expect(Service.hasInstance(TestService)).toBe(false);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
it('destroy runs registerCleanup callbacks once', async () => {
|
|
103
|
+
const instance = TestService.getInstance();
|
|
104
|
+
let calls = 0;
|
|
105
|
+
instance.addCleanup(() => { calls++; });
|
|
106
|
+
|
|
107
|
+
await TestService.destroy();
|
|
108
|
+
instance.dispose();
|
|
109
|
+
|
|
110
|
+
expect(calls).toBe(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('destroyAll runs every instance\'s registerCleanup callbacks', async () => {
|
|
114
|
+
const instance = TestService.getInstance();
|
|
115
|
+
let cleaned = false;
|
|
116
|
+
instance.addCleanup(() => { cleaned = true; });
|
|
117
|
+
|
|
118
|
+
await Service.destroyAll();
|
|
119
|
+
|
|
120
|
+
expect(cleaned).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('does not tear anything down on page unload', () => {
|
|
124
|
+
const instance = TestService.getInstance();
|
|
125
|
+
let cleaned = false;
|
|
126
|
+
instance.addCleanup(() => { cleaned = true; });
|
|
127
|
+
|
|
128
|
+
window.dispatchEvent(new window.Event('beforeunload'));
|
|
129
|
+
|
|
130
|
+
expect(cleaned).toBe(false);
|
|
131
|
+
expect(Service.hasInstance(TestService)).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
102
134
|
it('destroy is a no-op when no instance exists', async () => {
|
|
103
135
|
expect(Service.hasInstance(TestService)).toBe(false);
|
|
104
136
|
await TestService.destroy();
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* Supports constructor parameters for initialization.
|
|
4
4
|
*/
|
|
5
5
|
export abstract class Service<T extends Service<T>> {
|
|
6
|
-
private static _instances = new Map<Function, any
|
|
6
|
+
private static _instances = new Map<Function, Service<any>>();
|
|
7
7
|
private static _instantiating = new Set<Function>();
|
|
8
8
|
private _cleanupCallbacks: (() => void)[] = [];
|
|
9
9
|
|
|
10
|
-
constructor(
|
|
10
|
+
constructor() {
|
|
11
11
|
const constructor = this.constructor as new (...args: any[]) => T;
|
|
12
12
|
|
|
13
13
|
if (!Service._instantiating.has(constructor)) {
|
|
@@ -16,7 +16,6 @@ export abstract class Service<T extends Service<T>> {
|
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
this.autoBindMethods();
|
|
19
|
-
window.addEventListener('beforeunload', this.dispose);
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
/**
|
|
@@ -24,10 +23,10 @@ export abstract class Service<T extends Service<T>> {
|
|
|
24
23
|
* doesn't exist yet. Throws if args are passed to an already-initialized instance,
|
|
25
24
|
* preventing silent re-initialization mismatches.
|
|
26
25
|
*/
|
|
27
|
-
public static getInstance<
|
|
28
|
-
this:
|
|
29
|
-
...args:
|
|
30
|
-
):
|
|
26
|
+
public static getInstance<C extends new (...args: any) => Service<any>>(
|
|
27
|
+
this: C,
|
|
28
|
+
...args: ConstructorParameters<C> | []
|
|
29
|
+
): InstanceType<C> {
|
|
31
30
|
const alreadyExists = Service._instances.has(this);
|
|
32
31
|
|
|
33
32
|
if (alreadyExists && args.length > 0) {
|
|
@@ -45,7 +44,7 @@ export abstract class Service<T extends Service<T>> {
|
|
|
45
44
|
Service._instantiating.delete(this);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
return Service._instances.get(this)
|
|
47
|
+
return Service._instances.get(this) as InstanceType<C>;
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
public static hasInstance<T extends Service<T>>(
|
|
@@ -60,21 +59,24 @@ export abstract class Service<T extends Service<T>> {
|
|
|
60
59
|
const instance = Service._instances.get(this);
|
|
61
60
|
if (instance) {
|
|
62
61
|
await instance.onDestroy();
|
|
62
|
+
instance.runCleanup();
|
|
63
63
|
Service._instances.delete(this);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// No page-unload hook: the browser frees everything on unload, and tearing down on
|
|
68
|
+
// `beforeunload` left a page restored from the back/forward cache (or kept after a
|
|
69
|
+
// cancelled "Leave site?" prompt) with every service already disposed.
|
|
67
70
|
public dispose = (): void => {
|
|
68
|
-
this.
|
|
69
|
-
this._cleanupCallbacks = [];
|
|
71
|
+
this.runCleanup();
|
|
70
72
|
Service._instances.delete(this.constructor);
|
|
71
73
|
};
|
|
72
74
|
|
|
73
75
|
public static async destroyAll(): Promise<void> {
|
|
74
|
-
const
|
|
75
|
-
.map(instance => instance.onDestroy());
|
|
76
|
+
const instances = Array.from(Service._instances.values());
|
|
76
77
|
|
|
77
|
-
await Promise.all(
|
|
78
|
+
await Promise.all(instances.map(instance => instance.onDestroy()));
|
|
79
|
+
instances.forEach(instance => instance.runCleanup());
|
|
78
80
|
Service._instances.clear();
|
|
79
81
|
Service._instantiating.clear();
|
|
80
82
|
}
|
|
@@ -83,6 +85,12 @@ export abstract class Service<T extends Service<T>> {
|
|
|
83
85
|
this._cleanupCallbacks.push(callback);
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
private runCleanup(): void {
|
|
89
|
+
const callbacks = this._cleanupCallbacks;
|
|
90
|
+
this._cleanupCallbacks = [];
|
|
91
|
+
callbacks.forEach(fn => fn());
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
/**
|
|
87
95
|
* For async and custom cleanup operations.
|
|
88
96
|
*/
|
|
@@ -23,6 +23,10 @@ describe('View', () => {
|
|
|
23
23
|
expect(() => new HomeView({ viewName: '' })).toThrow();
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
it('has no document title of its own by default', () => {
|
|
27
|
+
expect(new NamedView({ viewName: 'custom-name' }).documentTitle()).toBe(undefined);
|
|
28
|
+
});
|
|
29
|
+
|
|
26
30
|
it('uses the explicit viewName option', () => {
|
|
27
31
|
const view = new NamedView({ viewName: 'custom-name' });
|
|
28
32
|
expect(view.viewName).toBe('custom-name');
|