@ng-zen/cli 22.0.0 → 22.1.0-next.2
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 +2 -16
- package/README.md +18 -17
- package/package.json +3 -3
- package/schematics/ui/files/alert/alert.scss +1 -1
- package/schematics/ui/files/alert/alert.ts +1 -2
- package/schematics/ui/files/avatar/avatar.ts +1 -2
- package/schematics/ui/files/button/button.ts +1 -2
- package/schematics/ui/files/card/card.scss +57 -0
- package/schematics/ui/files/card/card.spec.ts +20 -0
- package/schematics/ui/files/card/card.stories.ts +157 -0
- package/schematics/ui/files/card/card.ts +64 -0
- package/schematics/ui/files/card/index.ts +1 -0
- package/schematics/ui/files/checkbox/checkbox.stories.ts +59 -33
- package/schematics/ui/files/checkbox/checkbox.ts +22 -26
- package/schematics/ui/files/dialog/dialog.ts +1 -2
- package/schematics/ui/files/divider/divider.ts +1 -2
- package/schematics/ui/files/form-control/form-control.spec.ts +20 -5
- package/schematics/ui/files/form-control/form-control.stories.ts +58 -36
- package/schematics/ui/files/form-control/form-control.ts +55 -70
- package/schematics/ui/files/icon/icon.ts +1 -2
- package/schematics/ui/files/input/input.stories.ts +84 -34
- package/schematics/ui/files/input/input.ts +27 -24
- package/schematics/ui/files/popover/host/popover-host.ts +1 -2
- package/schematics/ui/files/radio/radio.registry.ts +2 -2
- package/schematics/ui/files/radio/radio.spec.ts +3 -72
- package/schematics/ui/files/radio/radio.stories.ts +60 -112
- package/schematics/ui/files/radio/radio.ts +21 -33
- package/schematics/ui/files/skeleton/skeleton.ts +1 -2
- package/schematics/ui/files/switch/switch.stories.ts +46 -30
- package/schematics/ui/files/switch/switch.ts +20 -20
- package/schematics/ui/files/textarea/textarea.ts +1 -2
- package/schematics/ui/index.js +17 -2
- package/schematics/ui/index.js.map +1 -1
- package/schematics/ui/index.ts +26 -4
- package/schematics/ui/schema.js.map +1 -1
- package/schematics/ui/schema.json +1 -1
- package/schematics/ui/schema.ts +9 -1
- package/utils/apply-file-template.util.js.map +1 -1
- package/utils/apply-file-template.util.ts +2 -2
- package/types/files-config.js +0 -3
- package/types/files-config.js.map +0 -1
- package/types/files-config.ts +0 -7
- package/types/generator-schema-base.js +0 -3
- package/types/generator-schema-base.js.map +0 -1
- package/types/generator-schema-base.ts +0 -8
- package/types/index.js +0 -7
- package/types/index.js.map +0 -1
- package/types/index.ts +0 -3
- package/types/schematics-folder.js +0 -3
- package/types/schematics-folder.js.map +0 -1
- package/types/schematics-folder.ts +0 -3
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { provideZonelessChangeDetection } from '@angular/core';
|
|
2
2
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
3
|
-
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
4
3
|
import { By } from '@angular/platform-browser';
|
|
5
4
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
5
|
|
|
@@ -14,7 +13,7 @@ describe('ZenRadio', () => {
|
|
|
14
13
|
|
|
15
14
|
beforeEach(async () => {
|
|
16
15
|
await TestBed.configureTestingModule({
|
|
17
|
-
imports: [ZenRadio
|
|
16
|
+
imports: [ZenRadio],
|
|
18
17
|
providers: [ZenRadioRegistry, provideZonelessChangeDetection()],
|
|
19
18
|
}).compileComponents();
|
|
20
19
|
|
|
@@ -73,7 +72,7 @@ describe('ZenRadio', () => {
|
|
|
73
72
|
});
|
|
74
73
|
|
|
75
74
|
it('should not call onInput when disabled', () => {
|
|
76
|
-
|
|
75
|
+
fixture.componentRef.setInput('disabled', true);
|
|
77
76
|
fixture.detectChanges();
|
|
78
77
|
|
|
79
78
|
const spy = vi.spyOn(component, 'onInput');
|
|
@@ -85,63 +84,8 @@ describe('ZenRadio', () => {
|
|
|
85
84
|
expect(spy).not.toHaveBeenCalled();
|
|
86
85
|
});
|
|
87
86
|
|
|
88
|
-
it('should integrate with ngModel', () => {
|
|
89
|
-
let selectedValue: string | null = null;
|
|
90
|
-
|
|
91
|
-
const testFixture = TestBed.createComponent(ZenRadio);
|
|
92
|
-
testFixture.componentRef.setInput('name', 'ng-model-group');
|
|
93
|
-
testFixture.componentRef.setInput('option', 'option1');
|
|
94
|
-
testFixture.detectChanges();
|
|
95
|
-
|
|
96
|
-
// Simulate ngModel binding
|
|
97
|
-
testFixture.componentInstance.registerOnChange((value: string | null) => {
|
|
98
|
-
selectedValue = value;
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const inputElement = testFixture.debugElement.query(By.css('input')).nativeElement;
|
|
102
|
-
inputElement.checked = true;
|
|
103
|
-
inputElement.dispatchEvent(new Event('change'));
|
|
104
|
-
|
|
105
|
-
expect(selectedValue).toBe('option1');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should integrate with reactive forms', () => {
|
|
109
|
-
const formControl = new FormControl<string | null>(null);
|
|
110
|
-
const testFixture = TestBed.createComponent(ZenRadio);
|
|
111
|
-
testFixture.componentRef.setInput('name', 'reactive-group');
|
|
112
|
-
testFixture.componentRef.setInput('option', 'option1');
|
|
113
|
-
|
|
114
|
-
// Simulate form control binding
|
|
115
|
-
testFixture.componentInstance.writeValue(formControl.value);
|
|
116
|
-
|
|
117
|
-
testFixture.componentInstance.registerOnChange((value: string | null) => {
|
|
118
|
-
formControl.setValue(value);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
testFixture.detectChanges();
|
|
122
|
-
|
|
123
|
-
const inputElement = testFixture.debugElement.query(By.css('input')).nativeElement;
|
|
124
|
-
inputElement.checked = true;
|
|
125
|
-
inputElement.dispatchEvent(new Event('change'));
|
|
126
|
-
|
|
127
|
-
// The component's onInput method should call the onChange callback
|
|
128
|
-
// which should update form control value
|
|
129
|
-
expect(formControl.value).toBe('option1');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should handle disabled state from form control', () => {
|
|
133
|
-
component.setDisabledState(true);
|
|
134
|
-
fixture.detectChanges();
|
|
135
|
-
|
|
136
|
-
expect(component.disabled()).toBe(true);
|
|
137
|
-
|
|
138
|
-
const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
|
|
139
|
-
expect(inputElement.disabled).toBe(true);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
87
|
it('should have correct ARIA attributes', () => {
|
|
143
88
|
component.value.set('test-value');
|
|
144
|
-
component.disabled.set(false);
|
|
145
89
|
fixture.detectChanges();
|
|
146
90
|
|
|
147
91
|
const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
|
|
@@ -151,7 +95,7 @@ describe('ZenRadio', () => {
|
|
|
151
95
|
|
|
152
96
|
it('should update ARIA attributes when state changes', () => {
|
|
153
97
|
component.value.set('different-value');
|
|
154
|
-
|
|
98
|
+
fixture.componentRef.setInput('disabled', true);
|
|
155
99
|
fixture.detectChanges();
|
|
156
100
|
|
|
157
101
|
const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
|
|
@@ -159,24 +103,12 @@ describe('ZenRadio', () => {
|
|
|
159
103
|
expect(inputElement.getAttribute('aria-disabled')).toBe('true');
|
|
160
104
|
});
|
|
161
105
|
|
|
162
|
-
it('should call onTouched when host element loses focus', () => {
|
|
163
|
-
const onTouchedSpy = vi.fn();
|
|
164
|
-
component.registerOnTouched(onTouchedSpy);
|
|
165
|
-
|
|
166
|
-
const hostElement = fixture.debugElement.nativeElement;
|
|
167
|
-
hostElement.dispatchEvent(new Event('blur'));
|
|
168
|
-
|
|
169
|
-
expect(onTouchedSpy).toHaveBeenCalled();
|
|
170
|
-
});
|
|
171
|
-
|
|
172
106
|
it('should register with radio registry on initialization', () => {
|
|
173
|
-
// Create a new radio to test registry registration
|
|
174
107
|
const newFixture = TestBed.createComponent(ZenRadio);
|
|
175
108
|
newFixture.componentRef.setInput('name', 'registry-test');
|
|
176
109
|
newFixture.componentRef.setInput('option', 'registry-option');
|
|
177
110
|
newFixture.detectChanges();
|
|
178
111
|
|
|
179
|
-
// The radio should be registered after next render
|
|
180
112
|
expect(newFixture.componentInstance).toBeTruthy();
|
|
181
113
|
});
|
|
182
114
|
|
|
@@ -190,7 +122,6 @@ describe('ZenRadio', () => {
|
|
|
190
122
|
|
|
191
123
|
const radio2 = radio2Fixture.componentInstance;
|
|
192
124
|
|
|
193
|
-
// Test registry selection
|
|
194
125
|
registry.select('test-group', 'option2');
|
|
195
126
|
|
|
196
127
|
expect(radio1.value()).toBe('option2');
|
|
@@ -1,87 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NgComponentOutlet } from '@angular/common';
|
|
2
|
+
import { Component, signal } from '@angular/core';
|
|
3
|
+
import { form, FormField } from '@angular/forms/signals';
|
|
2
4
|
import { Meta, StoryObj } from '@storybook/angular';
|
|
3
5
|
|
|
6
|
+
import FormControlStories from '../form-control/form-control.stories';
|
|
4
7
|
import { ZenRadio } from './radio';
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
selectedColor?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type Options = ZenRadio & StoryParams;
|
|
9
|
+
type Options = ZenRadio;
|
|
11
10
|
|
|
12
11
|
export default {
|
|
13
12
|
title: 'Ui/Radio',
|
|
14
13
|
component: ZenRadio,
|
|
15
14
|
argTypes: {
|
|
15
|
+
...FormControlStories.argTypes,
|
|
16
16
|
value: {
|
|
17
17
|
table: {
|
|
18
18
|
category: 'models',
|
|
19
|
-
type: {
|
|
20
|
-
|
|
21
|
-
},
|
|
22
|
-
defaultValue: {
|
|
23
|
-
summary: 'null',
|
|
24
|
-
},
|
|
19
|
+
type: { summary: 'string | null' },
|
|
20
|
+
defaultValue: { summary: 'null' },
|
|
25
21
|
},
|
|
26
|
-
control: 'text',
|
|
22
|
+
control: 'text' as const,
|
|
27
23
|
},
|
|
28
24
|
name: {
|
|
29
25
|
table: {
|
|
30
26
|
category: 'models',
|
|
31
|
-
type: {
|
|
32
|
-
|
|
33
|
-
},
|
|
34
|
-
defaultValue: {
|
|
35
|
-
summary: 'radio',
|
|
36
|
-
},
|
|
27
|
+
type: { summary: 'string' },
|
|
28
|
+
defaultValue: { summary: 'radio' },
|
|
37
29
|
},
|
|
38
|
-
control: 'text',
|
|
30
|
+
control: 'text' as const,
|
|
39
31
|
},
|
|
40
32
|
option: {
|
|
41
33
|
table: {
|
|
42
34
|
category: 'inputs',
|
|
43
|
-
type: {
|
|
44
|
-
summary: 'string',
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
control: 'text',
|
|
48
|
-
},
|
|
49
|
-
disabled: {
|
|
50
|
-
control: 'boolean',
|
|
51
|
-
table: {
|
|
52
|
-
category: 'models',
|
|
53
|
-
type: {
|
|
54
|
-
summary: 'boolean',
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
required: {
|
|
59
|
-
control: 'boolean',
|
|
60
|
-
table: {
|
|
61
|
-
category: 'inputs',
|
|
62
|
-
type: {
|
|
63
|
-
summary: 'boolean',
|
|
64
|
-
},
|
|
65
|
-
defaultValue: {
|
|
66
|
-
summary: 'false',
|
|
67
|
-
},
|
|
35
|
+
type: { summary: 'string' },
|
|
68
36
|
},
|
|
37
|
+
control: 'text' as const,
|
|
69
38
|
},
|
|
70
39
|
onInput: {
|
|
71
40
|
table: {
|
|
72
41
|
readonly: true,
|
|
73
|
-
type: {
|
|
74
|
-
summary: '(value: string | null) => void',
|
|
75
|
-
},
|
|
42
|
+
type: { summary: '(value: string | null) => void' },
|
|
76
43
|
},
|
|
77
44
|
},
|
|
78
45
|
},
|
|
79
46
|
args: {
|
|
47
|
+
...FormControlStories.args,
|
|
80
48
|
value: null,
|
|
81
49
|
name: 'radio-group',
|
|
82
50
|
option: 'option1',
|
|
83
|
-
disabled: false,
|
|
84
|
-
required: false,
|
|
85
51
|
},
|
|
86
52
|
} satisfies Meta<Options>;
|
|
87
53
|
|
|
@@ -100,70 +66,52 @@ export const WithLabel: Story = {
|
|
|
100
66
|
}),
|
|
101
67
|
};
|
|
102
68
|
|
|
103
|
-
export const
|
|
69
|
+
export const WithSignalForm: Story = {
|
|
104
70
|
render: () => ({
|
|
105
|
-
moduleMetadata: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
props: {
|
|
109
|
-
selectedColor: 'blue',
|
|
110
|
-
},
|
|
111
|
-
template: `
|
|
112
|
-
<div style="display: flex; flex-direction: column; gap: 1rem">
|
|
113
|
-
<div>
|
|
114
|
-
<strong>Selected value:</strong>
|
|
115
|
-
<p> {{ selectedColor || 'None' }}</p>
|
|
116
|
-
</div>
|
|
117
|
-
<div style="display: flex; flex-direction: column; gap: 0.5rem">
|
|
118
|
-
<div style="display: flex; align-items: center; gap: 0.25rem">
|
|
119
|
-
<zen-radio name="color-group" option="red" [(ngModel)]="selectedColor" />
|
|
120
|
-
<label>Red</label>
|
|
121
|
-
</div>
|
|
122
|
-
<div style="display: flex; align-items: center; gap: 0.25rem">
|
|
123
|
-
<zen-radio name="color-group" option="green" [(ngModel)]="selectedColor" />
|
|
124
|
-
<label>Green</label>
|
|
125
|
-
</div>
|
|
126
|
-
<div style="display: flex; align-items: center; gap: 0.25rem">
|
|
127
|
-
<zen-radio name="color-group" option="blue" [(ngModel)]="selectedColor" />
|
|
128
|
-
<label>Blue</label>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
`,
|
|
71
|
+
moduleMetadata: { imports: [NgComponentOutlet] },
|
|
72
|
+
props: { component: RadioSignalFormComponent },
|
|
73
|
+
template: '<ng-container *ngComponentOutlet="component" />',
|
|
133
74
|
}),
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
imports: [ReactiveFormsModule],
|
|
141
|
-
},
|
|
142
|
-
props: {
|
|
143
|
-
colorControl: new FormControl('green'),
|
|
75
|
+
parameters: {
|
|
76
|
+
docs: {
|
|
77
|
+
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" />`,
|
|
144
81
|
},
|
|
145
|
-
|
|
146
|
-
<div style="display: flex; flex-direction: column; gap: 1rem">
|
|
147
|
-
<div>
|
|
148
|
-
<strong>Selected value:</strong>
|
|
149
|
-
<p> {{ colorControl.value || 'None' }}</p>
|
|
150
|
-
</div>
|
|
151
|
-
<div style="display: flex; flex-direction: column; gap: 0.5rem">
|
|
152
|
-
<div style="display: flex; align-items: center; gap: 0.25rem">
|
|
153
|
-
<zen-radio name="working-group" option="red" [formControl]="colorControl" />
|
|
154
|
-
<label>Red</label>
|
|
155
|
-
</div>
|
|
156
|
-
<div style="display: flex; align-items: center; gap: 0.25rem">
|
|
157
|
-
<zen-radio name="working-group" option="green" [formControl]="colorControl" />
|
|
158
|
-
<label>Green</label>
|
|
159
|
-
</div>
|
|
160
|
-
<div style="display: flex; align-items: center; gap: 0.25rem">
|
|
161
|
-
<zen-radio name="working-group" option="blue" [formControl]="colorControl" />
|
|
162
|
-
<label>Blue</label>
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
`,
|
|
167
|
-
};
|
|
82
|
+
},
|
|
168
83
|
},
|
|
169
84
|
};
|
|
85
|
+
|
|
86
|
+
@Component({
|
|
87
|
+
standalone: true,
|
|
88
|
+
imports: [FormField, ZenRadio],
|
|
89
|
+
template: `
|
|
90
|
+
<div style="display: flex; flex-direction: column; gap: 1rem">
|
|
91
|
+
<div>
|
|
92
|
+
<strong>Selected:</strong>
|
|
93
|
+
{{ form.color().value() }}
|
|
94
|
+
</div>
|
|
95
|
+
<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 [formField]="form.color" option="red" />
|
|
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 [formField]="form.color" option="green" />
|
|
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 [formField]="form.color" option="blue" />
|
|
109
|
+
Blue
|
|
110
|
+
</label>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
`,
|
|
114
|
+
})
|
|
115
|
+
class RadioSignalFormComponent {
|
|
116
|
+
readonly form = form(signal({ color: 'blue' }));
|
|
117
|
+
}
|
|
@@ -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
|
|
3
|
+
import { ZenFormControl } from '../form-control';
|
|
14
4
|
import { ZenRadioRegistry } from './radio.registry';
|
|
15
5
|
|
|
16
6
|
/**
|
|
17
|
-
* ZenRadio is a
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
-
* @
|
|
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 {
|
|
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 {
|
|
1
|
+
import { Component, model } from '@angular/core';
|
|
2
2
|
|
|
3
|
-
import { ZenFormControl
|
|
3
|
+
import { ZenFormControl } from '../form-control';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
9
|
+
* Connect it to a Signal Forms field with `[formField]`:
|
|
11
10
|
*
|
|
12
11
|
* ```html
|
|
13
|
-
* <zen-switch [
|
|
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
|
-
*
|
|
19
|
+
*
|
|
18
20
|
*
|
|
19
21
|
* ```css
|
|
20
22
|
* :root {
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
* @
|
|
31
|
+
* @extends {ZenFormControl<boolean>}
|
|
30
32
|
*
|
|
31
|
-
* @author Konrad
|
|
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
|
-
|
|
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
|
|
57
|
+
/** Holds the current switch value. */
|
|
58
58
|
readonly value = model(false);
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 {}
|
package/schematics/ui/index.js
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
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
|
}
|