@ng-zen/cli 22.0.0 → 22.1.0-next.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.
@@ -1,43 +1,36 @@
1
- import {
2
- afterNextRender,
3
- ChangeDetectionStrategy,
4
- Component,
5
- computed,
6
- DestroyRef,
7
- inject,
8
- input,
9
- model,
10
- } from '@angular/core';
11
- import { FormsModule } from '@angular/forms';
1
+ import { afterNextRender, Component, computed, DestroyRef, inject, input, model } from '@angular/core';
12
2
 
13
- import { ZenFormControl, ZenFormControlProvider } from '../form-control';
3
+ import { ZenFormControl } from '../form-control';
14
4
  import { ZenRadioRegistry } from './radio.registry';
15
5
 
16
6
  /**
17
- * ZenRadio is a reusable radio button component designed to provide
18
- * a consistent and customizable radio button style across the application.
19
- * It supports Angular forms integration and provides two-way data binding
20
- * for string values.
7
+ * ZenRadio is a radio button component backed by Signal Forms.
21
8
  *
22
- * @example
23
- * <zen-radio name="group" option="option1" [(ngModel)]="selectedValue" /> Option 1
24
- * <zen-radio name="group" option="option2" [(ngModel)]="selectedValue" /> Option 2
9
+ * Radio buttons with the same `name` form a group. The `value` is shared
10
+ * across the group — when one radio is selected, `onInput(value)` is called
11
+ * on every radio in the group via {@link ZenRadioRegistry}.
12
+ *
13
+ * Use together with a shared `[formField]` binding:
14
+ *
15
+ * ```html
16
+ * <zen-radio [formField]="form.color" name="color" option="red" />
17
+ * <zen-radio [formField]="form.color" name="color" option="green" />
18
+ * ```
25
19
  *
26
20
  * ### CSS Custom Properties
27
21
  *
28
- * You can customize the component using CSS custom properties:
29
22
  *
30
23
  * ```css
31
24
  * :root {
32
- * --zen-radio-size: 1rem;
33
- * --zen-radio-border-radius: 50%;
34
- * --zen-radio-appearance: hsl(0deg 0% 10%);
35
- * --zen-radio-disabled-opacity: 0.6;
36
- * --zen-radio-border: 1px solid hsl(0deg 0% 80%);
25
+ * --zen-radio-size: 1rem;
26
+ * --zen-radio-border-radius: 50%;
27
+ * --zen-radio-appearance: hsl(0deg 0% 10%);
28
+ * --zen-radio-disabled-opacity: 0.6;
29
+ * --zen-radio-border: 1px solid hsl(0deg 0% 80%);
37
30
  * }
38
31
  * ```
39
32
  *
40
- * @implements {ZenFormControl<string | null>}
33
+ * @extends {ZenFormControl<string | null>}
41
34
  *
42
35
  * @author Konrad Stępień
43
36
  * @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
@@ -49,6 +42,7 @@ import { ZenRadioRegistry } from './radio.registry';
49
42
  <input
50
43
  [attr.aria-checked]="checked()"
51
44
  [attr.aria-disabled]="disabled()"
45
+ [attr.aria-invalid]="invalid() || null"
52
46
  [checked]="checked()"
53
47
  [disabled]="disabled()"
54
48
  [name]="name()"
@@ -62,12 +56,6 @@ import { ZenRadioRegistry } from './radio.registry';
62
56
  }
63
57
  `,
64
58
  styleUrls: ['./radio.scss'],
65
- changeDetection: ChangeDetectionStrategy.OnPush,
66
- imports: [FormsModule],
67
- providers: [ZenFormControlProvider(ZenRadio)],
68
- host: {
69
- '(blur)': 'onTouched()',
70
- },
71
59
  })
72
60
  export class ZenRadio extends ZenFormControl<string | null> {
73
61
  /**
@@ -80,7 +68,7 @@ export class ZenRadio extends ZenFormControl<string | null> {
80
68
  * The name attribute for the radio button group.
81
69
  * Radio buttons with the same name will be grouped together.
82
70
  */
