@nutin/cli 2.1.1 → 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 +268 -0
- package/README.md +13 -5
- 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 +9 -8
- package/templates/base/AGENTS.md.hbs +42 -41
- package/templates/base/GETTING_STARTED.md.hbs +26 -20
- package/templates/base/README.md.hbs +7 -4
- 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 +7 -2
- 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 +286 -24
- 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 +66 -4
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +135 -75
- 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 +93 -54
- 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 +183 -4
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +207 -84
- 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 +306 -0
- package/templates/base/src/core/services/router/router.ts.hbs +113 -53
- package/templates/base/tools/builder/builder.config.js.hbs +6 -0
- package/templates/base/tools/builder/builder.js.hbs +18 -6
- 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/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 +15 -7
- 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 +43 -13
- 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 +28 -21
- 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 +24 -6
- 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 +24 -11
- package/templates/base/tools/builder/core/styles/sass.js.hbs +3 -2
- package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
- package/templates/base/tools/dev/dev-serve.js.hbs +30 -5
- package/templates/base/tools/dev/serve.js.hbs +152 -22
- package/templates/base/tools/dev/watcher.js.hbs +34 -3
- package/templates/base/tools/generator/generator.js.hbs +1 -1
- package/templates/base/tools/generator/handle-file.js.hbs +4 -2
- package/templates/base/tools/generator/handle-file.test.js.hbs +3 -3
- 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 +30 -2
- 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 +2 -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 +3 -0
- package/templates/features/docker/tools/docker/nginx.conf.template +6 -4
- 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 +21 -4
- 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/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
|
@@ -1,92 +1,62 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { KeepableComponentConfig } from './children.helper.js';
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
items
|
|
5
|
-
elementName: string;
|
|
6
|
-
elementTag?: keyof HTMLElementTagNameMap;
|
|
7
|
-
selector: string;
|
|
8
|
-
component: new (el: HTMLElement, data: any, props?: any) => Component;
|
|
9
|
-
};
|
|
3
|
+
export function generateCatalog(config: Nutin.CatalogConfig, scope: Element = document.documentElement): Nutin.ComponentConfig[] {
|
|
4
|
+
if (!config.items || config.items.length < 1) return [];
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
6
|
+
const componentConfigs: Nutin.ComponentConfig[] = [];
|
|
7
|
+
const containers = scope.querySelectorAll(`[data-catalog="${config.selector}"]`);
|
|
14
8
|
|
|
15
|
-
|
|
9
|
+
containers.forEach((container) => {
|
|
10
|
+
if (!container || !(container instanceof HTMLElement)) return;
|
|
11
|
+
componentConfigs.push(...getComponentConfigItems(config, container))
|
|
12
|
+
})
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
value: string | number | boolean | null | undefined;
|
|
14
|
+
return componentConfigs;
|
|
19
15
|
}
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
items: CatalogItemConfig[];
|
|
28
|
-
elementName: string;
|
|
29
|
-
elementTag?: keyof HTMLElementTagNameMap;
|
|
30
|
-
selector: string;
|
|
31
|
-
component: new (el: HTMLElement, data: any) => Component;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type CatalogItemConfig<T = any> =
|
|
35
|
-
T extends object ? CatalogItemObject<T> : CatalogItemPrimitive;
|
|
36
|
-
```
|
|
37
|
-
*/
|
|
38
|
-
export class CatalogHelper {
|
|
39
|
-
public static generateCatalog(config: CatalogConfig, scope: Element = document.documentElement): ComponentConfig[] {
|
|
40
|
-
if (!config.items || config.items.length < 1) return [];
|
|
41
|
-
|
|
42
|
-
const componentConfigs: ComponentConfig[] = [];
|
|
43
|
-
const containers = scope.querySelectorAll(`[data-catalog="${config.selector}"]`);
|
|
44
|
-
|
|
45
|
-
containers.forEach((container) => {
|
|
46
|
-
if (!container || !(container instanceof HTMLElement)) return;
|
|
47
|
-
componentConfigs.push(...this.getComponentConfigItems(config, container))
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
return componentConfigs;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private static getComponentConfigItems(config: CatalogConfig, container: HTMLElement): ComponentConfig[] {
|
|
54
|
-
container.innerHTML = '';
|
|
55
|
-
const componentConfigs: ComponentConfig[] = [];
|
|
56
|
-
for (let i = 0; i < config.items.length; i++) {
|
|
57
|
-
this.createElements(i, config, container);
|
|
58
|
-
this.pushConfig(i, componentConfigs, config);
|
|
59
|
-
}
|
|
60
|
-
return componentConfigs;
|
|
17
|
+
function getComponentConfigItems(config: Nutin.CatalogConfig, container: HTMLElement): Nutin.ComponentConfig[] {
|
|
18
|
+
container.innerHTML = '';
|
|
19
|
+
const componentConfigs: Nutin.ComponentConfig[] = [];
|
|
20
|
+
for (let i = 0; i < config.items.length; i++) {
|
|
21
|
+
createElements(i, config, container);
|
|
22
|
+
pushConfig(i, componentConfigs, config);
|
|
61
23
|
}
|
|
24
|
+
return componentConfigs;
|
|
25
|
+
}
|
|
62
26
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
27
|
+
function createElements(index: number, config: Nutin.CatalogConfig, container: HTMLElement): void {
|
|
28
|
+
const wrapper = document.createElement(config.elementTag || 'div');
|
|
29
|
+
const el = document.createElement('div');
|
|
30
|
+
wrapper.appendChild(el);
|
|
31
|
+
el.setAttribute('data-component', `${config.elementName}-${index}`);
|
|
32
|
+
wrapper.dataset.index = String(index);
|
|
33
|
+
container?.appendChild(wrapper);
|
|
34
|
+
}
|
|
71
35
|
|
|
72
|
-
|
|
73
|
-
|
|
36
|
+
function pushConfig(index: number, componentConfigs: Nutin.ComponentConfig[], config: Nutin.CatalogConfig): void {
|
|
37
|
+
const configWithIndex = getConfigWithIndex(config, index)
|
|
74
38
|
|
|
75
|
-
|
|
76
|
-
|
|
39
|
+
const { props, defaults } = config;
|
|
40
|
+
const options = { ...props, ...defaults };
|
|
77
41
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
},
|
|
83
|
-
)
|
|
84
|
-
}
|
|
42
|
+
const componentConfig: KeepableComponentConfig = {
|
|
43
|
+
selector: `${config.elementName}-${index}`,
|
|
44
|
+
factory: (el) => new config.component(el, configWithIndex, options),
|
|
45
|
+
};
|
|
85
46
|
|
|
86
|
-
|
|
47
|
+
if (config.keepBy) {
|
|
87
48
|
const item = config.items[index];
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
49
|
+
componentConfig.keep = config.keepBy(item, index);
|
|
50
|
+
componentConfig.scope = `catalog:${config.selector}:${config.elementName}`;
|
|
51
|
+
componentConfig.keepIf = { item, options, component: config.component };
|
|
91
52
|
}
|
|
53
|
+
|
|
54
|
+
componentConfigs.push(componentConfig);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getConfigWithIndex(config: Nutin.CatalogConfig, index: number): Nutin.CatalogItemConfig {
|
|
58
|
+
const item = config.items[index];
|
|
59
|
+
return (item && typeof item === 'object')
|
|
60
|
+
? { ...(item as object), index: index }
|
|
61
|
+
: { value: item, index: index };
|
|
92
62
|
}
|
package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as ChildrenHelper from '#root/dist/src/core/base-classes/base-component/helpers/children.helper.js';
|
|
2
2
|
import { BaseComponent } from '#root/dist/src/core/index.js';
|
|
3
3
|
|
|
4
|
+
class Widget extends BaseComponent {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
super(options);
|
|
7
|
+
this.renders = 0;
|
|
8
|
+
this.destroyed = false;
|
|
9
|
+
}
|
|
10
|
+
generateTemplate() { return '<p>widget</p>'; }
|
|
11
|
+
render() {
|
|
12
|
+
this.renders++;
|
|
13
|
+
return super.render();
|
|
14
|
+
}
|
|
15
|
+
destroy() {
|
|
16
|
+
this.destroyed = true;
|
|
17
|
+
this.onDestroy?.();
|
|
18
|
+
super.destroy();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
4
22
|
describe('ChildrenHelper', () => {
|
|
5
23
|
let element;
|
|
6
24
|
|
|
@@ -32,13 +50,13 @@ describe('ChildrenHelper', () => {
|
|
|
32
50
|
}],
|
|
33
51
|
};
|
|
34
52
|
|
|
35
|
-
const
|
|
36
|
-
ChildrenHelper.addChildren(component, element,
|
|
53
|
+
const registry = ChildrenHelper.createRegistry();
|
|
54
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
37
55
|
|
|
38
56
|
expect(factoryCalls.length).toBe(1);
|
|
39
57
|
expect(factoryCalls[0]).toBe(element.querySelector('[data-component="widget"]'));
|
|
40
58
|
expect(rendered.length).toBe(1);
|
|
41
|
-
expect(
|
|
59
|
+
expect(registry.rebuilt.length).toBe(1);
|
|
42
60
|
});
|
|
43
61
|
|
|
44
62
|
it('instantiates one child per matched element for a single registerChildren entry', () => {
|
|
@@ -50,10 +68,10 @@ describe('ChildrenHelper', () => {
|
|
|
50
68
|
}],
|
|
51
69
|
};
|
|
52
70
|
|
|
53
|
-
const
|
|
54
|
-
ChildrenHelper.addChildren(component, element,
|
|
71
|
+
const registry = ChildrenHelper.createRegistry();
|
|
72
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
55
73
|
|
|
56
|
-
expect(
|
|
74
|
+
expect(registry.rebuilt.length).toBe(2);
|
|
57
75
|
});
|
|
58
76
|
|
|
59
77
|
it('keeps the placeholder attributes (except data-component) on the mounted child element', () => {
|
|
@@ -68,10 +86,10 @@ describe('ChildrenHelper', () => {
|
|
|
68
86
|
}],
|
|
69
87
|
};
|
|
70
88
|
|
|
71
|
-
const
|
|
72
|
-
ChildrenHelper.addChildren(component, element,
|
|
89
|
+
const registry = ChildrenHelper.createRegistry();
|
|
90
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
73
91
|
|
|
74
|
-
const childEl =
|
|
92
|
+
const childEl = registry.rebuilt[0].getElement();
|
|
75
93
|
expect(element.contains(childEl)).toBe(true);
|
|
76
94
|
expect(childEl.classList.contains('my-class')).toBe(true);
|
|
77
95
|
expect(childEl.id).toBe('my-widget');
|
|
@@ -81,9 +99,10 @@ describe('ChildrenHelper', () => {
|
|
|
81
99
|
|
|
82
100
|
it('does nothing when there are no registerChildren', () => {
|
|
83
101
|
const component = { registerChildren: () => [] };
|
|
84
|
-
const
|
|
85
|
-
ChildrenHelper.addChildren(component, element,
|
|
86
|
-
expect(
|
|
102
|
+
const registry = ChildrenHelper.createRegistry();
|
|
103
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
104
|
+
expect(registry.rebuilt.length).toBe(0);
|
|
105
|
+
expect(registry.keepable.size).toBe(0);
|
|
87
106
|
});
|
|
88
107
|
|
|
89
108
|
it('destroys previously-tracked children and resets the array before mounting new ones on a second call', () => {
|
|
@@ -96,25 +115,268 @@ describe('ChildrenHelper', () => {
|
|
|
96
115
|
}],
|
|
97
116
|
};
|
|
98
117
|
|
|
99
|
-
const
|
|
100
|
-
ChildrenHelper.addChildren(component, element,
|
|
101
|
-
expect(
|
|
118
|
+
const registry = ChildrenHelper.createRegistry();
|
|
119
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
120
|
+
expect(registry.rebuilt.length).toBe(1);
|
|
102
121
|
expect(destroyCount).toBe(0);
|
|
103
122
|
|
|
104
123
|
element.innerHTML = '<div data-component="widget"></div>';
|
|
105
|
-
ChildrenHelper.addChildren(component, element,
|
|
124
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
106
125
|
|
|
107
126
|
expect(destroyCount).toBe(1);
|
|
108
|
-
expect(
|
|
127
|
+
expect(registry.rebuilt.length).toBe(1);
|
|
109
128
|
});
|
|
110
129
|
|
|
111
|
-
it('destroyChildren calls destroy on every child', () => {
|
|
130
|
+
it('destroyChildren calls destroy on every keepable and rebuilt child and empties the registry', () => {
|
|
112
131
|
let destroyCount = 0;
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
ChildrenHelper.destroyChildren(children);
|
|
132
|
+
const registry = ChildrenHelper.createRegistry();
|
|
133
|
+
registry.rebuilt.push({ destroy: () => destroyCount++ });
|
|
134
|
+
registry.keepable.set('a', { instance: { destroy: () => destroyCount++ } });
|
|
135
|
+
ChildrenHelper.destroyChildren(registry);
|
|
118
136
|
expect(destroyCount).toBe(2);
|
|
137
|
+
expect(registry.rebuilt.length).toBe(0);
|
|
138
|
+
expect(registry.keepable.size).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('child with an unchanged keep value is moved into the new placeholder without calling its factory or rendering it', () => {
|
|
142
|
+
const created = [];
|
|
143
|
+
const component = {
|
|
144
|
+
registerChildren: () => [{
|
|
145
|
+
selector: 'widget',
|
|
146
|
+
keep: 'w',
|
|
147
|
+
factory: (el) => { const w = new Widget({ mountTarget: el }); created.push(w); return w; },
|
|
148
|
+
}],
|
|
149
|
+
};
|
|
150
|
+
const registry = ChildrenHelper.createRegistry();
|
|
151
|
+
|
|
152
|
+
element.innerHTML = '<div data-component="widget"></div>';
|
|
153
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
154
|
+
element.innerHTML = '<div data-component="widget"></div>';
|
|
155
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
156
|
+
|
|
157
|
+
expect(created.length).toBe(1);
|
|
158
|
+
expect(created[0].renders).toBe(1);
|
|
159
|
+
expect(created[0].destroyed).toBe(false);
|
|
160
|
+
expect(element.contains(created[0].getElement())).toBe(true);
|
|
161
|
+
expect(element.querySelector('[data-component="widget"]')).toBe(null);
|
|
162
|
+
expect(registry.kept).toEqual([created[0]]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('child whose keep value changed is destroyed and recreated', () => {
|
|
166
|
+
let keep = 1;
|
|
167
|
+
const created = [];
|
|
168
|
+
const component = {
|
|
169
|
+
registerChildren: () => [{
|
|
170
|
+
selector: 'widget',
|
|
171
|
+
keep,
|
|
172
|
+
factory: (el) => { const w = new Widget({ mountTarget: el }); created.push(w); return w; },
|
|
173
|
+
}],
|
|
174
|
+
};
|
|
175
|
+
const registry = ChildrenHelper.createRegistry();
|
|
176
|
+
|
|
177
|
+
element.innerHTML = '<div data-component="widget"></div>';
|
|
178
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
179
|
+
keep = 2;
|
|
180
|
+
element.innerHTML = '<div data-component="widget"></div>';
|
|
181
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
182
|
+
|
|
183
|
+
expect(created.length).toBe(2);
|
|
184
|
+
expect(created[0].destroyed).toBe(true);
|
|
185
|
+
expect(created[1].destroyed).toBe(false);
|
|
186
|
+
expect(registry.keepable.size).toBe(1);
|
|
187
|
+
expect(registry.kept.length).toBe(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('keepable child whose placeholder disappeared is destroyed', () => {
|
|
191
|
+
const created = [];
|
|
192
|
+
const component = {
|
|
193
|
+
registerChildren: () => [{
|
|
194
|
+
selector: 'widget',
|
|
195
|
+
keep: 'w',
|
|
196
|
+
factory: (el) => { const w = new Widget({ mountTarget: el }); created.push(w); return w; },
|
|
197
|
+
}],
|
|
198
|
+
};
|
|
199
|
+
const registry = ChildrenHelper.createRegistry();
|
|
200
|
+
|
|
201
|
+
element.innerHTML = '<div data-component="widget"></div>';
|
|
202
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
203
|
+
element.innerHTML = '';
|
|
204
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
205
|
+
|
|
206
|
+
expect(created[0].destroyed).toBe(true);
|
|
207
|
+
expect(registry.keepable.size).toBe(0);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('destroys removed children before creating new ones', () => {
|
|
211
|
+
const order = [];
|
|
212
|
+
let keep = 1;
|
|
213
|
+
const component = {
|
|
214
|
+
registerChildren: () => [{
|
|
215
|
+
selector: 'widget',
|
|
216
|
+
keep,
|
|
217
|
+
factory: (el) => {
|
|
218
|
+
const w = new Widget({ mountTarget: el });
|
|
219
|
+
const id = keep;
|
|
220
|
+
order.push(`create ${id}`);
|
|
221
|
+
w.onDestroy = () => order.push(`destroy ${id}`);
|
|
222
|
+
return w;
|
|
223
|
+
},
|
|
224
|
+
}],
|
|
225
|
+
};
|
|
226
|
+
const registry = ChildrenHelper.createRegistry();
|
|
227
|
+
|
|
228
|
+
element.innerHTML = '<div data-component="widget"></div>';
|
|
229
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
230
|
+
keep = 2;
|
|
231
|
+
element.innerHTML = '<div data-component="widget"></div>';
|
|
232
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
233
|
+
|
|
234
|
+
expect(order).toEqual(['create 1', 'destroy 1', 'create 2']);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('the same keep value under two placeholders of one selector gives each its own identity', () => {
|
|
238
|
+
const created = [];
|
|
239
|
+
const component = {
|
|
240
|
+
registerChildren: () => [{
|
|
241
|
+
selector: 'widget',
|
|
242
|
+
keep: 'w',
|
|
243
|
+
factory: (el) => { const w = new Widget({ mountTarget: el }); created.push(w); return w; },
|
|
244
|
+
}],
|
|
245
|
+
};
|
|
246
|
+
const registry = ChildrenHelper.createRegistry();
|
|
247
|
+
|
|
248
|
+
element.innerHTML = '<div data-component="widget"></div><div data-component="widget"></div>';
|
|
249
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
250
|
+
element.innerHTML = '<div data-component="widget"></div><div data-component="widget"></div>';
|
|
251
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
252
|
+
|
|
253
|
+
expect(created.length).toBe(2);
|
|
254
|
+
expect(registry.kept.length).toBe(2);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('warns about a duplicate keep value and recreates the duplicate on every render', () => {
|
|
258
|
+
const warnSpy = spyOn(console, 'warn').andCallFake(() => {});
|
|
259
|
+
const component = {
|
|
260
|
+
registerChildren: () => [
|
|
261
|
+
{ selector: 'widget', keep: 'w', factory: (el) => new Widget({ mountTarget: el }) },
|
|
262
|
+
{ selector: 'widget', keep: 'w', factory: (el) => new Widget({ mountTarget: el }) },
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
const registry = ChildrenHelper.createRegistry();
|
|
266
|
+
|
|
267
|
+
element.innerHTML = '<div data-component="widget"></div>';
|
|
268
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
269
|
+
warnSpy.restore();
|
|
270
|
+
|
|
271
|
+
expect(warnSpy.callCount).toBe(1);
|
|
272
|
+
expect(registry.keepable.size).toBe(1);
|
|
273
|
+
expect(registry.rebuilt.length).toBe(1);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('keptCount a catalog-style child only when its keepIf item, options and component are shallow-equal', () => {
|
|
277
|
+
const created = [];
|
|
278
|
+
let item = { id: 1, name: 'a' };
|
|
279
|
+
let options = { tone: 'x' };
|
|
280
|
+
const component = {
|
|
281
|
+
registerChildren: () => [{
|
|
282
|
+
selector: 'item-0',
|
|
283
|
+
keep: item.id,
|
|
284
|
+
scope: 'catalog:list:item',
|
|
285
|
+
keepIf: { item, options, component: Widget },
|
|
286
|
+
factory: (el) => { const w = new Widget({ mountTarget: el }); created.push(w); return w; },
|
|
287
|
+
}],
|
|
288
|
+
};
|
|
289
|
+
const registry = ChildrenHelper.createRegistry();
|
|
290
|
+
const rerender = () => {
|
|
291
|
+
element.innerHTML = '<div data-component="item-0"></div>';
|
|
292
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
rerender();
|
|
296
|
+
item = { id: 1, name: 'a' };
|
|
297
|
+
options = { tone: 'x' };
|
|
298
|
+
rerender();
|
|
299
|
+
expect(created.length).toBe(1);
|
|
300
|
+
|
|
301
|
+
item = { id: 1, name: 'b' };
|
|
302
|
+
rerender();
|
|
303
|
+
expect(created.length).toBe(2);
|
|
304
|
+
|
|
305
|
+
options = { tone: 'y' };
|
|
306
|
+
rerender();
|
|
307
|
+
expect(created.length).toBe(3);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('recreates a catalog-style child when its keepIf item changes between null, an object and an array', () => {
|
|
311
|
+
const created = [];
|
|
312
|
+
let item = null;
|
|
313
|
+
const component = {
|
|
314
|
+
registerChildren: () => [{
|
|
315
|
+
selector: 'item-0',
|
|
316
|
+
keep: 1,
|
|
317
|
+
scope: 'catalog:list:item',
|
|
318
|
+
keepIf: { item, options: undefined, component: Widget },
|
|
319
|
+
factory: (el) => { const w = new Widget({ mountTarget: el }); created.push(w); return w; },
|
|
320
|
+
}],
|
|
321
|
+
};
|
|
322
|
+
const registry = ChildrenHelper.createRegistry();
|
|
323
|
+
const rerender = () => {
|
|
324
|
+
element.innerHTML = '<div data-component="item-0"></div>';
|
|
325
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
rerender();
|
|
329
|
+
item = { 0: 'a' };
|
|
330
|
+
rerender();
|
|
331
|
+
expect(created.length).toBe(2);
|
|
332
|
+
|
|
333
|
+
item = ['a'];
|
|
334
|
+
rerender();
|
|
335
|
+
expect(created.length).toBe(3);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('ignores a non-HTML placeholder element', () => {
|
|
339
|
+
let calls = 0;
|
|
340
|
+
const component = {
|
|
341
|
+
registerChildren: () => [{
|
|
342
|
+
selector: 'icon',
|
|
343
|
+
factory: (el) => { calls++; return new Widget({ mountTarget: el }); },
|
|
344
|
+
}],
|
|
345
|
+
};
|
|
346
|
+
element.innerHTML = '<svg data-component="icon"></svg>';
|
|
347
|
+
|
|
348
|
+
ChildrenHelper.addChildren(component, element, ChildrenHelper.createRegistry());
|
|
349
|
+
|
|
350
|
+
expect(calls).toBe(0);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('moves a kept child with Element.moveBefore when the browser supports it', () => {
|
|
354
|
+
const moved = [];
|
|
355
|
+
const proto = Element.prototype;
|
|
356
|
+
proto.moveBefore = function (node, child) {
|
|
357
|
+
moved.push(node);
|
|
358
|
+
this.insertBefore(node, child);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const component = {
|
|
363
|
+
registerChildren: () => [{ selector: 'widget', keep: 'w', factory: (el) => new Widget({ mountTarget: el }) }],
|
|
364
|
+
};
|
|
365
|
+
const registry = ChildrenHelper.createRegistry();
|
|
366
|
+
element.innerHTML = '<div data-component="widget"></div>';
|
|
367
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
368
|
+
const [{ instance }] = registry.keepable.values();
|
|
369
|
+
|
|
370
|
+
// Stand-in for a parked child: still connected, outside the new placeholder.
|
|
371
|
+
document.body.appendChild(instance.getElement());
|
|
372
|
+
element.innerHTML = '<div data-component="widget"></div>';
|
|
373
|
+
ChildrenHelper.addChildren(component, element, registry);
|
|
374
|
+
|
|
375
|
+
expect(moved).toEqual([instance.getElement()]);
|
|
376
|
+
expect(element.contains(instance.getElement())).toBe(true);
|
|
377
|
+
expect(element.querySelector('[data-component="widget"]')).toBe(null);
|
|
378
|
+
} finally {
|
|
379
|
+
delete proto.moveBefore;
|
|
380
|
+
}
|
|
119
381
|
});
|
|
120
382
|
});
|
|
@@ -1,31 +1,149 @@
|
|
|
1
|
-
import { BaseComponent
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
import type { BaseComponent } from '../base-component.js';
|
|
2
|
+
import * as DomHelper from './dom.helper.js';
|
|
3
|
+
|
|
4
|
+
// What a catalog item was built from: a kept item must still match it.
|
|
5
|
+
export interface KeepCondition {
|
|
6
|
+
item: unknown;
|
|
7
|
+
options: Record<string, unknown>;
|
|
8
|
+
component: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Internal fields CatalogHelper sets on top of the public ComponentConfig.
|
|
12
|
+
export interface KeepableComponentConfig extends Nutin.ComponentConfig {
|
|
13
|
+
scope?: string;
|
|
14
|
+
keepIf?: KeepCondition;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface KeepableChild {
|
|
18
|
+
instance: BaseComponent;
|
|
19
|
+
keepIf?: KeepCondition;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ChildRegistry {
|
|
23
|
+
keepable: Map<string, KeepableChild>;
|
|
24
|
+
rebuilt: BaseComponent[];
|
|
25
|
+
kept: BaseComponent[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Placement {
|
|
29
|
+
config: KeepableComponentConfig;
|
|
30
|
+
placeholder: HTMLElement;
|
|
31
|
+
id?: string;
|
|
32
|
+
kept?: KeepableChild;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createRegistry(): ChildRegistry {
|
|
36
|
+
return { keepable: new Map(), rebuilt: [], kept: [] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Children whose keep value is unchanged are moved into their new
|
|
40
|
+
// placeholder as-is; everything else is destroyed and recreated.
|
|
41
|
+
export function addChildren(
|
|
42
|
+
component: BaseComponent,
|
|
43
|
+
element: HTMLElement,
|
|
44
|
+
registry: ChildRegistry
|
|
45
|
+
): void {
|
|
46
|
+
const placements = matchPlaceholders(component.registerChildren(), element, registry);
|
|
47
|
+
destroyUnkept(registry, placements);
|
|
48
|
+
|
|
49
|
+
// Every kept child moves before any new one renders, so move targets are
|
|
50
|
+
// prepared once rather than after each DOM change.
|
|
51
|
+
const kept = placements.filter(({ kept }) => kept);
|
|
52
|
+
kept.forEach(({ kept, placeholder }) => DomHelper.prepareMoveTarget(placeholder.parentNode, kept!.instance.getElement()));
|
|
53
|
+
kept.forEach(placement => keep(placement, registry));
|
|
54
|
+
placements.filter(({ kept }) => !kept).forEach(placement => create(placement, registry));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getKeepableElements(registry: ChildRegistry): HTMLElement[] {
|
|
58
|
+
return Array.from(registry.keepable.values(), ({ instance }) => instance.getElement());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function destroyChildren(registry: ChildRegistry): void {
|
|
62
|
+
registry.rebuilt.forEach(child => child.destroy());
|
|
63
|
+
registry.keepable.forEach(({ instance }) => instance.destroy());
|
|
64
|
+
registry.rebuilt = [];
|
|
65
|
+
registry.keepable.clear();
|
|
66
|
+
registry.kept = [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function matchPlaceholders(
|
|
70
|
+
configs: KeepableComponentConfig[],
|
|
71
|
+
element: HTMLElement,
|
|
72
|
+
registry: ChildRegistry
|
|
73
|
+
): Placement[] {
|
|
74
|
+
const placements: Placement[] = [];
|
|
75
|
+
const seen = new Set<string>();
|
|
76
|
+
|
|
77
|
+
configs.forEach(config => {
|
|
78
|
+
element.querySelectorAll(`[data-component="${config.selector}"]`).forEach((placeholder, occurrence) => {
|
|
79
|
+
if (!(placeholder instanceof HTMLElement)) return;
|
|
80
|
+
const id = getId(config, occurrence, seen);
|
|
81
|
+
const previous = id === undefined ? undefined : registry.keepable.get(id);
|
|
82
|
+
const kept = previous && canKeep(previous, config) ? previous : undefined;
|
|
83
|
+
placements.push({ config, placeholder, id, kept });
|
|
21
84
|
});
|
|
22
|
-
}
|
|
85
|
+
});
|
|
23
86
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
87
|
+
return placements;
|
|
88
|
+
}
|
|
27
89
|
|
|
28
|
-
|
|
29
|
-
|
|
90
|
+
function getId(config: KeepableComponentConfig, occurrence: number, seen: Set<string>): string | undefined {
|
|
91
|
+
if (config.keep === undefined) return undefined;
|
|
92
|
+
|
|
93
|
+
const scope = config.scope ?? config.selector;
|
|
94
|
+
const id = JSON.stringify([scope, config.keep, occurrence]);
|
|
95
|
+
if (seen.has(id)) {
|
|
96
|
+
console.warn(`Duplicate child keep value "${config.keep}" in "${scope}" - it will be recreated on every render.`);
|
|
97
|
+
return undefined;
|
|
30
98
|
}
|
|
99
|
+
seen.add(id);
|
|
100
|
+
return id;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function canKeep(previous: KeepableChild, config: KeepableComponentConfig): boolean {
|
|
104
|
+
const before = previous.keepIf;
|
|
105
|
+
const after = config.keepIf;
|
|
106
|
+
if (!before || !after) return before === after;
|
|
107
|
+
|
|
108
|
+
return before.component === after.component
|
|
109
|
+
&& shallowEqual(before.item, after.item)
|
|
110
|
+
&& shallowEqual(before.options, after.options);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function destroyUnkept(registry: ChildRegistry, placements: Placement[]): void {
|
|
114
|
+
const keptIds = new Set(placements.filter(({ kept }) => kept).map(({ id }) => id));
|
|
115
|
+
|
|
116
|
+
registry.rebuilt.forEach(child => child.destroy());
|
|
117
|
+
registry.rebuilt = [];
|
|
118
|
+
registry.kept = [];
|
|
119
|
+
registry.keepable.forEach((entry, id) => {
|
|
120
|
+
if (keptIds.has(id)) return;
|
|
121
|
+
entry.instance.destroy();
|
|
122
|
+
registry.keepable.delete(id);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function keep({ config, placeholder, kept }: Placement, registry: ChildRegistry): void {
|
|
127
|
+
DomHelper.replacePlaceholder(kept!.instance.getElement(), placeholder);
|
|
128
|
+
kept!.keepIf = config.keepIf;
|
|
129
|
+
registry.kept.push(kept!.instance);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function create({ config, placeholder, id }: Placement, registry: ChildRegistry): void {
|
|
133
|
+
const child = config.factory(placeholder);
|
|
134
|
+
child.render();
|
|
135
|
+
if (id === undefined) registry.rebuilt.push(child);
|
|
136
|
+
else registry.keepable.set(id, { instance: child, keepIf: config.keepIf });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function shallowEqual(a: unknown, b: unknown): boolean {
|
|
140
|
+
if (Object.is(a, b)) return true;
|
|
141
|
+
if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
|
|
142
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
143
|
+
|
|
144
|
+
const aRecord = a as Record<string, unknown>;
|
|
145
|
+
const bRecord = b as Record<string, unknown>;
|
|
146
|
+
const aKeys = Object.keys(aRecord);
|
|
147
|
+
return aKeys.length === Object.keys(bRecord).length
|
|
148
|
+
&& aKeys.every(key => Object.prototype.hasOwnProperty.call(bRecord, key) && Object.is(aRecord[key], bRecord[key]));
|
|
31
149
|
}
|