@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,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Component, effect, ElementRef, inject, input, model, untracked } from '@angular/core';
|
|
2
2
|
import { Cancel01Icon } from '@hugeicons/core-free-icons';
|
|
3
3
|
|
|
4
4
|
import { ZenIcon } from '../icon';
|
|
@@ -55,7 +55,6 @@ type DialogSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
|
55
55
|
</div>
|
|
56
56
|
`,
|
|
57
57
|
styleUrl: './dialog.scss',
|
|
58
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
59
58
|
imports: [ZenIcon],
|
|
60
59
|
host: {
|
|
61
60
|
'[attr.aria-label]': 'header()',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Component, input } from '@angular/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* ZenDivider is a reusable divider component that provides a simple way to
|
|
@@ -36,7 +36,6 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
|
|
36
36
|
selector: `zen-divider, zen-divider[vertical],`,
|
|
37
37
|
template: '<ng-content/>',
|
|
38
38
|
styleUrl: './divider.scss',
|
|
39
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
40
39
|
host: {
|
|
41
40
|
'[class]': '"zen-align-"+align()',
|
|
42
41
|
},
|
|
@@ -2,27 +2,42 @@ import { Component, model, provideZonelessChangeDetection } from '@angular/core'
|
|
|
2
2
|
import { TestBed } from '@angular/core/testing';
|
|
3
3
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
4
4
|
|
|
5
|
-
import { ZenFormControl
|
|
5
|
+
import { ZenFormControl } from './form-control';
|
|
6
6
|
|
|
7
7
|
@Component({
|
|
8
8
|
template: '...',
|
|
9
|
-
providers: [ZenFormControlProvider(FormControl)],
|
|
10
9
|
})
|
|
11
|
-
class
|
|
10
|
+
class TestFormControl extends ZenFormControl<string> {
|
|
12
11
|
readonly value = model<string>('');
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
describe('FormControl', () => {
|
|
16
15
|
beforeEach(async () => {
|
|
17
16
|
await TestBed.configureTestingModule({
|
|
18
|
-
imports: [
|
|
17
|
+
imports: [TestFormControl],
|
|
19
18
|
providers: [provideZonelessChangeDetection()],
|
|
20
19
|
}).compileComponents();
|
|
21
20
|
});
|
|
22
21
|
|
|
23
22
|
it('should create', () => {
|
|
24
|
-
const fixture = TestBed.createComponent(
|
|
23
|
+
const fixture = TestBed.createComponent(TestFormControl);
|
|
25
24
|
const component = fixture.componentInstance;
|
|
26
25
|
expect(component).toBeTruthy();
|
|
27
26
|
});
|
|
27
|
+
|
|
28
|
+
it('should have FormUiControl properties', () => {
|
|
29
|
+
const fixture = TestBed.createComponent(TestFormControl);
|
|
30
|
+
const component = fixture.componentInstance;
|
|
31
|
+
expect(component.disabled()).toBe(false);
|
|
32
|
+
expect(component.required()).toBe(false);
|
|
33
|
+
expect(component.touched()).toBe(false);
|
|
34
|
+
expect(component.dirty()).toBe(false);
|
|
35
|
+
expect(component.invalid()).toBe(false);
|
|
36
|
+
expect(component.pending()).toBe(false);
|
|
37
|
+
expect(component.hidden()).toBe(false);
|
|
38
|
+
expect(component.readonly()).toBe(false);
|
|
39
|
+
expect(component.name()).toBe('');
|
|
40
|
+
expect(component.errors()).toEqual([]);
|
|
41
|
+
expect(component.disabledReasons()).toEqual([]);
|
|
42
|
+
});
|
|
28
43
|
});
|
|
@@ -16,59 +16,81 @@ export default {
|
|
|
16
16
|
story => `<div style="display: flex; flex-direction: column; gap: 2rem; align-items: center">${story}</div>`
|
|
17
17
|
),
|
|
18
18
|
],
|
|
19
|
-
args: {
|
|
20
|
-
value: '',
|
|
21
|
-
disabled: false,
|
|
22
|
-
required: false,
|
|
23
|
-
},
|
|
24
19
|
argTypes: {
|
|
25
|
-
value: {
|
|
26
|
-
control: false,
|
|
27
|
-
table: {
|
|
28
|
-
category: 'models',
|
|
29
|
-
readonly: true,
|
|
30
|
-
type: {
|
|
31
|
-
summary: 'T',
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
20
|
disabled: {
|
|
36
|
-
control: 'boolean',
|
|
37
|
-
table: {
|
|
38
|
-
category: 'models',
|
|
39
|
-
type: {
|
|
40
|
-
summary: 'boolean',
|
|
41
|
-
},
|
|
42
|
-
readonly: true,
|
|
43
|
-
},
|
|
21
|
+
control: 'boolean' as const,
|
|
22
|
+
table: { category: 'inputs', type: { summary: 'boolean' }, defaultValue: { summary: 'false' } },
|
|
44
23
|
},
|
|
45
24
|
required: {
|
|
46
|
-
control: 'boolean',
|
|
25
|
+
control: 'boolean' as const,
|
|
26
|
+
table: { category: 'inputs', type: { summary: 'boolean' }, defaultValue: { summary: 'false' } },
|
|
27
|
+
},
|
|
28
|
+
touched: {
|
|
29
|
+
control: false as const,
|
|
30
|
+
table: { category: 'models', type: { summary: 'boolean' }, readonly: true },
|
|
31
|
+
},
|
|
32
|
+
dirty: {
|
|
33
|
+
control: false as const,
|
|
34
|
+
table: { category: 'inputs', type: { summary: 'boolean' }, readonly: true },
|
|
35
|
+
},
|
|
36
|
+
invalid: {
|
|
37
|
+
control: false as const,
|
|
38
|
+
table: { category: 'inputs', type: { summary: 'boolean' }, readonly: true },
|
|
39
|
+
},
|
|
40
|
+
pending: {
|
|
41
|
+
control: false as const,
|
|
42
|
+
table: { category: 'inputs', type: { summary: 'boolean' }, readonly: true },
|
|
43
|
+
},
|
|
44
|
+
hidden: {
|
|
45
|
+
control: 'boolean' as const,
|
|
46
|
+
table: { category: 'inputs', type: { summary: 'boolean' }, defaultValue: { summary: 'false' } },
|
|
47
|
+
},
|
|
48
|
+
readonly: {
|
|
49
|
+
control: 'boolean' as const,
|
|
50
|
+
table: { category: 'inputs', type: { summary: 'boolean' }, defaultValue: { summary: 'false' } },
|
|
51
|
+
},
|
|
52
|
+
name: {
|
|
53
|
+
control: 'text' as const,
|
|
54
|
+
table: { category: 'models', type: { summary: 'string' } },
|
|
55
|
+
},
|
|
56
|
+
errors: {
|
|
57
|
+
control: false as const,
|
|
58
|
+
table: { category: 'models', type: { summary: 'ValidationError[]' }, readonly: true },
|
|
59
|
+
},
|
|
60
|
+
disabledReasons: {
|
|
61
|
+
control: false as const,
|
|
62
|
+
table: { category: 'models', type: { summary: 'DisabledReason[]' }, readonly: true },
|
|
63
|
+
},
|
|
64
|
+
value: {
|
|
65
|
+
control: false as const,
|
|
47
66
|
table: {
|
|
48
|
-
category: '
|
|
49
|
-
type: {
|
|
50
|
-
summary: 'boolean',
|
|
51
|
-
},
|
|
52
|
-
defaultValue: {
|
|
53
|
-
summary: 'false',
|
|
54
|
-
},
|
|
67
|
+
category: 'models',
|
|
55
68
|
readonly: true,
|
|
69
|
+
type: { summary: 'T' },
|
|
56
70
|
},
|
|
57
71
|
},
|
|
58
72
|
onInput: {
|
|
59
73
|
table: {
|
|
60
74
|
readonly: true,
|
|
61
|
-
type: {
|
|
62
|
-
summary: '(value: T) => void',
|
|
63
|
-
},
|
|
75
|
+
type: { summary: '(value: T) => void' },
|
|
64
76
|
},
|
|
65
77
|
},
|
|
66
78
|
},
|
|
79
|
+
args: {
|
|
80
|
+
disabled: false,
|
|
81
|
+
required: false,
|
|
82
|
+
touched: false,
|
|
83
|
+
dirty: false,
|
|
84
|
+
invalid: false,
|
|
85
|
+
pending: false,
|
|
86
|
+
hidden: false,
|
|
87
|
+
readonly: false,
|
|
88
|
+
name: '',
|
|
89
|
+
value: '',
|
|
90
|
+
},
|
|
67
91
|
parameters: {
|
|
68
92
|
docs: {
|
|
69
93
|
canvas: {
|
|
70
|
-
// This will remove the "show code" button
|
|
71
|
-
// https://storybook.js.org/docs/api/doc-blocks/doc-block-canvas#sourcestate
|
|
72
94
|
sourceState: 'none',
|
|
73
95
|
},
|
|
74
96
|
},
|
|
@@ -1,100 +1,85 @@
|
|
|
1
|
-
import { booleanAttribute, Directive,
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
-
export const ZenFormControlProvider = <T extends ZenFormControl<any>>(component: Type<T>) => ({
|
|
6
|
-
provide: NG_VALUE_ACCESSOR,
|
|
7
|
-
useExisting: forwardRef(() => component),
|
|
8
|
-
multi: true,
|
|
9
|
-
});
|
|
1
|
+
import { booleanAttribute, Directive, input, ModelSignal, output } from '@angular/core';
|
|
2
|
+
import type { DisabledReason, ValidationError, WithOptionalFieldTree } from '@angular/forms/signals';
|
|
3
|
+
import { FormValueControl } from '@angular/forms/signals';
|
|
10
4
|
|
|
11
5
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
6
|
+
* Abstract base class for form controls that integrate with Signal Forms
|
|
7
|
+
* via the {@link FormValueControl} interface.
|
|
8
|
+
*
|
|
9
|
+
* Provides all {@link FormUiControl} state inputs (`disabled`, `required`,
|
|
10
|
+
* `touched`, `errors`, etc.) and the `touch` output used by the
|
|
11
|
+
* `[formField]` directive.
|
|
15
12
|
*
|
|
16
|
-
* Subclasses must provide an implementation
|
|
13
|
+
* Subclasses must provide an implementation of the `value` model signal
|
|
14
|
+
* with the appropriate value type.
|
|
15
|
+
*
|
|
16
|
+
* Decorated with `@Directive` only to declare the host event binding
|
|
17
|
+
* `(blur) -> touch.emit()`.
|
|
17
18
|
*
|
|
18
19
|
* @example
|
|
19
20
|
*
|
|
20
21
|
* ```ts
|
|
21
22
|
* @Component({
|
|
22
|
-
*
|
|
23
|
-
*
|
|
23
|
+
* selector: 'my-input',
|
|
24
|
+
* template: `<input [value]="value()" (input)="onInput($event.target.value)" />`,
|
|
24
25
|
* })
|
|
25
|
-
* export class
|
|
26
|
-
* readonly value = model<string>('')
|
|
26
|
+
* export class MyInput extends ZenFormControl<string> {
|
|
27
|
+
* readonly value = model<string>('');
|
|
27
28
|
* }
|
|
28
29
|
* ```
|
|
29
30
|
*
|
|
30
|
-
* @implements {
|
|
31
|
+
* @implements {FormValueControl}
|
|
31
32
|
*
|
|
32
33
|
* @author Konrad Stępień
|
|
33
34
|
* @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
|
|
34
35
|
* @see [GitHub](https://github.com/kstepien3/ng-zen)
|
|
35
36
|
*/
|
|
36
|
-
@Directive({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
@Directive({
|
|
38
|
+
host: {
|
|
39
|
+
'(blur)': 'touch.emit()',
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
export abstract class ZenFormControl<Value> implements FormValueControl<Value> {
|
|
43
|
+
/** The underlying value of the control. Subclasses must provide their own implementation using `model()`. */
|
|
42
44
|
abstract readonly value: ModelSignal<Value>;
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
/** Validation errors for the field. Auto-bound by the Signal Forms directive. */
|
|
47
|
+
readonly errors = input<readonly ValidationError.WithOptionalFieldTree[]>([]);
|
|
48
|
+
/** Whether the control is disabled. Auto-bound by the Signal Forms directive. */
|
|
49
|
+
readonly disabled = input(false);
|
|
50
|
+
/** Reasons why the field is disabled. Auto-bound by the Signal Forms directive. */
|
|
51
|
+
readonly disabledReasons = input<readonly WithOptionalFieldTree<DisabledReason>[]>([]);
|
|
52
|
+
/** Whether the field is read-only. Auto-bound by the Signal Forms directive. */
|
|
53
|
+
readonly readonly = input<boolean>(false);
|
|
54
|
+
/** Whether the field is hidden from view. Auto-bound by the Signal Forms directive. */
|
|
55
|
+
readonly hidden = input<boolean>(false);
|
|
56
|
+
/** Whether the field has validation errors. Auto-bound by the Signal Forms directive. */
|
|
57
|
+
readonly invalid = input<boolean>(false);
|
|
58
|
+
/** Whether async validation is in progress. Auto-bound by the Signal Forms directive. */
|
|
59
|
+
readonly pending = input<boolean>(false);
|
|
60
|
+
/** Whether the user has focused and then blurred the field. */
|
|
61
|
+
readonly touched = input<boolean>(false);
|
|
62
|
+
/** Whether the field value has been modified. Auto-bound by the Signal Forms directive. */
|
|
63
|
+
readonly dirty = input(false);
|
|
64
|
+
/** Field name in the form. Auto-bound by the Signal Forms directive. */
|
|
65
|
+
readonly name = input('');
|
|
66
|
+
/** Whether the field is required. Auto-bound by the Signal Forms directive from schema validators. */
|
|
45
67
|
readonly required = input(false, { transform: booleanAttribute });
|
|
68
|
+
/** Emitted when the field is blurred. The {@link FormField} directive listens to this output to mark the field as touched. */
|
|
69
|
+
readonly touch = output<void>();
|
|
46
70
|
|
|
47
|
-
//
|
|
48
|
-
|
|
71
|
+
// @ignore
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
|
73
|
+
focus(_options?: FocusOptions): void {}
|
|
49
74
|
|
|
75
|
+
// @ignore
|
|
50
76
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
51
|
-
|
|
77
|
+
reset(): void {}
|
|
52
78
|
|
|
53
|
-
/**
|
|
54
|
-
* Should be called by the subclass when the control's value changes
|
|
55
|
-
* as a result of user interaction. This method updates the control's value
|
|
56
|
-
* and notifies Angular of the change.
|
|
57
|
-
*/
|
|
79
|
+
/** Should be called by the subclass when the control's value changes as a result of user interaction. */
|
|
58
80
|
onInput(value: Value): void {
|
|
59
81
|
if (this.disabled()) return;
|
|
60
82
|
|
|
61
83
|
this.value.set(value);
|
|
62
|
-
this.onChange(this.value());
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Writes a new value to the element.
|
|
67
|
-
* @internal For internal use by Angular forms.
|
|
68
|
-
* @ignore
|
|
69
|
-
*/
|
|
70
|
-
writeValue(value: Value): void {
|
|
71
|
-
this.value.set(value);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Registers a callback function that is called when the control's value changes in the UI.
|
|
76
|
-
* @internal For internal use by Angular forms.
|
|
77
|
-
* @ignore
|
|
78
|
-
*/
|
|
79
|
-
registerOnChange(fn: (value: Value) => void): void {
|
|
80
|
-
this.onChange = fn;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Registers a callback function that is called by the forms API on component touch.
|
|
85
|
-
* @internal For internal use by Angular forms.
|
|
86
|
-
* @ignore
|
|
87
|
-
*/
|
|
88
|
-
registerOnTouched(fn: () => void): void {
|
|
89
|
-
this.onTouched = fn;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* This function is called by the forms API when the control status changes to or from "DISABLED".
|
|
94
|
-
* @internal For internal use by Angular forms.
|
|
95
|
-
* @ignore
|
|
96
|
-
*/
|
|
97
|
-
setDisabledState(isDisabled: boolean): void {
|
|
98
|
-
this.disabled.set(isDisabled);
|
|
99
84
|
}
|
|
100
85
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { booleanAttribute,
|
|
1
|
+
import { booleanAttribute, Component, computed, inject, input } from '@angular/core';
|
|
2
2
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
|
3
3
|
|
|
4
4
|
/** SVG icon data structure from HugeIcons library - an array of [tagName, attributes] tuples. */
|
|
@@ -43,7 +43,6 @@ export type IconSvgObject = readonly (readonly [string, Record<string, string |
|
|
|
43
43
|
></svg>
|
|
44
44
|
`,
|
|
45
45
|
styles: [':host { display: inline-flex; }'],
|
|
46
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
47
46
|
})
|
|
48
47
|
export class ZenIcon {
|
|
49
48
|
/** SVG object imported from HugeIcons library. */
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { NgComponentOutlet } from '@angular/common';
|
|
2
|
+
import { Component, signal } from '@angular/core';
|
|
3
|
+
import { form, FormField, FormRoot, required } 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 { ZenInput } from './input';
|
|
4
8
|
|
|
5
9
|
type Options = ZenInput;
|
|
@@ -8,54 +12,27 @@ export default {
|
|
|
8
12
|
title: 'Ui/Input',
|
|
9
13
|
component: ZenInput,
|
|
10
14
|
argTypes: {
|
|
15
|
+
...FormControlStories.argTypes,
|
|
11
16
|
value: {
|
|
12
|
-
control: 'text',
|
|
17
|
+
control: 'text' as const,
|
|
13
18
|
table: {
|
|
14
19
|
category: 'models',
|
|
15
|
-
type: {
|
|
16
|
-
|
|
17
|
-
},
|
|
18
|
-
defaultValue: {
|
|
19
|
-
summary: '',
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
placeholder: { control: 'text', table: { type: { summary: 'string' } } },
|
|
24
|
-
disabled: {
|
|
25
|
-
control: 'boolean',
|
|
26
|
-
table: {
|
|
27
|
-
category: 'models',
|
|
28
|
-
type: {
|
|
29
|
-
summary: 'boolean',
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
required: {
|
|
34
|
-
control: 'boolean',
|
|
35
|
-
table: {
|
|
36
|
-
category: 'inputs',
|
|
37
|
-
type: {
|
|
38
|
-
summary: 'boolean',
|
|
39
|
-
},
|
|
40
|
-
defaultValue: {
|
|
41
|
-
summary: 'false',
|
|
42
|
-
},
|
|
20
|
+
type: { summary: 'string' },
|
|
21
|
+
defaultValue: { summary: "''" },
|
|
43
22
|
},
|
|
44
23
|
},
|
|
24
|
+
placeholder: { control: 'text' as const, table: { type: { summary: 'string' } } },
|
|
45
25
|
onInput: {
|
|
46
26
|
table: {
|
|
47
27
|
readonly: true,
|
|
48
|
-
type: {
|
|
49
|
-
summary: '(value: string) => void',
|
|
50
|
-
},
|
|
28
|
+
type: { summary: '(value: string) => void' },
|
|
51
29
|
},
|
|
52
30
|
},
|
|
53
31
|
},
|
|
54
32
|
args: {
|
|
33
|
+
...FormControlStories.args,
|
|
55
34
|
value: '',
|
|
56
35
|
placeholder: '',
|
|
57
|
-
disabled: false,
|
|
58
|
-
required: false,
|
|
59
36
|
},
|
|
60
37
|
} satisfies Meta<Options>;
|
|
61
38
|
|
|
@@ -73,3 +50,76 @@ export const WithLabel: Story = {
|
|
|
73
50
|
`,
|
|
74
51
|
}),
|
|
75
52
|
};
|
|
53
|
+
|
|
54
|
+
export const WithSignalForm: Story = {
|
|
55
|
+
render: () => ({
|
|
56
|
+
moduleMetadata: { imports: [NgComponentOutlet] },
|
|
57
|
+
props: { component: InputSignalFormComponent },
|
|
58
|
+
template: '<ng-container *ngComponentOutlet="component" />',
|
|
59
|
+
}),
|
|
60
|
+
parameters: {
|
|
61
|
+
docs: {
|
|
62
|
+
source: {
|
|
63
|
+
code: `<zen-input [formField]="form.name" />`,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const WithFormRoot: Story = {
|
|
70
|
+
render: () => ({
|
|
71
|
+
moduleMetadata: { imports: [NgComponentOutlet] },
|
|
72
|
+
props: { component: InputFormRootComponent },
|
|
73
|
+
template: '<ng-container *ngComponentOutlet="component" />',
|
|
74
|
+
}),
|
|
75
|
+
parameters: {
|
|
76
|
+
docs: {
|
|
77
|
+
source: {
|
|
78
|
+
code: `<form [formRoot]="loginForm">
|
|
79
|
+
<zen-input [formField]="loginForm.email" placeholder="Email" />
|
|
80
|
+
<zen-input [formField]="loginForm.password" placeholder="Password" type="password" />
|
|
81
|
+
<button type="submit">Sign in</button>
|
|
82
|
+
</form>`,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
@Component({
|
|
89
|
+
standalone: true,
|
|
90
|
+
template: `
|
|
91
|
+
<div style="display: flex; flex-direction: column; gap: 0.5rem; max-width: 300px;">
|
|
92
|
+
<zen-input [formField]="form.name" placeholder="Type something" />
|
|
93
|
+
@if (form.name().invalid()) {
|
|
94
|
+
<p style="color: red; margin: 0; font-size: 0.875rem;">
|
|
95
|
+
{{ form.name().errors()![0].message }}
|
|
96
|
+
</p>
|
|
97
|
+
}
|
|
98
|
+
</div>
|
|
99
|
+
`,
|
|
100
|
+
imports: [FormField, ZenInput],
|
|
101
|
+
})
|
|
102
|
+
class InputSignalFormComponent {
|
|
103
|
+
readonly form = form(signal({ name: '' }), s => {
|
|
104
|
+
required(s.name, { message: 'Name is required' });
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Component({
|
|
109
|
+
standalone: true,
|
|
110
|
+
template: `
|
|
111
|
+
<form [formRoot]="loginForm" style="display: flex; flex-direction: column; gap: 0.5rem; max-width: 300px;">
|
|
112
|
+
<zen-input [formField]="loginForm.email" placeholder="Email" />
|
|
113
|
+
<zen-input [formField]="loginForm.password" placeholder="Password" type="password" />
|
|
114
|
+
<button type="submit">Sign in</button>
|
|
115
|
+
</form>
|
|
116
|
+
`,
|
|
117
|
+
imports: [FormRoot, FormField, ZenInput],
|
|
118
|
+
})
|
|
119
|
+
class InputFormRootComponent {
|
|
120
|
+
readonly loginModel = signal({ email: '', password: '' });
|
|
121
|
+
readonly loginForm = form(this.loginModel, s => {
|
|
122
|
+
required(s.email, { message: 'Email is required' });
|
|
123
|
+
required(s.password, { message: 'Password is required' });
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -1,30 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FormsModule } from '@angular/forms';
|
|
1
|
+
import { Component, input, model } from '@angular/core';
|
|
3
2
|
|
|
4
|
-
import { ZenFormControl
|
|
3
|
+
import { ZenFormControl } from '../form-control';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
|
-
* ZenInput is a
|
|
8
|
-
* a consistent and customizable input style across the application.
|
|
9
|
-
* It supports Angular forms integration and provides two-way data binding.
|
|
6
|
+
* ZenInput is a single-line text input component backed by Signal Forms.
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Connect it to a Signal Forms field with `[formField]`:
|
|
9
|
+
*
|
|
10
|
+
* ```html
|
|
11
|
+
* <zen-input [formField]="myForm.name" placeholder="Enter name" />
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The `[formField]` directive automatically synchronises value, disabled
|
|
15
|
+
* state, validation errors, and touched/dirty status.
|
|
13
16
|
*
|
|
14
17
|
* ### CSS Custom Properties
|
|
15
|
-
*
|
|
18
|
+
*
|
|
16
19
|
*
|
|
17
20
|
* ```css
|
|
18
21
|
* :root {
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* --zen-input-border: 1px solid hsl(0deg 0% 80%);
|
|
23
|
+
* --zen-input-border-radius: 8px;
|
|
24
|
+
* --zen-input-padding: 0.5rem 1rem;
|
|
25
|
+
* --zen-input-focus-shadow: 0 1px 4px hsl(0deg 0% 60% / 20%) inset;
|
|
26
|
+
* --zen-input-placeholder-color: hsl(0deg 0% 60%);
|
|
24
27
|
* }
|
|
25
28
|
* ```
|
|
26
29
|
*
|
|
27
|
-
* @
|
|
30
|
+
* @extends {ZenFormControl<string>}
|
|
28
31
|
*
|
|
29
32
|
* @author Konrad Stępień
|
|
30
33
|
* @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
|
|
@@ -34,24 +37,24 @@ import { ZenFormControl, ZenFormControlProvider } from '../form-control';
|
|
|
34
37
|
selector: 'zen-input',
|
|
35
38
|
template: `
|
|
36
39
|
<input
|
|
37
|
-
[
|
|
38
|
-
[attr.required]="required()"
|
|
40
|
+
[aria-invalid]="invalid() || null"
|
|
39
41
|
[disabled]="disabled()"
|
|
40
|
-
[
|
|
42
|
+
[placeholder]="placeholder()"
|
|
43
|
+
[required]="required()"
|
|
44
|
+
[type]="type()"
|
|
41
45
|
[value]="value()"
|
|
42
|
-
(
|
|
43
|
-
|
|
46
|
+
(input)="onInput(inputRef.value)"
|
|
47
|
+
#inputRef
|
|
44
48
|
/>
|
|
45
49
|
`,
|
|
46
50
|
styleUrls: ['./input.scss'],
|
|
47
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
48
|
-
providers: [ZenFormControlProvider(ZenInput)],
|
|
49
|
-
imports: [FormsModule],
|
|
50
51
|
})
|
|
51
52
|
export class ZenInput extends ZenFormControl<string> {
|
|
52
53
|
/** The current input value with two-way binding support. */
|
|
53
54
|
readonly value = model('');
|
|
54
55
|
|
|
55
56
|
/** The placeholder text for the form control. */
|
|
56
|
-
readonly placeholder = input<string>();
|
|
57
|
+
readonly placeholder = input<string>('');
|
|
58
|
+
|
|
59
|
+
readonly type = input<'text' | 'email' | 'url' | 'password'>('text');
|
|
57
60
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Component, input } from '@angular/core';
|
|
2
2
|
|
|
3
3
|
import { PopoverPlacement } from '../popover-positions.type';
|
|
4
4
|
|
|
@@ -25,7 +25,6 @@ import { PopoverPlacement } from '../popover-positions.type';
|
|
|
25
25
|
<ng-content />
|
|
26
26
|
`,
|
|
27
27
|
styleUrl: './popover-host.scss',
|
|
28
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
29
28
|
host: {
|
|
30
29
|
popover: 'auto',
|
|
31
30
|
'[attr.id]': 'id()',
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Service } from '@angular/core';
|
|
2
2
|
|
|
3
3
|
import { ZenRadio } from './radio';
|
|
4
4
|
|
|
5
|
-
@
|
|
5
|
+
@Service()
|
|
6
6
|
export class ZenRadioRegistry {
|
|
7
7
|
private readonly radios = new Map<string, ZenRadio[]>();
|
|
8
8
|
|