83
- readonly name = input.required<string>();
71
+ override readonly name = input.required<string>();
84
72
 
85
73
  /**
86
74
  * The value for this specific radio button.
@@ -1,4 +1,4 @@
1
- import { ChangeDetectionStrategy, Component } from '@angular/core';
1
+ import { Component } from '@angular/core';
2
2
 
3
3
  /**
4
4
  * ZenInput loader component that displays loading states.
@@ -31,6 +31,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
31
31
  selector: 'zen-skeleton, zen-skeleton[rounded]',
32
32
  template: ``,
33
33
  styleUrl: './skeleton.scss',
34
- changeDetection: ChangeDetectionStrategy.OnPush,
35
34
  })
36
35
  export class ZenSkeleton {}
@@ -1,5 +1,9 @@
1
+ import { NgComponentOutlet } from '@angular/common';
2
+ import { Component, signal } from '@angular/core';
3
+ import { form, FormField } from '@angular/forms/signals';
1
4
  import { Meta, StoryObj } from '@storybook/angular';
2
5
 
6
+ import FormControlStories from '../form-control/form-control.stories';
3
7
  import { ZenSwitch } from './switch';
4
8
 
5
9
  type Options = ZenSwitch;
@@ -8,42 +12,18 @@ export default {
8
12
  title: 'Ui/Switch',
9
13
  component: ZenSwitch,
10
14
  argTypes: {
15
+ ...FormControlStories.argTypes,
11
16
  value: {
12
- control: 'boolean',
17
+ control: 'boolean' as const,
13
18
  table: {
14
19
  category: 'models',
15
- type: {
16
- summary: 'boolean',
17
- },
18
- },
19
- },
20
- disabled: {
21
- control: 'boolean',
22
- table: {
23
- category: 'models',
24
- type: {
25
- summary: 'boolean',
26
- },
27
- },
28
- },
29
- required: {
30
- control: 'boolean',
31
- table: {
32
- category: 'inputs',
33
- type: {
34
- summary: 'boolean',
35
- },
36
- defaultValue: {
37
- summary: 'false',
38
- },
20
+ type: { summary: 'boolean' },
39
21
  },
40
22
  },
41
23
  onInput: {
42
24
  table: {
43
25
  readonly: true,
44
- type: {
45
- summary: '(value: boolean) => void',
46
- },
26
+ type: { summary: '(value: boolean) => void' },
47
27
  },
48
28
  },
49
29
  onKeyDown: {
@@ -53,12 +33,48 @@ export default {
53
33
  },
54
34
  },
55
35
  args: {
36
+ ...FormControlStories.args,
56
37
  value: false,
57
- disabled: false,
58
- required: false,
59
38
  },
60
39
  } satisfies Meta<Options>;
61
40
 
62
41
  type Story = StoryObj<Options>;
63
42
 
64
43
  export const Default: Story = {};
44
+
45
+ export const WithSignalForm: Story = {
46
+ render: () => ({
47
+ moduleMetadata: { imports: [NgComponentOutlet] },
48
+ props: { component: SwitchSignalFormComponent },
49
+ template: '<ng-container *ngComponentOutlet="component" />',
50
+ }),
51
+ parameters: {
52
+ docs: {
53
+ source: {
54
+ code: `
55
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
56
+ <zen-switch id="notifications" [formField]="form.notifications" />
57
+ <label for="notifications">
58
+ {{ form.notifications().value() ? 'Notifications on' : 'Notifications off' }}
59
+ </label>
60
+ </div>`,
61
+ },
62
+ },
63
+ },
64
+ };
65
+
66
+ @Component({
67
+ standalone: true,
68
+ template: `
69
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
70
+ <zen-switch [formField]="form.notifications" id="notifications" />
71
+ <label for="notifications">
72
+ {{ form.notifications().value() ? 'Notifications on' : 'Notifications off' }}
73
+ </label>
74
+ </div>
75
+ `,
76
+ imports: [FormField, ZenSwitch],
77
+ })
78
+ class SwitchSignalFormComponent {
79
+ readonly form = form(signal({ notifications: false }));
80
+ }
@@ -1,34 +1,36 @@
1
- import { ChangeDetectionStrategy, Component, model } from '@angular/core';
1
+ import { Component, model } from '@angular/core';
2
2
 
3
- import { ZenFormControl, ZenFormControlProvider } from '../form-control';
3
+ import { ZenFormControl } from '../form-control';
4
4
 
5
5
  /**
6
- * ZenInput is a reusable text input component designed to provide
7
- * a consistent and customizable input style across the application.
8
- * It supports Angular forms integration and provides two-way data binding.
6
+ * ZenSwitch is a toggle switch component for boolean values backed by
7
+ * Signal Forms.
9
8
  *
10
- * @example
9
+ * Connect it to a Signal Forms field with `[formField]`:
11
10
  *
12
11
  * ```html
13
- * <zen-switch [disabled]="false" [value]="true" />
14
- *```
12
+ * <zen-switch [formField]="myForm.notifications" />
13
+ * ```
14
+ *
15
+ * Supports keyboard interaction: `Enter`, `Space`, `ArrowRight`, and
16
+ * `ArrowLeft` keys toggle or set the value.
15
17
  *
16
18
  * ### CSS Custom Properties
17
- * You can customize the component using CSS custom properties:
19
+ *
18
20
  *
19
21
  * ```css
20
22
  * :root {
21
- * --zen-input-border: 1px solid hsl(0deg 0% 80%);
22
- * --zen-input-border-radius: 8px;
23
- * --zen-input-padding: 0.5rem 1rem;
24
- * --zen-input-focus-shadow: 0 1px 4px hsl(0deg 0% 60% / 20%) inset;
25
- * --zen-input-placeholder-color: hsl(0deg 0% 60%);
23
+ * --zen-switch-thumb-size: 1rem;
24
+ * --zen-switch-height: 1.25rem;
25
+ * --zen-switch-width: 2rem;
26
+ * --zen-switch-apperance: hsl(0deg 0% 10%);
27
+ * --zen-switch-background: hsl(0deg 0% 80%);
26
28
  * }
27
29
  * ```
28
30
  *
29
- * @implements {ZenFormControl<boolean>}
31
+ * @extends {ZenFormControl<boolean>}
30
32
  *
31
- * @author Konrad Stępień
33
+ * @author Konrad Stepień
32
34
  * @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
33
35
  * @see [GitHub](https://github.com/kstepien3/ng-zen)
34
36
  */
