Skip to content

Commit 1cd4302

Browse files
committed
wip
1 parent acee6ce commit 1cd4302

File tree

1 file changed

+94
-80
lines changed
  • packages/forms/experimental/src/controls

1 file changed

+94
-80
lines changed

packages/forms/experimental/src/controls/control.ts

Lines changed: 94 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,27 @@ import {
2222
OutputRefSubscription,
2323
untracked,
2424
} from '@angular/core';
25-
import {ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl} from '@angular/forms';
26-
import {FormUiControl} from '../api/control';
27-
import {Field} from '../api/types';
25+
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
26+
import { FormUiControl } from '../api/control';
27+
import { Field } from '../api/types';
2828
import {
2929
illegallyGetComponentInstance,
3030
illegallyIsModelInput,
3131
illegallyIsSignalInput,
3232
illegallySetComponentInput as illegallySetInputSignal,
3333
} from '../illegal';
34-
import {InteropNgControl} from './interop_ng_control';
34+
import { InteropNgControl } from './interop_ng_control';
3535

3636
@Directive({
3737
selector: '[control]',
38-
providers: [{provide: NgControl, useFactory: () => inject(Control).ngControl}],
38+
providers: [{ provide: NgControl, useFactory: () => inject(Control).ngControl }],
3939
})
4040
export class Control<T> {
4141
readonly injector = inject(Injector);
42-
readonly field = input.required<Field<T>>({alias: 'control'});
42+
readonly field = input.required<Field<T>>({ alias: 'control' });
4343
readonly state = computed(() => this.field()());
4444
readonly el: ElementRef<HTMLElement> = inject(ElementRef);
45-
readonly cvaArray = inject<ControlValueAccessor[]>(NG_VALUE_ACCESSOR, {optional: true});
45+
readonly cvaArray = inject<ControlValueAccessor[]>(NG_VALUE_ACCESSOR, { optional: true });
4646

4747
private _ngControl: InteropNgControl | undefined;
4848

@@ -57,86 +57,100 @@ export class Control<T> {
5757
ngOnInit() {
5858
const injector = this.injector;
5959
const cmp = illegallyGetComponentInstance(injector);
60-
if (this.el.nativeElement instanceof HTMLInputElement) {
61-
// Bind our field to an <input>
62-
const i = this.el.nativeElement;
63-
const isCheckbox = i.type === 'checkbox';
64-
65-
i.addEventListener('input', () => {
66-
this.state().value.set((!isCheckbox ? i.value : i.checked) as T);
67-
this.state().markAsDirty();
68-
});
69-
i.addEventListener('blur', () => this.state().markAsTouched());
70-
71-
effect(
72-
() => {
73-
if (!isCheckbox) {
74-
i.value = this.state().value() as string;
75-
} else {
76-
i.checked = this.state().value() as boolean;
77-
}
78-
},
79-
{injector},
80-
);
60+
61+
if (cmp && isUiControl<T>(cmp)) {
62+
this.setupCustomUiControl(cmp);
63+
} else if (this.el.nativeElement instanceof HTMLInputElement || this.el.nativeElement instanceof HTMLTextAreaElement) {
64+
this.setupNativeInput(this.el.nativeElement);
8165
} else if (this.cva !== undefined) {
82-
const cva = this.cva;
83-
// Binding to a Control Value Accessor
84-
85-
cva.registerOnChange((value: T) => this.state().value.set(value));
86-
cva.registerOnTouched(() => this.state().markAsTouched());
87-
88-
effect(
89-
() => {
90-
const value = this.state().value();
91-
untracked(() => {
92-
cva.writeValue(value);
93-
});
94-
},
95-
{injector},
96-
);
97-
} else if (isUiControl<T>(cmp)) {
98-
// Binding to a custom UI component.
99-
100-
// Input bindings:
101-
maybeSynchronize(injector, () => this.state().value(), cmp.value);
102-
maybeSynchronize(injector, () => this.state().disabled(), cmp.disabled);
103-
maybeSynchronize(injector, () => this.state().readonly(), cmp.readonly);
104-
maybeSynchronize(injector, () => this.state().errors(), cmp.errors);
105-
maybeSynchronize(injector, () => this.state().touched(), cmp.touched);
106-
maybeSynchronize(injector, () => this.state().valid(), cmp.valid);
107-
108-
// Output bindings:
109-
const cleanupValue = cmp.value.subscribe((newValue) => this.state().value.set(newValue));
110-
let cleanupTouch: OutputRefSubscription | undefined;
111-
let cleanupDefaultTouch: (() => void) | undefined;
112-
if (cmp.touch !== undefined) {
113-
cleanupTouch = cmp.touch.subscribe(() => this.state().markAsTouched());
114-
} else {
115-
// If the component did not give us a touch event stream, use the standard touch logic,
116-
// marking it touched when the focus moves from inside the host element to outside.
117-
const listener = (event: FocusEvent) => {
118-
const newActiveEl = event.relatedTarget;
119-
if (!this.el.nativeElement.contains(newActiveEl as Element | null)) {
120-
this.state().markAsTouched();
121-
}
122-
};
123-
this.el.nativeElement.addEventListener('focusout', listener);
124-
cleanupDefaultTouch = () => this.el.nativeElement.removeEventListener('focusout', listener);
125-
}
126-
127-
// Cleanup for output binding subscriptions:
128-
injector.get(DestroyRef).onDestroy(() => {
129-
cleanupValue.unsubscribe();
130-
cleanupTouch?.unsubscribe();
131-
cleanupDefaultTouch?.();
132-
});
66+
this.setupControlValueAccessor(this.cva);
13367
} else {
13468
throw new Error(`Unhandled control?`);
13569
}
70+
13671
if (this.cva) {
13772
this.cva.writeValue(this.state().value());
13873
}
13974
}
75+
76+
// Bind our field to an <input> or <textarea>
77+
private setupNativeInput(input: HTMLInputElement | HTMLTextAreaElement): void {
78+
const isCheckbox = input instanceof HTMLInputElement && input.type === 'checkbox';
79+
80+
input.addEventListener('input', () => {
81+
this.state().value.set((!isCheckbox ? input.value : input.checked) as T);
82+
this.state().markAsDirty();
83+
});
84+
input.addEventListener('blur', () => this.state().markAsTouched());
85+
86+
effect(
87+
() => {
88+
if (!isCheckbox) {
89+
input.value = this.state().value() as string;
90+
} else {
91+
input.checked = this.state().value() as boolean;
92+
}
93+
},
94+
{ injector: this.injector },
95+
);
96+
}
97+
98+
99+
// Binding to a Control Value Accessor
100+
private setupControlValueAccessor(cva: ControlValueAccessor): void {
101+
cva.registerOnChange((value: T) => this.state().value.set(value));
102+
cva.registerOnTouched(() => this.state().markAsTouched());
103+
104+
effect(
105+
() => {
106+
const value = this.state().value();
107+
untracked(() => {
108+
cva.writeValue(value);
109+
});
110+
},
111+
{ injector: this.injector },
112+
);
113+
}
114+
115+
// Binding to a custom UI component.
116+
private setupCustomUiControl(
117+
cmp: FormUiControl<T>,
118+
) {
119+
120+
// Input bindings:
121+
maybeSynchronize(this.injector, () => this.state().value(), cmp.value);
122+
maybeSynchronize(this.injector, () => this.state().disabled(), cmp.disabled);
123+
maybeSynchronize(this.injector, () => this.state().readonly(), cmp.readonly);
124+
maybeSynchronize(this.injector, () => this.state().errors(), cmp.errors);
125+
maybeSynchronize(this.injector, () => this.state().touched(), cmp.touched);
126+
maybeSynchronize(this.injector, () => this.state().valid(), cmp.valid);
127+
128+
// Output bindings:
129+
const cleanupValue = cmp.value.subscribe((newValue) => this.state().value.set(newValue));
130+
let cleanupTouch: OutputRefSubscription | undefined;
131+
let cleanupDefaultTouch: (() => void) | undefined;
132+
if (cmp.touch !== undefined) {
133+
cleanupTouch = cmp.touch.subscribe(() => this.state().markAsTouched());
134+
} else {
135+
// If the component did not give us a touch event stream, use the standard touch logic,
136+
// marking it touched when the focus moves from inside the host element to outside.
137+
const listener = (event: FocusEvent) => {
138+
const newActiveEl = event.relatedTarget;
139+
if (!this.el.nativeElement.contains(newActiveEl as Element | null)) {
140+
this.state().markAsTouched();
141+
}
142+
};
143+
this.el.nativeElement.addEventListener('focusout', listener);
144+
cleanupDefaultTouch = () => this.el.nativeElement.removeEventListener('focusout', listener);
145+
}
146+
147+
// Cleanup for output binding subscriptions:
148+
this.injector.get(DestroyRef).onDestroy(() => {
149+
cleanupValue.unsubscribe();
150+
cleanupTouch?.unsubscribe();
151+
cleanupDefaultTouch?.();
152+
});
153+
}
140154
}
141155

142156
function isUiControl<T>(cmp: unknown): cmp is FormUiControl<T> {
@@ -163,5 +177,5 @@ function maybeSynchronize<T>(
163177
if (target === undefined) {
164178
return;
165179
}
166-
effect(() => illegallySetInputSignal(target, source()), {injector});
180+
effect(() => illegallySetInputSignal(target, source()), { injector });
167181
}

0 commit comments

Comments
 (0)