@nutin/cli 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +8 -9
  3. package/lib/index.mjs +18 -18
  4. package/lib/src/context-builder.mjs +21 -16
  5. package/lib/src/print.mjs +1 -0
  6. package/lib/src/project-generator.mjs +5 -5
  7. package/lib/src/prompts.mjs +13 -85
  8. package/lib/src/version.mjs +1 -1
  9. package/package.json +1 -1
  10. package/templates/base/builder.config.js.hbs +1 -0
  11. package/templates/base/src/app/globals.d.ts.hbs +12 -8
  12. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +25 -8
  13. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +34 -20
  14. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +34 -0
  15. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +11 -12
  16. package/templates/base/src/core/base-classes/component/component.ts.hbs +13 -17
  17. package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +17 -0
  18. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +2 -1
  19. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +0 -33
  20. package/templates/base/src/core/services/router/router.ts.hbs +0 -1
  21. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +35 -7
  22. package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +31 -8
  23. package/templates/base/src/libs/popover/helpers/focus-trap.helper.ts.hbs +197 -0
  24. package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +75 -0
  25. package/templates/base/src/libs/popover/popover.ts.hbs +84 -91
  26. package/templates/base/src/libs/snackbar/snackbar.ts.hbs +6 -3
  27. package/templates/base/src/styles/base/_variables.scss.hbs +1 -0
  28. package/templates/base/src/styles/core/_anchors.scss.hbs +10 -0
  29. package/templates/base/src/styles/core/_index.scss.hbs +6 -0
  30. package/templates/base/src/styles/core/_popover.scss.hbs +96 -0
  31. package/templates/base/src/styles/core/_snackbar.scss.hbs +50 -0
  32. package/templates/base/src/styles/main.scss.hbs +1 -1
  33. package/templates/features/i18n/src/core/services/i18n/i18n.ts.hbs +41 -8
  34. package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +14 -0
  35. package/templates/base/src/styles/core/_libs.scss.hbs +0 -132
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## V1.3.1
4
+
5
+ - Minor features:
6
+ - New BaseComponent protected method.
7
+ Register events that will trigger re-render. Call this in component's constructor.
8
+ ```ts
9
+ // force = true: calls forceRender() instead of render()
10
+ listenToRenderEvents(events: EventKey[], force: boolean = false): void
11
+ ```
12
+ - data-optional now supports JS values
13
+ ```html
14
+ data-optional="${myValue}"
15
+ ```
16
+ - PopoverView and AnchorComponents now have accessibility features
17
+ - ButtonManager now handles checkboxes as well
18
+ - i18nService now emits `language-changed` event and exposes related methods
19
+
20
+ - Fixes:
21
+ - CatalogConfig no longer renders multiple containers when render() or forceRender() are triggered by events
22
+ - Event Listeners are now destroyed properly before triggering a re-render
23
+ - Router now correctly redirects to 404 page when URL starts with two slashes `//`.
24
+ - Popover no longer flickers on close
25
+ - Snackbar messages are now properly sanitized
26
+
27
+ - CLI
28
+ - Rework: App creation flow
29
+ - No features enabled by default
30
+ - Presets `--preset <minimal|standard|full|cicd>`
31
+ Minimal: External templates
32
+ Standard: Minimal + i18n & built-in SCSS utilities.
33
+ Full: Standard + deployment helpers & built-in testing toolkit
34
+ CI/CD: Minimal + deployment helpers
35
+ - Remaining flags: `--i18n`, `--deploy-helper`, `--testin-nutin`, `--transition`
36
+
3
37
  ## V1.3.0
4
38
 
5
39
  Version 1.3.0 marks a stability milestone with various improvements and refinements, making this the recommended version for new projects.
package/README.md CHANGED
@@ -39,19 +39,18 @@ npx @nutin/cli
39
39
  # Package manager
40
40
  -pm --package-manager <manager> # npm (Default), yarn, pnpm, bun
41
41
 
42
+ # Presets
43
+ --preset <minimal|standard|full|cicd>
44
+ # minimal: external templates
45
+ # standard: minimal + i18n & built-in SCSS classes
46
+ # full: standard + deployment helpers & built-in testing toolkit
47
+ # cicd: minimal + deployment helpers
48
+
42
49
  # Features