@@ -41,20 +43,18 @@ import { ZenFormControl, ZenFormControlProvider } from '../form-control';
41
43
  </span>
42
44
  `,
43
45
  styleUrl: './switch.scss',
44
- changeDetection: ChangeDetectionStrategy.OnPush,
45
- providers: [ZenFormControlProvider(ZenSwitch)],
46
+
46
47
  host: {
47
48
  tabindex: '0',
48
49
  role: 'switch',
49
50
  '[attr.data-disabled]': 'disabled()',
50
51
  '[attr.data-checked]': 'value()',
51
- '(blur)': 'onTouched()',
52
52
  '(click)': 'onInput(!this.value())',
53
53
  '(keydown)': 'onKeyDown($event)',
54
54
  },
55
55
  })
56
56
  export class ZenSwitch extends ZenFormControl<boolean> {
57
- /** Holds the current checkbox value. */
57
+ /** Holds the current switch value. */
58
58
  readonly value = model(false);
59
59
 
60
60
  /**
@@ -1,4 +1,4 @@
1
- import { ChangeDetectionStrategy, Component } from '@angular/core';
1
+ import { Component } from '@angular/core';
2
2
 
3
3
  /**
4
4
  * ZenTextarea is a reusable textarea component designed to provide
@@ -32,6 +32,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
32
32
  <ng-content />
33
33
  `,
34
34
  styleUrls: ['./textarea.scss'],
