@ng-zen/cli 22.9.0-next.1 → 22.10.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.
Files changed (41) hide show
  1. package/package.json +3 -1
  2. package/schematics/ui/files/checkbox/checkbox.html +36 -0
  3. package/schematics/ui/files/checkbox/checkbox.scss +54 -31
  4. package/schematics/ui/files/checkbox/checkbox.spec.ts +94 -2
  5. package/schematics/ui/files/checkbox/checkbox.stories.ts +8 -28
  6. package/schematics/ui/files/checkbox/checkbox.ts +8 -21
  7. package/schematics/ui/files/form-control/form-control.spec.ts +18 -1
  8. package/schematics/ui/files/form-control/form-control.stories.ts +19 -4
  9. package/schematics/ui/files/form-control/form-control.ts +30 -4
  10. package/schematics/ui/files/hint/hint.scss +48 -0
  11. package/schematics/ui/files/hint/hint.spec.ts +54 -0
  12. package/schematics/ui/files/hint/hint.stories.ts +57 -0
  13. package/schematics/ui/files/hint/hint.ts +48 -0
  14. package/schematics/ui/files/hint/index.ts +1 -0
  15. package/schematics/ui/files/input/input.html +29 -0
  16. package/schematics/ui/files/input/input.scss +2 -1
  17. package/schematics/ui/files/input/input.spec.ts +107 -2
  18. package/schematics/ui/files/input/input.stories.ts +23 -16
  19. package/schematics/ui/files/input/input.ts +7 -14
  20. package/schematics/ui/files/label/index.ts +1 -0
  21. package/schematics/ui/files/label/label.html +6 -0
  22. package/schematics/ui/files/label/label.scss +52 -0
  23. package/schematics/ui/files/label/label.spec.ts +77 -0
  24. package/schematics/ui/files/label/label.stories.ts +67 -0
  25. package/schematics/ui/files/label/label.ts +57 -0
  26. package/schematics/ui/files/radio/radio.html +34 -0
  27. package/schematics/ui/files/radio/radio.scss +40 -27
  28. package/schematics/ui/files/radio/radio.spec.ts +40 -0
  29. package/schematics/ui/files/radio/radio.stories.ts +12 -25
  30. package/schematics/ui/files/radio/radio.ts +8 -20
  31. package/schematics/ui/files/switch/switch.html +31 -0
  32. package/schematics/ui/files/switch/switch.scss +34 -2
  33. package/schematics/ui/files/switch/switch.spec.ts +102 -2
  34. package/schematics/ui/files/switch/switch.stories.ts +12 -13
  35. package/schematics/ui/files/switch/switch.ts +7 -17
  36. package/schematics/ui/index.js +18 -8
  37. package/schematics/ui/index.js.map +1 -1
  38. package/schematics/ui/index.ts +19 -7
  39. package/schematics/ui/schema.js.map +1 -1
  40. package/schematics/ui/schema.json +2 -0
  41. package/schematics/ui/schema.ts +2 -0
@@ -12,60 +12,64 @@ $disabled-opacity: var(--zen-disabled-opacity, 0.6);
12
12
  --zen-radio-border: 1px solid hsl(0deg 0% 80%);
13
13
  }
14
14
 
15
- input {
16
- position: absolute;
17
- cursor: pointer;
18
- opacity: 0;
19
- height: 100%;
20
- width: 100%;
21
- padding: 0;
22
- margin: 0;
23
- top: 0;
24
- left: 0;
25
- }
26
-
27
15
  /* stylelint-disable-next-line no-duplicate-selectors -- separate variables and styles */