43
- --template # (Default) Use external templates
44
- --no-template # Use inline templates
45
- --stylin-nutin # (Default) Use built-in SCSS utility classes & mixins
46
- --no-stylin-nutin # Do not use built-in SCSS utility
47
- --testin-nutin # Use testin-nutin toolkit
48
- --no-testin-nutin # (Default) Do not use testin-nutin toolkit
49
50
  --i18n # Use i18n & json-based content
50
- --no-i18n # (Default) Do not use i18n & json-based content
51
51
  --deploy-helper # Use Docker & deployment helpers
52
- --no-deploy-helper, # (Default) Do not use Docker & deployment helpers
52
+ --testin-nutin # Use testin-nutin toolkit
53
53
  --transition # Use animated view transitions. Note: May interfere with CSS `position: fixed`, `z-index`...
54
- --no-transition # (Default) Do not use animated view transitions
55
54
  ```
56
55
 
57
56
  ## [First app tutorial](https://www.nutin.org/tutorial)
package/lib/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { program } from 'commander';
2
- import { print } from './src/print.mjs';
2
+ import { print, chalk } from './src/print.mjs';
3
3
  import { promptUser } from './src/prompts.mjs';
4
4
  import { createProject } from './src/project-generator.mjs';
5
5
  import { displaySuccessMessage } from './src/utils.mjs';
@@ -8,34 +8,34 @@ import { packageVersion } from './src/version.mjs';
8
8
  export async function createApp() {
9
9
  program
10
10
  .version(packageVersion)
11
+ .configureOutput({
12
+ helpWidth: 100
13
+ })
11
14
  .argument('[projectName]', 'Name of the project')
12
- .option('-d, --defaults', 'Use default options')
13
15
  .option('-pm, --package-manager <manager>', 'Specify package manager (npm, yarn, pnpm, bun)')
14
- .option('--template', '(Default) Use external templates')
15
- .option('--no-template', 'Do not use external templates')
16
- .option('--stylin-nutin', '(Default) Use built-in SCSS utility classes & mixins')
17
- .option('--no-stylin-nutin', 'Do not use built-in SCSS utility')
16
+ .option('--preset <preset>',
17
+ `${chalk.yellow('Project preset configuration:')}
18
+ ${chalk.cyan('• minimal')}${chalk.boldGray(' - external templates')}
19
+ ${chalk.cyan('• standard')}${chalk.boldGray(' - minimal + i18n & built-in SCSS utilities')}
20
+ ${chalk.cyan('• full')}${chalk.boldGray(' - standard + deployment helpers & built-in testing toolkit')}
21
+ ${chalk.cyan('• cicd')}${chalk.boldGray(' - minimal + deployment helpers')}`
22
+ )
18
23
  .option('--i18n', 'Use i18n & json-based content')
19
- .option('--no-i18n', '(Default) Do not use i18n & json-based content')
20
24
  .option('--deploy-helper', 'Use Docker & deployment helpers')
21
- .option('--no-deploy-helper', '(Default) Do not use Docker & deployment helpers')
22
- .option('--testin-nutin', 'Use testin-nutin toolkit')
23
- .option('--no-testin-nutin', '(Default) Do not use testin-nutin toolkit')
24
- .option('--transition', 'Use animated view transitions\nNote: May interfere with CSS `position: fixed`, `z-index`...')
25
- .option('--no-transition', '(Default) Do not use animated view transitions')
26
- .addHelpText('afterAll', '\nChaining options :\nExample: -d -pm npm --i18n --testinNutin')
25
+ .option('--testin-nutin', 'Use built-in testing toolkit')
26
+ .option('--transition', 'Use animated view transitions\nNote: may interfere with CSS `position: fixed`, `z-index`…')
27
27
  .action(async (projectName, cliOptions) => {
28
28
  print.blue('🚀 Welcome to nutin !');
29
-
30
29
  try {
31
- const answers = await promptUser(projectName, cliOptions);
32
- await createProject(answers);
33
- displaySuccessMessage(answers);
30
+ const preferences = await promptUser(projectName, cliOptions);
31
+ await createProject(preferences);
32
+ displaySuccessMessage(preferences);
34
33
  } catch (error) {
35
- print.boldError(`❌ Error creating project: ${error.message}`);
34
+ print.boldError(`❌ Error creating project: ${error.stack}`);
36
35
  process.exit(1);
37
36
  }
38
37
  });
39
38
 
40
39
  program.parse();
41
40
  }
41
+
@@ -1,32 +1,37 @@
1
1
  import { getCiCommand } from './package-manager.mjs';
2
2
  import { packageVersion } from './version.mjs';
3
3
 
4
+ const PRESET_MAP = {
5
+ default: { template: false, stylinNutin: false, i18n: false, deployHelper: false, testinNutin: false },
6
+ minimal: { template: true, stylinNutin: false, i18n: false, deployHelper: false, testinNutin: false },
7
+ standard: { template: true, stylinNutin: true, i18n: true, deployHelper: false, testinNutin: false },
8
+ full: { template: true, stylinNutin: true, i18n: true, deployHelper: true, testinNutin: true },
9
+ cicd: { template: true, stylinNutin: false, i18n: false, deployHelper: true, testinNutin: false },
10
+ };
11
+
4
12
  export const defaults = {
5
13
  projectName: 'my-nutin-app',
6
14
  packageManager: 'npm',
7
- stylinNutin: true,
8
- template: true,
9
- i18n: false,
10
- deployHelper: false,
11
- testinNutin: false,
12
- transition: false
15
+ ...PRESET_MAP['default']
13
16
  };
14
17
 
15
18
  export class ContextBuilder {
16
- buildContext(answers) {
19
+ buildContext(preferences) {
17
20
  const version = packageVersion;
18
- const ciCommand = getCiCommand(answers.packageManager);
21
+ const ciCommand = getCiCommand(preferences.packageManager);
22
+
23
+ const preset = PRESET_MAP[preferences.preset] ?? PRESET_MAP['default'];
19
24
 
20
25
  return {
21
- projectName: answers.projectName,
22
- packageManager: answers.packageManager,
26
+ projectName: preferences.projectName,
27
+ packageManager: preferences.packageManager,
23
28
 
24
- template: answers.template ?? defaults.template,
25
- stylinNutin: answers.stylinNutin ?? defaults.stylinNutin,
26
- i18n: answers.i18n ?? defaults.i18n,
27
- deployHelper: answers.deployHelper ?? defaults.deployHelper,
28
- testinNutin: answers.testinNutin ?? defaults.testinNutin,
29
- transition: answers.transition ?? defaults.transition,
29
+ template: preset.template,
30
+ stylinNutin: preset.stylinNutin,
31
+ i18n: preferences.i18n ?? preset.i18n,
32
+ deployHelper: preferences.deployHelper ?? preset.deployHelper,
33
+ testinNutin: preferences.testinNutin ?? preset.testinNutin,
34
+ transition: preferences.transition,
30
35
 
31
36
  ciCommand: ciCommand,
32
37
  version: version
package/lib/src/print.mjs CHANGED
@@ -12,6 +12,7 @@ export const chalk = {
12
12
  boldYellow: (s) => `\x1b[1;33m${s}\x1b[0m`,
13
13
  boldCyan: (s) => `\x1b[1;36m${s}\x1b[0m`,
14
14
  boldPurple: (s) => `\x1b[1;35m${s}\x1b[0m`,
15
+ boldGray: (s) => `\x1b[1;90m${s}\x1b[0m`,
15
16
  };
16
17
 
17
18
  export const print = {
@@ -16,10 +16,10 @@ export class ProjectGenerator {
16
16
  this.builder = new ContextBuilder();
17
17
  }
18
18
 
19
- async createProject(answers) {
20
- const { projectName } = answers;
19
+ async createProject(preferences) {
20
+ const { projectName } = preferences;
21
21
  const projectPath = path.join(process.cwd(), projectName);
22
- const context = this.builder.buildContext(answers);
22
+ const context = this.builder.buildContext(preferences);
23
23
 
24
24
  await this.validateProjectPath(projectPath, projectName);
25
25
  await this.setupProjectStructure(projectPath, context);
@@ -48,6 +48,6 @@ export class ProjectGenerator {
48
48
 
49
49
  export const projectGenerator = new ProjectGenerator();
50
50
 
51
- export async function createProject(answers) {
52
- return projectGenerator.createProject(answers);
51
+ export async function createProject(preferences) {
52
+ return projectGenerator.createProject(preferences);
53
53
  }
@@ -5,112 +5,40 @@ export async function promptUser(initialName, cliOptions = {}) {
5
5
 
6
6
  let projectName = initialName;
7
7
  if (!projectName) {
8
- const nameAnswer = await inquirer.prompt([
8
+ const nameInput = await inquirer.prompt([
9
9
  {
10
10
  type: 'input',
11
11
  name: 'projectName',
12
12
  message: 'Project name:',
13
13
  default: defaults.projectName,
14
- validate: validateProjectName
15
- }
14
+ validate: validateProjectName,
15
+ },
16
16
  ]);
17
- projectName = nameAnswer.projectName;
17
+ projectName = nameInput.projectName;
18
18
  }
19
19
 
20
20
  let packageManager = cliOptions.packageManager;
21
21
  if (!packageManager) {
22
- const pmAnswer = await inquirer.prompt([
22
+ const pmPreference = await inquirer.prompt([
23
23
  {
24
24
  type: 'list',
25
25
  name: 'packageManager',
26
26
  message: 'Package manager:',
27
27
  choices: ['npm', 'yarn', 'pnpm', 'bun'],
28
- default: defaults.packageManager
29
- }
28
+ default: defaults.packageManager,
29
+ },
30
30
  ]);
31
- packageManager = pmAnswer.packageManager;
31
+ packageManager = pmPreference.packageManager;
32
32
  }
33
33
 
34
- if (cliOptions.defaults) {
35
- return {
36
- projectName,
37
- packageManager,
38
- stylinNutin: cliOptions.stylinNutin,
39
- template: cliOptions.template,
40
- i18n: cliOptions.i18n,
41
- deployHelper: cliOptions.deployHelper,
42
- testinNutin: cliOptions.testinNutin,
43
- transition: cliOptions.transitions
44
- };
45
- }
46
-
47
- const questions = [];
48
-
49
- if (cliOptions.stylinNutin === undefined) {
50
- questions.push({
51
- type: 'confirm',
52
- name: 'stylinNutin',
53
- message: 'Use built-in SCSS utility classes ?',
54
- default: defaults.stylinNutin
55
- });
56
- }
57
-
58
- if (cliOptions.template === undefined) {
59
- questions.push({
60
- type: 'confirm',
61
- name: 'template',
62
- message: 'Use external templates ?',
63
- default: defaults.template
64
- });
65
- }
66
-
67
- if (cliOptions.i18n === undefined) {
68
- questions.push({
69
- type: 'confirm',
70
- name: 'i18n',
71
- message: 'Use i18n with json-based content ?',
72
- default: defaults.i18n
73
- });
74
- }
75
-
76
- if (cliOptions.deployHelper === undefined) {
77
- questions.push({
78
- type: 'confirm',
79
- name: 'deployHelper',
80
- message: 'Use Docker & deployment helpers ?',
81
- default: defaults.deployHelper
82
- });
83
- }
84
-
85
- if (cliOptions.testinNutin === undefined) {
86
- questions.push({
87
- type: 'confirm',
88
- name: 'testinNutin',
89
- message: 'Use testin-nutin toolkit?',
90
- default: defaults.testinNutin
91
- });
92
- }
93
-
94
- if (cliOptions.transition === undefined) {
95
- questions.push({
96
- type: 'confirm',
97
- name: 'transition',
98
- message: 'Use animated view transitions?',
99
- default: defaults.transition
100
- });
101
- }
102
-
103
- const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
104
-
105
34
  return {
106
35
  projectName,
107
36
  packageManager,
108
- template: cliOptions.template ?? answers.template,
109
- stylinNutin: cliOptions.stylinNutin ?? answers.stylinNutin,
110
- i18n: cliOptions.i18n ?? answers.i18n,
111
- deployHelper: cliOptions.deployHelper ?? answers.deployHelper,
112
- transition: cliOptions.transition ?? answers.transition,
113
- testinNutin: cliOptions.testinNutin ?? answers.testinNutin
37
+ preset: cliOptions.preset,
38
+ i18n: cliOptions.i18n,
39
+ deployHelper: cliOptions.deployHelper,
40
+ testinNutin: cliOptions.testinNutin,
41
+ transition: cliOptions.transition,
114
42
  };
115
43
  }
116
44
 
@@ -1 +1 @@
1
- export const packageVersion = '1.3.0';
1
+ export const packageVersion = '1.3.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nutin/cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Build SPAs by understanding SPAs. A minimal frontend toolkit crafted for learning and lean applications.",
5
5
  "preferGlobal": true,
6
6
  "main": "lib/index.mjs",
@@ -3,6 +3,7 @@ export default {
3
3
  bundle: true,
4
4
  minify: true,
5
5
  sourcemap: false,
6
+ drop: ['console', 'debugger'],
6
7
  platform: 'browser',
7
8
  format: 'esm',
8
9
  target: ['es2015'],
@@ -1,12 +1,16 @@
1
1
  declare type CoreEventMap = {
2
- 'navigate': string;
3
- 'reload': string;
4
- 'view-mount': string;
5
- 'view-render': string;
6
- 'view-unmount': string;
7
- 'track-pageview': { page: string };
8
-
9
- // Add other events and their payload types here
2
+ 'navigate': string;
3
+ 'reload': string;
4
+ 'view-mount': string;
5
+ 'view-render': string;
6
+ 'view-unmount': string;
7
+ 'track-pageview': { page: string };
8
+ 'popover-opened': string;
9
+ 'popover-close': string;
10
+ {{#if i18n}}
11
+ 'language-changed': string;
12
+ {{/if}}
13
+ // Add other events and their payload types here
10
14
  };
11
15
 
12
16
  declare type StoreEventMap = {[ K in `store:${string}` ]: any; };
@@ -6,19 +6,20 @@ import { I18nHelper } from './helpers/i18n.helper.js';
6
6
  import { PipeHelper } from './helpers/pipe.helper.js';
7
7
  import { ChildrenHelper } from './helpers/children.helper.js';
8
8
  import { CatalogHelper, CatalogConfig } from './helpers/catalog.helper.js';
9
- import { TrustLevel } from './helpers/security.helper.js';
10
- export { CatalogItemConfig } from './helpers/catalog.helper.js';
9
+ import { Component, ComponentOptions, AppEventBus } from '../../index.js';
10
+ import { SecurityHelper, TrustLevel } from './helpers/security.helper.js';
11
+ export { CatalogItemConfig, CatalogConfig, CatalogItemPrimitive } from './helpers/catalog.helper.js';
11
12
 
12
13
  /**```typescript
13
14
  * export interface ComponentConfig {
14
15
  * selector: string;
15
- * factory: (element: HTMLElement) => BaseComponent;
16
+ * factory: (element: HTMLElement) => Component;
16
17
  * }
17
18
  * ```
18
19
  */
19
20
  export interface ComponentConfig {
20
21
  selector: string;
21
- factory: (element: HTMLElement) => BaseComponent;
22
+ factory: (element: HTMLElement) => Component;
22
23
  }
23
24
 
24
25
  export interface BaseComponentOptions {
@@ -31,7 +32,8 @@ export interface BaseComponentOptions {
31
32
  export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
32
33
  private _children: BaseComponent[] = [];
33
34
  protected element: T;
34
- protected eventListeners: Array<[string, EventListener]> = [];
35
+ protected eventListeners: Array<[EventTarget, string, EventListener]> = [];
36
+ private _options: ComponentOptions;
35
37
 
36
38
  constructor({
37
39
  template = '',
@@ -40,6 +42,7 @@ export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
40
42
  trustLevel
41
43
  }: BaseComponentOptions) {
42
44
  this.element = DomHelper.createElement<T>(tagName, template, trustLevel);
45
+ this._options = { template, mountTarget, tagName, trustLevel };
43
46
  this.parseDataAttributes();
44
47
  this.autoBindEvents();
45
48
  DomHelper.mountElement(this.element, mountTarget);
@@ -50,19 +53,20 @@ export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
50
53
  }
51
54
 
52
55
  public destroy(): void {
53
- EventHelper.destroyEvents(this.element, this.eventListeners);
56
+ EventHelper.destroyEvents(this.eventListeners);
54
57
  ChildrenHelper.destroyChildren(this._children);
55
58
  this.element.remove();
56
59
  }
57
60
 
58
61
  public render(): HTMLElement {
59
62
  this.addChildren();
63
+ DomHelper.cleanupOptionalContent();
60
64
  return this.element;
61
65
  }
62
66
 
63
67
  /**
64
68
  * ```typescript
65
- interface CatalogConfig {
69
+ interface CatalogConfig extends ComponentOptions {
66
70
  array: CatalogItemConfig[];
67
71
  elementName: string;
68
72
  elementTag?: keyof HTMLElementTagNameMap;
@@ -72,7 +76,7 @@ interface CatalogConfig {
72
76
  ```
73
77
  */
74
78
  protected catalogConfig(config: CatalogConfig): ComponentConfig[] {
75
- return CatalogHelper.generateCatalog(config);
79
+ return CatalogHelper.generateCatalog(config, this.element);
76
80
  }
77
81
 
78
82
  protected parseDataAttributes(): void {
@@ -83,10 +87,23 @@ interface CatalogConfig {
83
87
  }
84
88
 
85
89
  protected autoBindEvents(): void {
90
+ EventHelper.destroyEvents(this.eventListeners);
91
+ this.eventListeners.length = 0;
86
92
  EventHelper.bindEvents(this, this.element, this.eventListeners);
87
93
  }
88
94
 
89
95
  protected addChildren(): void {
90
96
  ChildrenHelper.addChildren(this, this.element, this._children);
91
97
  }
98
+
99
+ protected forceRender(): void {
100
+ this.element.innerHTML = SecurityHelper.sanitizeTemplate(this._options.template, this._options.trustLevel);
101
+ this.render();
102
+ }
103
+
104
+ protected listenToRenderEvents(events: EventKey[], force: boolean = false): void {
105
+ events.forEach((event: EventKey) => {
106
+ AppEventBus.subscribe(event, () => force ? this.forceRender() : this.render());
107
+ })
108
+ }
92
109
  }
@@ -1,11 +1,11 @@
1
- import { BaseComponent, ComponentConfig } from "../base-component.js";
1
+ import { Component, ComponentConfig, ComponentOptions } from "../../index.js";
2
2
 
3
- export interface CatalogConfig {
3
+ export interface CatalogConfig extends ComponentOptions {
4
4
  array: CatalogItemConfig[];
5
5
  elementName: string;
6
6
  elementTag?: keyof HTMLElementTagNameMap;
7
7
  selector: string;
8
- component: new (el: HTMLElement, data: any) => BaseComponent;
8
+ component: new (el: HTMLElement, data: any, props?: any) => Component;
9
9
  };
10
10
 
11
11
  export interface CatalogItemBase {
@@ -23,12 +23,12 @@ export type CatalogItemConfig<T = any> =
23
23
 
24
24
  /**
25
25
  * ```typescript
26
- interface CatalogConfig {
26
+ interface CatalogConfig extends ComponentOptions {
27
27
  array: CatalogItemConfig[];
28
28
  elementName: string;
29
29
  elementTag?: keyof HTMLElementTagNameMap;
30
30
  selector: string;
31
- component: new (el: HTMLElement, data: any) => BaseComponent;
31
+ component: new (el: HTMLElement, data: any) => Component;
32
32
  };
33
33
 
34
34
  type CatalogItemConfig<T = any> =
@@ -36,42 +36,56 @@ type CatalogItemConfig<T = any> =
36
36
  ```
37
37
  */
38
38
  export class CatalogHelper {
39
- public static generateCatalog(config: CatalogConfig): ComponentConfig[] {
39
+ public static generateCatalog(config: CatalogConfig, scope: Element = document.documentElement): ComponentConfig[] {
40
40
  if (!config.array || config.array.length < 1) return [];
41
41
 
42
- const container = document.querySelector(`[data-catalog="${config.selector}"]`);
42
+ const componentConfigs: ComponentConfig[] = [];
43
+ const containers = scope.querySelectorAll(`[data-catalog="${config.selector}"]`);
43
44
 
44
- if (!container || !(container instanceof HTMLElement)) return [];
45
+ containers.forEach((container) => {
46
+ if (!container || !(container instanceof HTMLElement) || (container.firstElementChild)) return;
47
+ componentConfigs.push(...this.getComponentConfigArray(config, container))
48
+ })
45
49
 
46
- const componentConfigs: ComponentConfig[] = [];
50
+ return componentConfigs;
51
+ }
47
52
 
48
- for (let i = 0; i < config.array.length; i++) {
53
+ private static getComponentConfigArray(config: CatalogConfig, container: HTMLElement): ComponentConfig[] {
54
+ const componentConfigs: ComponentConfig[] = [];
55
+ for (let i = 0; i < config.array.length; i++) {
49
56
  this.createElements(i, config, container);
50
57
  this.pushConfig(i, componentConfigs, config);
51
58
  }
52
-
53
59
  return componentConfigs;
54
60
  }
55
61
 
56
62
  private static createElements(index: number, config: CatalogConfig, container: HTMLElement): void {
57
- const el = document.createElement(config.elementTag || 'div');
63
+ const wrapper = document.createElement(config.elementTag || 'div');
64
+ const el = document.createElement('div');
65
+ wrapper.appendChild(el);
58
66
  el.setAttribute('data-component', `${config.elementName}-${index}`);
59
- el.dataset.index = String(index);
60
- container?.appendChild(el);
67
+ wrapper.dataset.index = String(index);
68
+ container?.appendChild(wrapper);
61
69
  }
62
70
 
63
71
  private static pushConfig(index: number, componentConfigs: ComponentConfig[], config: CatalogConfig): void {
64
- const item = config.array[index];
72
+ const configWithIndex = this.getConfigWithIndex(config, index)
73
+
74
+ const { props, defaults, normalizeKeys } = config;
75
+ const options = { ...props, ...defaults, ...normalizeKeys };
65
76
 
66
- const configWithIndex: CatalogItemConfig = (item && typeof item === 'object')
67
- ? { ...(item as object), index: index }
68
- : { value: item, index: index };
69
-
70
77
  componentConfigs.push(
71
78
  {
72
79
  selector: `${config.elementName}-${index}`,
73
- factory: (el) => new config.component(el, configWithIndex)
80
+ factory: (el) => new config.component(el, configWithIndex, options),
74
81
  },
75
82
  )
76
83
  }
84
+
85
+ private static getConfigWithIndex(config: CatalogConfig, index: number): CatalogItemConfig {
86
+ const item = config.array[index];
87
+ return (item && typeof item === 'object')
88
+ ? { ...(item as object), index: index }
89
+ : { value: item, index: index };
90
+ }
77
91
  }
@@ -24,6 +24,40 @@ export class DomHelper {
24
24
  element.innerHTML = SecurityHelper.sanitizeTemplate(template, trustLevel);
25
25
  return element;
26
26
  }
27
+
28
+ public static cleanupOptionalContent(): void {
29
+ const isEmpty = (el: HTMLElement): boolean => {
30
+ if (el instanceof HTMLImageElement) {
31
+ return !el.src || el.src.trim() === "";
32
+ }
33
+
34
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
35
+ return !el.value?.trim();
36
+ }
37
+
38
+ if (el instanceof HTMLMediaElement || el instanceof HTMLSourceElement) {
39
+ return !el.getAttribute("src");
40
+ }
41
+
42
+ const content = el.textContent?.trim();
43
+ return !content || content === "undefined";
44
+ };
45
+
46
+ const isValueUndefined = (el: HTMLElement): boolean => {
47
+ const value = el.getAttribute("data-optional");
48
+ if (!value) return false;
49
+ return (
50
+ value.trim() === '' ||
51
+ value === "undefined" ||
52
+ value === "null"
53
+ );
54
+ }
55
+
56
+ document.querySelectorAll<HTMLElement>("[data-optional]").forEach(el => {
57
+ if (isValueUndefined(el) || isEmpty(el)) el.remove();
58
+ el.removeAttribute("data-optional");
59
+ });
60
+ }
27
61
 
28
62
  private static appendOrReplace(config: DomElementConfig) {
29
63
  if (config.target instanceof HTMLElement) {