35
- changeDetection: ChangeDetectionStrategy.OnPush,
36
35
  })
37
36
  export class ZenTextarea {}
@@ -6,8 +6,21 @@ const schematics_1 = require("@angular-devkit/schematics");
6
6
  const workspace_1 = require("@schematics/angular/utility/workspace");
7
7
  const utils_1 = require("../../utils");
8
8
  const DEFAULT_GENERATION_PATH = 'ui'; // src/app/ui
9
- function uiGenerator({ ui, project, ...options }) {
10
- return async (tree) => {
9
+ const FORM_CONTROL_DEPS = new Set(['input', 'checkbox', 'switch', 'radio']);
10
+ function resolveFormControlDependency(tree, logger, ui, workingDirectory) {
11
+ if (!ui.some(u => FORM_CONTROL_DEPS.has(u)))
12
+ return;
13
+ const fcPath = (0, core_1.normalize)(`${workingDirectory}/form-control/form-control.ts`);
14
+ if (tree.exists(fcPath)) {
15
+ logger.info('ℹ form-control skipped — already exists');
16
+ }
17
+ else {
18
+ ui.push('form-control');
19
+ logger.info('✔ form-control included (auto-included for form components)');
20
+ }
21
+ }
22
+ function uiGenerator({ ui: selected, project, ...options }) {
23
+ return async (tree, context) => {
11
24
  const workspace = await (0, workspace_1.getWorkspace)(tree);
12
25
  const projectName = project || workspace.extensions['defaultProject'];
13
26
  const projectObj = workspace.projects.get(projectName);
@@ -18,6 +31,8 @@ function uiGenerator({ ui, project, ...options }) {
18
31
  options.path = ((0, workspace_1.buildDefaultPath)(projectObj) + '/' + DEFAULT_GENERATION_PATH);
19
32
  }
20
33
  const workingDirectory = (0, core_1.normalize)((0, core_1.join)(options.currentDirectory, options.path));
34
+ const ui = [...selected];
35
+ resolveFormControlDependency(tree, context.logger, ui, workingDirectory);
21
36
  return (0, schematics_1.chain)([...(0, utils_1.applyFileTemplateUtil)(ui, { ...options, path: workingDirectory })]);
22
37
  };
23
38
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/schematics/ui/index.ts"],"names":[],"mappings":";;AASA,kCAkBC;AA3BD,+CAA6D;AAC7D,2DAA+D;AAC/D,qEAAuF;AAEvF,uCAAoD;AAGpD,MAAM,uBAAuB,GAAG,IAAI,CAAC,CAAC,aAAa;AAEnD,SAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,GAAG,OAAO,EAAa;IAChE,OAAO,KAAK,EAAE,IAAU,EAAE,EAAE;QAC1B,MAAM,SAAS,GAAG,MAAM,IAAA,wBAAY,EAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,WAAW,GAAG,OAAO,IAAK,SAAS,CAAC,UAAU,CAAC,gBAAgB,CAAY,CAAC;QAClF,MAAM,UAAU,GAAG,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAEvD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,YAAY,WAAW,2BAA2B,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,OAAO,CAAC,IAAI,GAAG,CAAC,IAAA,4BAAgB,EAAC,UAAU,CAAC,GAAG,GAAG,GAAG,uBAAuB,CAAS,CAAC;QACxF,CAAC;QAED,MAAM,gBAAgB,GAAG,IAAA,gBAAS,EAAC,IAAA,WAAI,EAAC,OAAO,CAAC,gBAAgB,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAEjF,OAAO,IAAA,kBAAK,EAAC,CAAC,GAAG,IAAA,6BAAqB,EAAC,EAAE,EAAE,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,CAAC;IACvF,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { join, normalize, Path } from '@angular-devkit/core';\nimport { chain, Rule, Tree } from '@angular-devkit/schematics';\nimport { buildDefaultPath, getWorkspace } from '@schematics/angular/utility/workspace';\n\nimport { applyFileTemplateUtil } from '../../utils';\nimport { Schema as UiOptions } from './schema';\n\nconst DEFAULT_GENERATION_PATH = 'ui'; // src/app/ui\n\nexport function uiGenerator({ ui, project, ...options }: UiOptions): Rule {\n return async (tree: Tree) => {\n const workspace = await getWorkspace(tree);\n const projectName = project || (workspace.extensions['defaultProject'] as string);\n const projectObj = workspace.projects.get(projectName);\n\n if (!projectObj) {\n throw new Error(`Project \"${projectName}\" not found in workspace.`);\n }\n\n if (options.path === undefined) {\n options.path = (buildDefaultPath(projectObj) + '/' + DEFAULT_GENERATION_PATH) as Path;\n }\n\n const workingDirectory = normalize(join(options.currentDirectory, options.path));\n\n return chain([...applyFileTemplateUtil(ui, { ...options, path: workingDirectory })]);\n };\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/schematics/ui/index.ts"],"names":[],"mappings":";;AA4BA,kCAqBC;AAjDD,+CAA6D;AAC7D,2DAAiF;AACjF,qEAAuF;AAEvF,uCAAoD;AAGpD,MAAM,uBAAuB,GAAG,IAAI,CAAC,CAAC,aAAa;AAEnD,MAAM,iBAAiB,GAAwB,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAEjG,SAAS,4BAA4B,CACnC,IAAU,EACV,MAAkC,EAClC,EAAY,EACZ,gBAAsB;IAEtB,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAAE,OAAO;IAEpD,MAAM,MAAM,GAAG,IAAA,gBAAS,EAAC,GAAG,gBAAgB,+BAA+B,CAAC,CAAC;IAC7E,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;IACzD,CAAC;SAAM,CAAC;QACN,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACxB,MAAM,CAAC,IAAI,CAAC,6DAA6D,CAAC,CAAC;IAC7E,CAAC;AACH,CAAC;AAED,SAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAa;IAC1E,OAAO,KAAK,EAAE,IAAU,EAAE,OAAyB,EAAE,EAAE;QACrD,MAAM,SAAS,GAAG,MAAM,IAAA,wBAAY,EAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,WAAW,GAAG,OAAO,IAAK,SAAS,CAAC,UAAU,CAAC,gBAAgB,CAAY,CAAC;QAClF,MAAM,UAAU,GAAG,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAEvD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,YAAY,WAAW,2BAA2B,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,OAAO,CAAC,IAAI,GAAG,CAAC,IAAA,4BAAgB,EAAC,UAAU,CAAC,GAAG,GAAG,GAAG,uBAAuB,CAAS,CAAC;QACxF,CAAC;QAED,MAAM,gBAAgB,GAAG,IAAA,gBAAS,EAAC,IAAA,WAAI,EAAC,OAAO,CAAC,gBAAgB,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACjF,MAAM,EAAE,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;QAEzB,4BAA4B,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,EAAE,gBAAgB,CAAC,CAAC;QAEzE,OAAO,IAAA,kBAAK,EAAC,CAAC,GAAG,IAAA,6BAAqB,EAAC,EAAE,EAAE,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,CAAC;IACvF,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { join, normalize, Path } from '@angular-devkit/core';\nimport { chain, Rule, SchematicContext, Tree } from '@angular-devkit/schematics';\nimport { buildDefaultPath, getWorkspace } from '@schematics/angular/utility/workspace';\n\nimport { applyFileTemplateUtil } from '../../utils';\nimport { Schema as UiOptions, UiType } from './schema';\n\nconst DEFAULT_GENERATION_PATH = 'ui'; // src/app/ui\n\nconst FORM_CONTROL_DEPS: ReadonlySet<UiType> = new Set(['input', 'checkbox', 'switch', 'radio']);\n\nfunction resolveFormControlDependency(\n tree: Tree,\n logger: SchematicContext['logger'],\n ui: UiType[],\n workingDirectory: Path\n): void {\n if (!ui.some(u => FORM_CONTROL_DEPS.has(u))) return;\n\n const fcPath = normalize(`${workingDirectory}/form-control/form-control.ts`);\n if (tree.exists(fcPath)) {\n logger.info('ℹ form-control skipped — already exists');\n } else {\n ui.push('form-control');\n logger.info('✔ form-control included (auto-included for form components)');\n }\n}\n\nexport function uiGenerator({ ui: selected, project, ...options }: UiOptions): Rule {\n return async (tree: Tree, context: SchematicContext) => {\n const workspace = await getWorkspace(tree);\n const projectName = project || (workspace.extensions['defaultProject'] as string);\n const projectObj = workspace.projects.get(projectName);\n\n if (!projectObj) {\n throw new Error(`Project \"${projectName}\" not found in workspace.`);\n }\n\n if (options.path === undefined) {\n options.path = (buildDefaultPath(projectObj) + '/' + DEFAULT_GENERATION_PATH) as Path;\n }\n\n const workingDirectory = normalize(join(options.currentDirectory, options.path));\n const ui = [...selected];\n\n resolveFormControlDependency(tree, context.logger, ui, workingDirectory);\n\n return chain([...applyFileTemplateUtil(ui, { ...options, path: workingDirectory })]);\n };\n}\n"]}
@@ -1,14 +1,33 @@
1
1
  import { join, normalize, Path } from '@angular-devkit/core';
2
- import { chain, Rule, Tree } from '@angular-devkit/schematics';
2
+ import { chain, Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
3
3
  import { buildDefaultPath, getWorkspace } from '@schematics/angular/utility/workspace';
4
4
 
5
5
  import { applyFileTemplateUtil } from '../../utils';
6
- import { Schema as UiOptions } from './schema';
6
+ import { Schema as UiOptions, UiType } from './schema';
7
7
 
8
8
  const DEFAULT_GENERATION_PATH = 'ui'; // src/app/ui
9
9
 
10
- export function uiGenerator({ ui, project, ...options }: UiOptions): Rule {
11
- return async (tree: Tree) => {
10
+ const FORM_CONTROL_DEPS: ReadonlySet<UiType> = new Set(['input', 'checkbox', 'switch', 'radio']);
11
+
12
+ function resolveFormControlDependency(
13
+ tree: Tree,
14
+ logger: SchematicContext['logger'],
15
+ ui: UiType[],
16
+ workingDirectory: Path
17
+ ): void {
18
+ if (!ui.some(u => FORM_CONTROL_DEPS.has(u))) return;
19
+
20
+ const fcPath = normalize(`${workingDirectory}/form-control/form-control.ts`);
21
+ if (tree.exists(fcPath)) {
22
+ logger.info('ℹ form-control skipped — already exists');
23
+ } else {
24
+ ui.push('form-control');
25
+ logger.info('✔ form-control included (auto-included for form components)');
26
+ }
27
+ }
28
+
29
+ export function uiGenerator({ ui: selected, project, ...options }: UiOptions): Rule {
30
+ return async (tree: Tree, context: SchematicContext) => {
12
31
  const workspace = await getWorkspace(tree);
13
32
  const projectName = project || (workspace.extensions['defaultProject'] as string);
14
33
  const projectObj = workspace.projects.get(projectName);
@@ -22,6 +41,9 @@ export function uiGenerator({ ui, project, ...options }: UiOptions): Rule {
22
41
  }
23
42
 
24
43
  const workingDirectory = normalize(join(options.currentDirectory, options.path));
44
+ const ui = [...selected];
45
+
46
+ resolveFormControlDependency(tree, context.logger, ui, workingDirectory);
25
47
 
26
48
  return chain([...applyFileTemplateUtil(ui, { ...options, path: workingDirectory })]);
27
49
  };
@@ -43,7 +43,6 @@
43
43
  "checkbox",
44
44
  "dialog",
45
45
  "divider",
46
- "form-control",
47
46
  "icon",
48
47
  "input",
49
48
  "popover",