28
16
  :host {
17
+ display: inline-grid;
18
+ grid-template-columns: var(--zen-radio-size) 1fr;
19
+ gap: 0.25rem 0.5rem;
20
+ align-items: center;
21
+
22
+ &:has(input[type='radio']:disabled) {
23
+ opacity: $disabled-opacity;
24
+
25
+ &,
26
+ input,
27
+ label {
28
+ cursor: not-allowed;
29
+ }
30
+ }
31
+ }
32
+
33
+ input[type='radio'] {
34
+ appearance: none;
35
+ grid-column: 1;
36
+ grid-row: 1;
29
37
  border: var(--zen-radio-border);
30
38
  border-radius: var(--zen-radio-border-radius);
31
39
  height: var(--zen-radio-size);
32
40
  width: var(--zen-radio-size);
33
41
  background-color: white;
34
42
  cursor: pointer;
35
- position: relative;
43
+ margin: 0;
44
+ padding: 0;
36
45
  transition:
37
46
  background-color ease,
38
47
  border-color ease;
39
48
  transition-duration: $transition-duration;
40
- user-select: none;
41
- justify-content: center;
42
- display: grid;
43
- place-items: center;
44
49
 
45
- &:has(input:checked) {
50
+ &:checked {
46
51
  border-color: var(--zen-radio-appearance);
47
52
  }
48
53
 
49
- &:has(input[type='radio']:disabled) {
50
- opacity: $disabled-opacity;
51
-
52
- &,
53
- input {
54
- cursor: not-allowed;
55
- }
54
+ &:disabled {
55
+ cursor: not-allowed;
56
56
  }
57
57
 
58
- &:has(input:focus-visible) {
58
+ &:focus-visible {
59
59
  outline: $outline;
60
60
  box-shadow: $focus-shadow;
61
61
  }
62
62
 
63
- &:has(input:user-invalid) {
63
+ &:user-invalid {
64
64
  border-color: $error;
65
65
  }
66
66
  }
67
67
 
68
68
  .radio-dot {
69
+ grid-column: 1;
70
+ grid-row: 1;
71
+ place-self: center center;
72
+ pointer-events: none;
69
73
  width: calc(var(--zen-radio-size) * 0.4);
70
74
  height: calc(var(--zen-radio-size) * 0.4);
71
75
  background-color: var(--zen-radio-appearance);
@@ -84,3 +88,12 @@ input {
84
88
  opacity: 1;
85
89
  }
86
90
  }
91
+
92
+ zen-label {
93
+ grid-column: 2;
94
+ grid-row: 1;
95
+ }
96
+
97
+ zen-hint {
98
+ grid-column: 1 / -1;
99
+ }
@@ -45,6 +45,46 @@ describe('ZenRadio', () => {
45
45
  expect(inputElement.checked).toBe(false);
46
46
  });
47
47
 
48
+ it('should render label linked to radio input id and handle required state', () => {
49
+ fixture.componentRef.setInput('label', 'Option Label');
50
+ fixture.detectChanges();
51
+
52
+ const labelEl = fixture.debugElement.query(By.css('label')).nativeElement as HTMLLabelElement;
53
+ const inputEl = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
54
+
55
+ expect(labelEl).toBeTruthy();
56
+ expect(labelEl.textContent?.trim()).toBe('Option Label');
57
+ expect(labelEl.getAttribute('for')).toBe(inputEl.id);
58
+
59
+ fixture.componentRef.setInput('required', true);
60
+ fixture.detectChanges();
61
+
62
+ const requiredSpan = fixture.debugElement.query(By.css('zen-label .required'))?.nativeElement;
63
+ expect(requiredSpan).toBeTruthy();
64
+ expect(requiredSpan.textContent).toBe('*');
65
+ });
66
+
67
+ it('should render hint, warn, and errors on radio', () => {
68
+ fixture.componentRef.setInput('hint', 'Choose one option');
69
+ fixture.detectChanges();
70
+ let hintEl = fixture.debugElement.query(By.css('zen-hint')).nativeElement;
71
+ expect(hintEl.getAttribute('data-severity')).toBe('neutral');
72
+ expect(hintEl.textContent?.trim()).toBe('Choose one option');
73
+
74
+ fixture.componentRef.setInput('warn', 'Selection cannot be changed');
75
+ fixture.detectChanges();
76
+ hintEl = fixture.debugElement.query(By.css('zen-hint')).nativeElement;
77
+ expect(hintEl.getAttribute('data-severity')).toBe('warning');
78
+
79
+ fixture.componentRef.setInput('invalid', true);
80
+ fixture.componentRef.setInput('touched', true);
81
+ fixture.componentRef.setInput('errors', [{ message: 'Please select an option' }]);
82
+ fixture.detectChanges();
83
+ hintEl = fixture.debugElement.query(By.css('zen-hint')).nativeElement;
84
+ expect(hintEl.getAttribute('data-severity')).toBe('error');
85
+ expect(hintEl.textContent?.trim()).toBe('Please select an option');
86
+ });
87
+
48
88
  it('should show radio dot when checked', () => {
49
89
  component.value.set('test-value');
50
90
  fixture.detectChanges();
@@ -56,13 +56,12 @@ type Story = StoryObj<Options>;
56
56
  export const Default: Story = {};
57
57
 
58
58
  export const WithLabel: Story = {
59
- render: () => ({
60
- template: `
61
- <div style="display: flex; align-items: center; gap: 0.25rem">
62
- <zen-radio name="label-example" option="option1" />
63
- <label for="label-example"> With label </label>
64
- </div>
65
- `,
59
+ args: {
60
+ label: 'Option 1',
61
+ },
62
+ render: args => ({
63
+ props: args,
64
+ template: '<zen-radio name="label-example" option="option1" [label]="label" />',
66
65
  }),
67
66
  };
68
67
 
@@ -75,9 +74,9 @@ export const WithSignalForm: Story = {
75
74
  parameters: {
76
75
  docs: {
77
76
  source: {
78
- code: `<zen-radio [formField]="form.color" option="red" />
79
- <zen-radio [formField]="form.color" option="green" />
80
- <zen-radio [formField]="form.color" option="blue" />`,
77
+ code: `<zen-radio [formField]="form.color" option="red" label="Red" />
78
+ <zen-radio [formField]="form.color" option="green" label="Green" />
79
+ <zen-radio [formField]="form.color" option="blue" label="Blue" />`,
81
80
  },
82
81
  },
83
82
  },
@@ -93,21 +92,9 @@ export const WithSignalForm: Story = {
93
92
  {{ form.color().value() }}
94
93
  </div>
95
94
  <div style="display: flex; flex-direction: column; gap: 0.5rem">
96
- <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -->
97
- <label style="display: flex; align-items: center; gap: 0.25rem">
98
- <zen-radio option="red" [formField]="form.color" />
99
- Red
100
- </label>
101
- <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -->
102
- <label style="display: flex; align-items: center; gap: 0.25rem">
103
- <zen-radio option="green" [formField]="form.color" />
104
- Green
105
- </label>
106
- <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -->
107
- <label style="display: flex; align-items: center; gap: 0.25rem">
108
- <zen-radio option="blue" [formField]="form.color" />
109
- Blue
110
- </label>
95
+ <zen-radio label="Red" option="red" [formField]="form.color" />
96
+ <zen-radio label="Green" option="green" [formField]="form.color" />
97
+ <zen-radio label="Blue" option="blue" [formField]="form.color" />
111
98
  </div>
112
99
  </div>
113
100
  `,
@@ -1,6 +1,9 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
1
2
  import { afterNextRender, Component, computed, DestroyRef, inject, input, model } from '@angular/core';
2
3
 
3
4
  import { ZenFormControl } from '../form-control';
5
+ import { ZenHint } from '../hint';
6
+ import { ZenLabel } from '../label';
4
7
  import { ZenRadioRegistry } from './radio.registry';
5
8
 
6
9
  /**
@@ -13,8 +16,8 @@ import { ZenRadioRegistry } from './radio.registry';
13
16
  * Use together with a shared `[formField]` binding:
14
17
  *
15
18
  * ```html
16
- * <zen-radio [formField]="form.color" name="color" option="red" />
17
- * <zen-radio [formField]="form.color" name="color" option="green" />
19
+ * <zen-radio [formField]="form.color" name="color" option="red" label="Red" />
20
+ * <zen-radio [formField]="form.color" name="color" option="green" label="Green" />
18
21
  * ```
19
22
  *
20
23
  * ### CSS Custom Properties
@@ -38,24 +41,9 @@ import { ZenRadioRegistry } from './radio.registry';
38
41
  */
39
42
  @Component({
40
43
  selector: 'zen-radio',
41
- template: `
42
- <input
43
- #inputElement
44
- type="radio"
45
- [attr.aria-checked]="checked()"
46
- [attr.aria-disabled]="disabled()"
47
- [attr.aria-invalid]="invalid() || null"
48
- [checked]="checked()"
49
- [disabled]="disabled()"
50
- [name]="name()"
51
- [value]="option()"
52
- (change)="onRadioChange()"
53
- />
54
- @if (checked()) {
55
- <span class="radio-dot"></span>
56
- }
57
- `,
58
- styleUrls: ['./radio.scss'],
44
+ imports: [NgTemplateOutlet, ZenHint, ZenLabel],
45
+ templateUrl: './radio.html',
46
+ styleUrl: './radio.scss',
59
47
  })
60
48
  export class ZenRadio extends ZenFormControl<string | null> {
61
49
  /**
@@ -0,0 +1,31 @@
1
+ <button
2
+ role="switch"
3
+ type="button"
4
+ [attr.aria-checked]="value()"
5
+ [attr.data-checked]="value()"
6
+ [attr.data-disabled]="disabled()"
7
+ [disabled]="disabled()"
8
+ [id]="id()"
9
+ (click)="onInput(!value())"
10
+ (keydown)="onKeyDown($event)"
11
+ >
12
+ <span class="thumb"></span>
13
+ </button>
14
+ @if (label()) {
15
+ <zen-label [disabled]="disabled()" [for]="id()" [required]="required()">{{ label() }}</zen-label>
16
+ }
17
+ @if (shouldShowError()) {
18
+ @for (error of errors(); track $index) {
19
+ <zen-hint severity="error">
20
+ @if (errorTemplate()) {
21
+ <ng-container *ngTemplateOutlet="errorTemplate()!; context: { $implicit: error }" />
22
+ } @else {
23
+ {{ error.message }}
24
+ }
25
+ </zen-hint>
26
+ }
27
+ } @else if (warn()) {
28
+ <zen-hint severity="warning">{{ warn() }}</zen-hint>
29
+ } @else if (hint()) {
30
+ <zen-hint>{{ hint() }}</zen-hint>
31
+ }
@@ -16,17 +16,39 @@ $disabled-opacity: var(--zen-disabled-opacity, 0.6);
16
16
 
17
17
  /* stylelint-disable-next-line no-duplicate-selectors -- separate variables and styles */
18
18
  :host {
19
+ display: inline-grid;
20
+ grid-template-columns: var(--zen-switch-width) 1fr;
21
+ gap: 0.25rem 0.5rem;
22
+ align-items: center;
23
+
24
+ &:has(button:disabled) {
25
+ opacity: $disabled-opacity;
26
+
27
+ &,
28
+ button,
29
+ label {
30
+ cursor: not-allowed;
31
+ }
32
+ }
33
+ }
34
+
35
+ button[role='switch'] {
19
36
  $thumb-x-center-position: calc(var(--zen-switch-height) / 2 - var(--zen-switch-thumb-size) / 2);
20
37
 
38
+ grid-column: 1;
39
+ grid-row: 1;
21
40
  background: var(--zen-switch-background);
22
41
  height: var(--zen-switch-height);
23
42
  width: var(--zen-switch-width);
24
43
  cursor: pointer;
44
+ border: none;
25
45
  border-radius: 9999px;
26
46
  position: relative;
27
47
  display: flex;
28
48
  place-items: center;
29
49
  user-select: none;
50
+ padding: 0;
51
+ margin: 0;
30
52
 
31
53
  // Animation
32
54
  transition-property: background-color, border-color;
@@ -72,12 +94,22 @@ $disabled-opacity: var(--zen-disabled-opacity, 0.6);
72
94
  }
73
95
  }
74
96
 
75
- &[data-disabled='true'] {
97
+ &[data-disabled='true'],
98
+ &:disabled {
76
99
  cursor: not-allowed;
77
100
  opacity: $disabled-opacity;
78
101
  }
79
102
 
80
- &:active:not([data-disabled='true']) .thumb {
103
+ &:active:not(:disabled) .thumb {
81
104
  width: calc(var(--zen-switch-thumb-size) + 2px);
82
105
  }
83
106
  }
107
+
108
+ zen-label {
109
+ grid-column: 2;
110
+ grid-row: 1;
111
+ }
112
+
113
+ zen-hint {
114
+ grid-column: 1 / -1;
115
+ }
@@ -1,13 +1,39 @@
1
- import { provideZonelessChangeDetection } from '@angular/core';
1
+ import { Component, provideZonelessChangeDetection, signal } from '@angular/core';
2
2
  import { TestBed } from '@angular/core/testing';
3
+ import type { ValidationError } from '@angular/forms/signals';
4
+ import { By } from '@angular/platform-browser';
3
5
  import { beforeEach, describe, expect, it } from 'vitest';
4
6
 
5
7
  import { ZenSwitch } from './switch';
6
8
 
9
+ @Component({
10
+ imports: [ZenSwitch],
11
+ template: `
12
+ <zen-switch
13
+ [errors]="errors()"
14
+ [hint]="hint()"
15
+ [invalid]="invalid()"
16
+ [label]="label()"
17
+ [required]="required()"
18
+ [touched]="touched()"
19
+ [warn]="warn()"
20
+ />
21
+ `,
22
+ })
23
+ class TestSwitchHostComponent {
24
+ readonly label = signal('');
25
+ readonly hint = signal('');
26
+ readonly warn = signal('');
27
+ readonly invalid = signal(false);
28
+ readonly touched = signal(false);
29
+ readonly required = signal(false);
30
+ readonly errors = signal<ValidationError.WithOptionalFieldTree[]>([]);
31
+ }
32
+
7
33
  describe('ZenSwitch', () => {
8
34
  beforeEach(async () => {
9
35
  await TestBed.configureTestingModule({
10
- imports: [ZenSwitch],
36
+ imports: [ZenSwitch, TestSwitchHostComponent],
11
37
  providers: [provideZonelessChangeDetection()],
12
38
  }).compileComponents();
13
39
  });
@@ -17,4 +43,78 @@ describe('ZenSwitch', () => {
17
43
  const component = fixture.componentInstance;
18
44
  expect(component).toBeTruthy();
19
45
  });
46
+
47
+ it('should render label linked to switch button id and handle required state', () => {
48
+ const fixture = TestBed.createComponent(TestSwitchHostComponent);
49
+ fixture.componentInstance.label.set('Enable feature');
50
+ fixture.detectChanges();
51
+
52
+ const labelEl = fixture.nativeElement.querySelector('label') as HTMLLabelElement;
53
+ const buttonEl = fixture.nativeElement.querySelector('button[role="switch"]') as HTMLButtonElement;
54
+
55
+ expect(labelEl).toBeTruthy();
56
+ expect(labelEl.textContent?.trim()).toBe('Enable feature');
57
+ expect(labelEl.getAttribute('for')).toBe(buttonEl.id);
58
+
59
+ fixture.componentInstance.required.set(true);
60
+ fixture.detectChanges();
61
+
62
+ const requiredSpan = fixture.nativeElement.querySelector('zen-label .required');
63
+ expect(requiredSpan).toBeTruthy();
64
+ expect(requiredSpan.textContent).toBe('*');
65
+ });
66
+
67
+ it('should toggle value on click', () => {
68
+ const fixture = TestBed.createComponent(ZenSwitch);
69
+ fixture.detectChanges();
70
+
71
+ const buttonEl = fixture.nativeElement.querySelector('button') as HTMLButtonElement;
72
+ expect(fixture.componentInstance.value()).toBe(false);
73
+
74
+ buttonEl.click();
75
+ expect(fixture.componentInstance.value()).toBe(true);
76
+
77
+ buttonEl.click();
78
+ expect(fixture.componentInstance.value()).toBe(false);
79
+ });
80
+
81
+ it('should handle keyboard interaction', () => {
82
+ const fixture = TestBed.createComponent(ZenSwitch);
83
+ fixture.detectChanges();
84
+
85
+ const component = fixture.componentInstance;
86
+ component.onKeyDown(new KeyboardEvent('keydown', { code: 'Space' }));
87
+ expect(component.value()).toBe(true);
88
+
89
+ component.onKeyDown(new KeyboardEvent('keydown', { code: 'ArrowLeft' }));
90
+ expect(component.value()).toBe(false);
91
+
92
+ component.onKeyDown(new KeyboardEvent('keydown', { code: 'ArrowRight' }));
93
+ expect(component.value()).toBe(true);
94
+
95
+ component.onKeyDown(new KeyboardEvent('keydown', { code: 'Enter' }));
96
+ expect(component.value()).toBe(false);
97
+ });
98
+
99
+ it('should render hint, warn, and errors', () => {
100
+ const fixture = TestBed.createComponent(TestSwitchHostComponent);
101
+ fixture.componentInstance.hint.set('Informational hint');
102
+ fixture.detectChanges();
103
+
104
+ let hintEl = fixture.debugElement.query(By.css('zen-hint')).nativeElement;
105
+ expect(hintEl.getAttribute('data-severity')).toBe('neutral');
106
+
107
+ fixture.componentInstance.warn.set('Warning text');
108
+ fixture.detectChanges();
109
+ hintEl = fixture.debugElement.query(By.css('zen-hint')).nativeElement;
110
+ expect(hintEl.getAttribute('data-severity')).toBe('warning');
111
+
112
+ fixture.componentInstance.invalid.set(true);
113
+ fixture.componentInstance.touched.set(true);
114
+ fixture.componentInstance.errors.set([{ kind: 'required', message: 'Switch must be enabled' }]);
115
+ fixture.detectChanges();
116
+ hintEl = fixture.debugElement.query(By.css('zen-hint')).nativeElement;
117
+ expect(hintEl.getAttribute('data-severity')).toBe('error');
118
+ expect(hintEl.textContent?.trim()).toBe('Switch must be enabled');
119
+ });
20
120
  });
@@ -42,6 +42,16 @@ type Story = StoryObj<Options>;
42
42
 
43
43
  export const Default: Story = {};
44
44
 
45
+ export const WithLabel: Story = {
46
+ args: {
47
+ label: 'Enable email notifications',
48
+ },
49
+ render: args => ({
50
+ props: args,
51
+ template: '<zen-switch [label]="label" />',
52
+ }),
53
+ };
54
+
45
55
  export const WithSignalForm: Story = {
46
56
  render: () => ({
47
57
  moduleMetadata: { imports: [NgComponentOutlet] },
@@ -51,13 +61,7 @@ export const WithSignalForm: Story = {
51
61
  parameters: {
52
62
  docs: {
53
63
  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>`,
64
+ code: `<zen-switch label="Notifications" [formField]="form.notifications" />`,
61
65
  },
62
66
  },
63
67
  },
@@ -66,12 +70,7 @@ export const WithSignalForm: Story = {
66
70
  @Component({
67
71
  standalone: true,
68
72
  template: `
69
- <div style="display: flex; align-items: center; gap: 0.5rem;">
70
- <zen-switch id="notifications" [formField]="form.notifications" />
71
- <label for="notifications">
72
- {{ form.notifications().value() ? 'Notifications on' : 'Notifications off' }}
73
- </label>
74
- </div>
73
+ <zen-switch label="Notifications" [formField]="form.notifications" />
75
74
  `,
76
75
  imports: [FormField, ZenSwitch],
77
76
  })
@@ -1,6 +1,9 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
1
2
  import { Component, model } from '@angular/core';
2
3
 
3
4
  import { ZenFormControl } from '../form-control';
5
+ import { ZenHint } from '../hint';
6
+ import { ZenLabel } from '../label';
4
7
 
5
8
  /**
6
9
  * ZenSwitch is a toggle switch component for boolean values backed by
@@ -9,7 +12,7 @@ import { ZenFormControl } from '../form-control';
9
12
  * Connect it to a Signal Forms field with `[formField]`:
10
13
  *
11
14
  * ```html
12
- * <zen-switch [formField]="myForm.notifications" />
15
+ * <zen-switch [formField]="myForm.notifications" label="Notifications" />
13
16
  * ```
14
17
  *
15
18
  * Supports keyboard interaction: `Enter`, `Space`, `ArrowRight`, and
@@ -30,28 +33,15 @@ import { ZenFormControl } from '../form-control';
30
33
  *
31
34
  * @extends {ZenFormControl<boolean>}
32
35
  *
33
- * @author Konrad Stepień
36
+ * @author Konrad Stępień
34
37
  * @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
35
38
  * @see [GitHub](https://github.com/kstepien3/ng-zen)
36
39
  */
37
40
  @Component({
38
41
  selector: 'zen-switch',
39
- template: `
40
- <span class="thumb">
41
- <!-- @if (value()) { ✓ } -->
42
- <!-- @if (!value()) { ─ } -->
43
- </span>
44
- `,
42
+ imports: [NgTemplateOutlet, ZenHint, ZenLabel],
43
+ templateUrl: './switch.html',
45
44
  styleUrl: './switch.scss',
46
-
47
- host: {
48
- tabindex: '0',
49
- role: 'switch',
50
- '[attr.data-disabled]': 'disabled()',
51
- '[attr.data-checked]': 'value()',
52
- '(click)': 'onInput(!this.value())',
53
- '(keydown)': 'onKeyDown($event)',
54
- },
55
45
  })
56
46
  export class ZenSwitch extends ZenFormControl<boolean> {
57
47
  /** Holds the current switch value. */
@@ -7,16 +7,25 @@ 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
9
  const FORM_CONTROL_DEPS = new Set(['input', 'checkbox', 'switch', 'radio']);
10
+ const FORM_HELPER_COMPONENTS = [
11
+ { name: 'form-control', file: 'form-control/form-control.ts' },
12
+ { name: 'hint', file: 'hint/hint.ts' },
13
+ { name: 'label', file: 'label/label.ts' },
14
+ ];
10
15
  function resolveFormControlDependency(tree, logger, ui, workingDirectory) {
11
16
  if (!ui.some(u => FORM_CONTROL_DEPS.has(u)))
12
17
  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)');
18
+ for (const { name, file } of FORM_HELPER_COMPONENTS) {
19
+ if (ui.includes(name))
20
+ continue;
21
+ const targetPath = (0, core_1.normalize)(`${workingDirectory}/${file}`);
22
+ if (tree.exists(targetPath)) {
23
+ logger.info(`ℹ ${name} skipped — already exists`);
24
+ }
25
+ else {
26
+ ui.push(name);
27
+ logger.info(`✔ ${name} included (auto-included for form components)`);
28
+ }
20
29
  }
21
30
  }
22
31
  function uiGenerator({ ui: selected, project, ...options }) {
@@ -31,7 +40,8 @@ function uiGenerator({ ui: selected, project, ...options }) {
31
40
  const workingDirectory = (0, core_1.normalize)((0, core_1.join)(options.currentDirectory, options.path));
32
41
  const ui = [...selected];
33
42
  resolveFormControlDependency(tree, context.logger, ui, workingDirectory);
34
- return (0, schematics_1.chain)([...(0, utils_1.applyFileTemplateUtil)(ui, { ...options, path: workingDirectory })]);
43
+ const deduplicatedUi = Array.from(new Set(ui));
44
+ return (0, schematics_1.chain)([...(0, utils_1.applyFileTemplateUtil)(deduplicatedUi, { ...options, path: workingDirectory })]);
35
45
  };
36
46
  }
37
47
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/schematics/ui/index.ts"],"names":[],"mappings":";;AA4BA,kCAmBC;AA/CD,+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,OAAO,CAAC,IAAI,KAAK,CAAC,IAAA,4BAAgB,EAAC,UAAU,CAAC,GAAG,GAAG,GAAG,uBAAuB,CAAS,CAAC;QAExF,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 options.path ??= (buildDefaultPath(projectObj) + '/' + DEFAULT_GENERATION_PATH) as Path;\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
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/schematics/ui/index.ts"],"names":[],"mappings":";;AAsCA,kCAqBC;AA3DD,+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,MAAM,sBAAsB,GAA8C;IACxE,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,8BAA8B,EAAE;IAC9D,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE;IACtC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE;CAC1C,CAAC;AAEF,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,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,sBAAsB,EAAE,CAAC;QACpD,IAAI,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,SAAS;QAEhC,MAAM,UAAU,GAAG,IAAA,gBAAS,EAAC,GAAG,gBAAgB,IAAI,IAAI,EAAE,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5B,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,2BAA2B,CAAC,CAAC;QACpD,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACd,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,+CAA+C,CAAC,CAAC;QACxE,CAAC;IACH,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,OAAO,CAAC,IAAI,KAAK,CAAC,IAAA,4BAAgB,EAAC,UAAU,CAAC,GAAG,GAAG,GAAG,uBAAuB,CAAS,CAAC;QAExF,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,MAAM,cAAc,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QAE/C,OAAO,IAAA,kBAAK,EAAC,CAAC,GAAG,IAAA,6BAAqB,EAAC,cAAc,EAAE,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,CAAC;IACnG,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\nconst FORM_HELPER_COMPONENTS: readonly { name: UiType; file: string }[] = [\n { name: 'form-control', file: 'form-control/form-control.ts' },\n { name: 'hint', file: 'hint/hint.ts' },\n { name: 'label', file: 'label/label.ts' },\n];\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 for (const { name, file } of FORM_HELPER_COMPONENTS) {\n if (ui.includes(name)) continue;\n\n const targetPath = normalize(`${workingDirectory}/${file}`);\n if (tree.exists(targetPath)) {\n logger.info(`ℹ ${name} skipped — already exists`);\n } else {\n ui.push(name);\n logger.info(`✔ ${name} included (auto-included for form components)`);\n }\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 options.path ??= (buildDefaultPath(projectObj) + '/' + DEFAULT_GENERATION_PATH) as Path;\n\n const workingDirectory = normalize(join(options.currentDirectory, options.path));\n const ui = [...selected];\n\n resolveFormControlDependency(tree, context.logger, ui, workingDirectory);\n\n const deduplicatedUi = Array.from(new Set(ui));\n\n return chain([...applyFileTemplateUtil(deduplicatedUi, { ...options, path: workingDirectory })]);\n };\n}\n"]}