di-controls
Version:
<!-- PROJECT LOGO -->
945 lines (937 loc) • 33.5 kB
JavaScript
import * as i0 from '@angular/core';
import { signal, inject, model, ElementRef, Renderer2, ChangeDetectorRef, Directive, DestroyRef, computed } from '@angular/core';
import { NgControl } from '@angular/forms';
import { EMPTY_FUNCTION, DI_DEFAULT_COMPARE } from 'di-controls/constants';
import { hasValue, resolveValue } from 'di-controls/helpers';
import { toObservable } from '@angular/core/rxjs-interop';
import { SetCompare } from 'di-controls/classes';
/**
* Base implementation of ControlValueAccessor
*/
class DIControlValueAccessor {
config;
model = signal(null);
ngControl = inject(NgControl, { optional: true, self: true });
changeDetectorRef;
disabled = model(false);
touch = EMPTY_FUNCTION;
change = EMPTY_FUNCTION;
#element = inject(ElementRef).nativeElement;
#renderer = inject(Renderer2);
constructor(config) {
this.config = config;
this.changeDetectorRef = inject(ChangeDetectorRef);
if (this.ngControl) {
this.ngControl.valueAccessor = this;
}
if (this.config?.withNativeElementSupport) {
toObservable(this.disabled)
.subscribe((isDisabled) => this.addDisabledAttribute(isDisabled));
}
}
/**
* Returns true if the control is not empty.
*/
get hasValue() {
return hasValue(this.model());
}
/**
* Method is called by the forms API.
*
* @param fn - callback function to register on value change
* @internal
*/
registerOnChange(fn) {
this.change = fn;
}
/**
* Method is called by the forms API.
*
* @param fn - callback function to register on touch
* @internal
*/
registerOnTouched(fn) {
this.touch = fn;
}
/**
* Method is called by the forms API to write to the view when programmatic changes from model to view are requested.
*
* @param obj - new value
* @internal
*/
writeValue(obj) {
if (this.model() !== obj) {
this.update(obj);
}
}
/**
* Updates the model.
*
* @param value - new value
*/
internalUpdateModel(value) {
this.model.set(value);
this.change(this.model());
this.changeDetectorRef.markForCheck();
}
/**
* Method is called by the forms API to write to the view when programmatic changes from model to view are requested.
*
* @param isDisabled - new value
* @internal
*/
setDisabledState(isDisabled) {
this.disabled.set(isDisabled);
this.addDisabledAttribute(isDisabled);
}
update(value) {
this.model.set(value);
this.config?.onIncomingUpdate && this.config.onIncomingUpdate(value);
this.changeDetectorRef.markForCheck();
}
addDisabledAttribute(isDisabled) {
if (this.config?.withNativeElementSupport) {
isDisabled
? this.#renderer.setAttribute(this.#element, 'disabled', 'true')
: this.#renderer.removeAttribute(this.#element, 'disabled');
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DIControlValueAccessor, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.0.5", type: DIControlValueAccessor, isStandalone: true, inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange" }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DIControlValueAccessor, decorators: [{
type: Directive
}], ctorParameters: () => [{ type: undefined }] });
/**
* `DIControl` can be used to implement any control that you want. It can work with any model type.
* All updates from children will be accepted as is. And updates from outside (`FormControl`, `NgModel`, another Control)
* will be accepted as is too.
*
* ## Creating a control
* To create a control you need to extend your `@Component` or `@Directive` from `DIControl` class.
* After that your control will be able to work with `NgModel`, `FormControl`.
*
* ```ts fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DIControl<string> {
* constructor() {
* super();
* }
* }
* ```
*
* ## Registering as a host
* By default your control can work only with `NgModel` and `FormControl`. But you can register your control as a host
* for another controls, then your control will be able to update them and accept updates from them. To do that you need to
* use `provideHostControl` function.
*
* ```ts {2} fileName="custom-control.component.ts"
* @Component({
* providers: [provideHostControl(CustomControlComponent)],
* })
* export class CustomControlComponent extends DIControl<string> {
* constructor() {
* super();
* }
* }
* ```
*
* ## Injecting host control
* By default your control doesn't communicate with host controls. But you can inject host control and put it
* into `super` call. This will register your control in the host control and start communication between them.
*
* > **Note**
* > If you register your control as a host for another controls, then you can inject it
* > only with `skipSelf` option.
*
* ```ts {5} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DIControl<string> {
* constructor() {
* // we add `optional` option to make it possible to use this control without host
* super({host: injectHostControl({optional: true})});
* }
* }
* ```
*
* ## Getting model
* To get model you need to use `model` property. It will return model for the current control.
*
* ```ts {9} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DIControl<string> {
* constructor() {
* super();
* }
*
* @HostListener('click')
* onClick() {
* console.log(this.model());
* }
* }
* ```
*
* ## Updating model
* To update model you need to call `updateModel` method. It will update model for the current control and all
* children controls, as well as for the `NgModel` or `FormControl`.
*
* ```ts {9} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DIControl<string> {
* constructor() {
* super();
* }
*
* @HostListener('click')
* onClick() {
* this.updateModel('new value');
* }
* }
* ```
* ## Catching updates
* Sometimes you may need to catch updates from different sources. For example, to update the value of the native
* input element. To do this, you can provide the `onIncomingUpdate` hook.
*
* ```ts {6} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DIControl<string> {
* constructor() {
* super({
* onIncomingUpdate: (value: string | null) => {
* this.elementRef.nativeElement.value = value;
* },
* });
* }
* }
* ```
*/
class DIControl extends DIControlValueAccessor {
config;
/**
* List of children controls.
*
* @protected
* @internal
*/
children = new Set();
/**
* Control from which we have to update our model.
*
* @protected
* @internal
*/
updateFrom = null;
/**
* Request host for update the current control.
* Host will update the current control based on its current state and host control logic.
*
* @protected
* @internal
*/
requestForUpdate = EMPTY_FUNCTION;
/**
* Function that should be used to make control touched.
*/
touch = () => this.config?.host?.touch();
onControlChangeFn = EMPTY_FUNCTION;
destroyRef = inject(DestroyRef);
constructor(config) {
super(config);
this.config = config;
this.destroyRef.onDestroy(() => this.config?.host?.unregisterControl(this));
}
ngOnInit() {
/*
* We have to register control with Promise.resolve because NgModel uses it too to set first
* value (https://github.com/angular/angular/blob/7df9127088bda3c9d29937a04287b87dc2045ea7/packages/forms/src/directives/ng_model.ts#L314)
*/
Promise.resolve().then(() => this.config?.host?.registerControl(this));
}
/**
* Registers provided control as a child of the current control.
*
* @param control - control that will be registered.
* @internal
*/
registerControl(control) {
this.children.add(control);
/*
* We have to update control because its can be created dynamically.
* We use Promise.resolve because NgModel uses it too to set first value (https://github.com/angular/angular/blob/7df9127088bda3c9d29937a04287b87dc2045ea7/packages/forms/src/directives/ng_model.ts#L314)
* so there's no need to use angular life cycle hooks
*/
Promise.resolve().then(() => {
this.updateControl(control, this.model());
});
control.registerOnControlChange((value) => {
this.childControlChange(control, value);
this.config?.onChildControlChange?.(control, value);
});
control.registerRequestForUpdate(() => {
this.updateControl(control, this.model());
});
}
/**
* Unregisters provided control from the current control.
*
* @param control - control that will be unregistered.
* @internal
*/
unregisterControl(control) {
this.children.delete(control);
}
registerOnTouched(fn) {
this.touch = () => {
fn();
// Touch host control to update its state
this.config?.host?.touch();
};
}
/**
* Registers provided function as a callback that will be called when the current control changes.
* This function will be provided by the host control to update its model.
*
* @param fn - callback function.
* @internal
*/
registerOnControlChange(fn) {
this.onControlChangeFn = fn;
}
/**
* Registers provided function as a callback that can be called to request an update from the host control.
* After calling this function the host control will update the model of the current control based on the current
* state of the control and host control logic.
*
* @param fn - callback function.
* @internal
*/
registerRequestForUpdate(fn) {
this.requestForUpdate = fn;
}
/**
* Updates the model of the current control.
* This is the main method that should be used to update the model.
*
* @param value - new value.
*/
updateModel(value) {
this.updateFrom = null;
this.internalUpdateModel(value);
}
/**
* Updates the model of the current control.
* Don't use this method directly, use `updateModel` instead.
*
* @param value - new value.
* @internal
*/
internalUpdateModel(value) {
super.internalUpdateModel(value);
this.onControlChangeFn(value);
this.updateControls(this.model());
}
writeValue(value) {
if (this.model() !== value) {
super.writeValue(value);
this.updateControls(value);
this.onControlChangeFn(value);
}
}
/**
* Method is called by the host to update the value of the control.
*
* @param value - new value
* @internal
*/
writeValueFromHost(value) {
if (this.model() !== value) {
super.writeValue(value);
this.change(value);
this.updateControls(value);
}
}
/**
* Updates all child controls with the provided value.
*
* @param value - new value.
* @protected
* @internal
*/
updateControls(value) {
this.children.forEach((control) => {
if (control !== this.updateFrom) {
this.updateControl(control, value);
}
});
this.updateFrom = null;
}
/**
* Updates provided control with the provided value.
*
* @param control - control that will be updated.
* @param value - new value.
* @protected
* @internal
*/
updateControl(control, value) {
control.writeValueFromHost(value);
}
/**
* Function catches updates from child controls and updates the current control model.
*
* @param control - control that was updated.
* @param value - new value.
* @protected
* @internal
*/
childControlChange(control, value) {
if (this.model() !== value) {
this.updateFrom = control;
this.internalUpdateModel(value);
this.config?.onIncomingUpdate && this.config.onIncomingUpdate(value);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DIControl, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.5", type: DIControl, isStandalone: true, usesInheritance: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DIControl, decorators: [{
type: Directive
}], ctorParameters: () => [{ type: undefined }] });
/**
* `DIStateControl` can be used to implement state controls (checkbox, radio, chip, switch, etc.).
* It extends `DIControl` and adds `checked` signal that can be used to get checked state.
* By default it works with `boolean` type, it adds `value` input that can be used to set custom
* "true" value.
*
* ## Creating a control
* To create a control you need to extend your `@Component` or `@Directive` from `DIStateControl` class.
* After that your control will be able to work with `NgModel`, `FormControl`.
*
* ```ts fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent<T = boolean> extends DIStateControl<T> {
* @Input({required: true})
* value!: T;
*
* constructor() {
* super();
* }
* }
* ```
*
* ## Injecting host control
* By default your control doesn't communicate with host controls. But you can inject host control and put it
* into `super` call. This will register your control in the host control and start communication between them.
*
* > **Note**
* > If you register your control as a host for another controls, then you can inject it
* > only with `skipSelf` option.
*
* ```ts {5} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent<T = boolean> extends DIStateControl<T> {
* constructor() {
* // we add `optional` option to make it possible to use this control without host
* super({host: injectHostControl({optional: true})});
* }
* }
* ```
*
* ## Getting checked state
* To get checked state you need to use `checked` signal. It will return `true` if the current control is checked,
* `false` if it is unchecked and `null` if it is in intermediate state.
*
* ```ts {9} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent<T> extends DIControl<T> {
* @Input({required: true})
* value!: T;
*
* constructor() {
* super();
*
* console.log(this.checked());
* }
* }
* ```
*
* ## Getting model
* To get model you need to use `model` property. It will return model for the current control.
*
* ```ts {9} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent<T = boolean> extends DIStateControl<T> {
* constructor() {
* super();
* }
*
* @HostListener('click')
* onClick() {
* console.log(this.checked());
* }
* }
* ```
*
* ## Updating model
* To update model you should use `check`, `uncheck`, `intermediate` or `toggle` methods.
* They will update model based on the current state and configuration.
*
* ```ts {9} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent<T = boolean> extends DIStateControl<T> {
* constructor() {
* super();
* }
*
* @HostListener('click')
* onClick() {
* this.toggle();
* }
* }
* ```
*
* ## Catching updates
* Sometimes you may need to catch updates from different sources. For example, to update the value of the native
* input element. To do this, you can provide the `onIncomingUpdate` hook.
*
* ```ts {6} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent<T = boolean> extends DIStateControl<T> {
* constructor() {
* super({
* onIncomingUpdate: (value: string | null) => {
* this.elementRef.nativeElement.value = value;
* },
* });
* }
* }
* ```
*
*
* ## Using with DICollectionControl
* Using `DIStateControl` together with `DICollectionControl` will result in
* `DICollectionControl` containing a list of values from `DIStateControl` that have
* a checked state. If your `DIStateControl` has objects as values, you may
* likely need a comparison function because they can sometimes be immutable.
*
* To achieve this, provide your `DICollectionControl` as a `DICompareHost` and
* inject it into your `DIStateControl` to give `DIStateControl` access to
* the `compareFn` function.
*
* > **Warning**
* > `DICollectionControl` requires an explicit specification of the `uncheckedValue` in the `DIStateControl`
*
* ```ts {3-4} fileName="checkbox-group.component.ts"
* @Component({
* providers: [
* provideHostControl(CheckboxGroupComponent),
* provideCompareHost(CheckboxGroupComponent),
* ],
* })
* export class CheckboxGroupComponent<T> extends DICollectionControl<T> {
* constructor() {
* super();
* }
* }
* ```
*
* ```ts {5-7} fileName="checkbox.component.ts"
* @Component()
* export class CheckboxComponent<T> extends DIStateControl<T> {
* constructor() {
* super({
* host: injectHostControl({ optional: true }),
* compare: inject(DICompareHost, { optional: true }),
* hasIntermediate: true,
* });
*
* @HostListener('click')
* onClick() {
* this.toggle();
* }
* }
* ```
*/
class DIStateControl extends DIControl {
config;
checked = computed(() => {
const compareFn = typeof this.config?.compare === 'function'
? this.config.compare
: resolveValue(this.config?.compare?.compareFn ?? DI_DEFAULT_COMPARE);
return compareFn(this.model(), resolveValue(this.value)) ? true : this.isIntermediate ? null : false;
});
constructor(config) {
super(config);
this.config = config;
}
ngOnChanges({ value }) {
/*
* We have to request host for updates, because when we use ngFor directive
* with trackBy function, Angular doesn't re-create components, it just changes their inputs,
* so we have to request for updates our host, to determine right checked state
*/
if (value) {
this.requestForUpdate();
}
}
/** Sets checked state */
check() {
this.updateModel(resolveValue(this.value));
}
/** Sets unchecked state */
uncheck() {
if (!('uncheckValue' in (this.config ?? {}))) {
throw new Error('To use uncheck method you need to provide uncheckValue in DIStateControl config');
}
this.updateModel(this.config.uncheckValue);
}
/** Sets intermediate state */
intermediate() {
this.updateModel(null);
}
/** Toggles checked state */
toggle() {
this.checked() ? this.uncheck() : this.check();
}
get isIntermediate() {
return this.model() === null && !!this.config?.hasIntermediate;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DIStateControl, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.5", type: DIStateControl, isStandalone: false, usesInheritance: true, usesOnChanges: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DIStateControl, decorators: [{
type: Directive,
args: [{
standalone: false
}]
}], ctorParameters: () => [{ type: undefined }] });
/**
* `DICollectionControl` can be used to implement array controls (checkbox group, radio group, chips, etc.).
* It has an additional integration with `DIStateControl` that allows you to use it as a host for
* `DIStateControl` controls. If you use `DIStateControl` as a child control, then `DICollectionControl`
* will update its model when the child control is checked or unchecked, so `DICollectionControl` will
* contain only checked values.
*
* It also works with other controls, but their model should be an array.
*
* > **Warning**
* > If child control model is updated with non-array value, then `DICollectionControl` will be updated with `null`.
*
* ## Creating a control
* To create a control you need to extend your `@Component` or `@Directive` from `DICollectionControl` class.
* After that your control will be able to work with `NgModel`, `FormControl`.
*
* ```ts fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DICollectionControl<string> {
* constructor() {
* super();
* }
* }
* ```
*
* ## Registering as a host
* By default your control can work only with `NgModel` and `FormControl`. But you can register your control as a host
* for another controls, then your control will be able to update them and accept updates from them. To do that you need to
* use `provideHostControl` function.
*
* ```ts {2} fileName="custom-control.component.ts"
* @Component({
* providers: [provideHostControl(CustomControlComponent)],
* })
* export class CustomControlComponent extends DICollectionControl<string> {
* constructor() {
* super();
* }
* }
* ```
*
* ## Injecting host control
* By default your control doesn't communicate with host controls. But you can inject host control and put it
* into `super` call. This will register your control in the host control and start communication between them.
*
* > **Note**
* > If you register your control as a host for another controls, then you can inject it
* > only with `skipSelf` option.
*
* ```ts {5} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DICollectionControl<string> {
* constructor() {
* // we add `optional` option to make it possible to use this control without host
* super({host: injectHostControl({optional: true})});
* }
* }
* ```
*
* ## Getting model
* To get model you need to use `model` property. It will return model for the current control.
*
* ```ts {9} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DICollectionControl<string> {
* constructor() {
* super();
* }
*
* @HostListener('click')
* onClick() {
* console.log(this.model());
* }
* }
* ```
*
* ## Updating model
* To update model you need to call `updateModel` method. It will update model for the current control and all
* children controls, as well as for the `NgModel` or `FormControl`.
*
* ```ts {9} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DICollectionControl<string> {
* constructor() {
* super();
* }
*
* @HostListener('click')
* onClick() {
* this.updateModel(['new value']);
* }
* }
* ```
* ## Catching updates
* Sometimes you may need to catch updates from different sources. For example, to update the value of the native
* input element. To do this, you can provide the `onIncomingUpdate` hook.
*
* ```ts {6} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DICollectionControl<string> {
* constructor() {
* super({
* onIncomingUpdate: (value: string[] | null) => {
* this.elementRef.nativeElement.value = value;
* },
* });
* }
* }
* ```
* ```
*/
class DICollectionControl extends DIControl {
config;
proxyModel = new SetCompare();
constructor(config) {
super(config);
this.config = config;
}
getCompareFn() {
return typeof this.config?.compare === 'function'
? this.config.compare
: resolveValue(this.config?.compare?.compareFn ?? DI_DEFAULT_COMPARE);
}
internalUpdateModel(obj) {
this.proxyModel = new SetCompare(this.getCompareFn(), obj);
super.internalUpdateModel(obj);
}
writeValue(value) {
this.proxyModel = new SetCompare(this.getCompareFn(), value);
super.writeValue(value);
}
writeValueFromHost(obj) {
this.proxyModel = new SetCompare(this.getCompareFn(), obj);
super.writeValueFromHost(obj);
}
childControlChange(control, value) {
this.updateFrom = control;
this.updateModel(this.getNewModel(control, value));
this.config?.onIncomingUpdate && this.config.onIncomingUpdate(this.model());
}
getNewModel(control, updates) {
if (control instanceof DIStateControl) {
control.checked()
? this.proxyModel.add(resolveValue(control.value))
: this.proxyModel.delete(resolveValue(control.value));
}
else if (Array.isArray(updates)) {
this.proxyModel = new SetCompare(this.getCompareFn(), updates);
}
else {
this.proxyModel = new SetCompare(this.getCompareFn());
return null;
}
return this.proxyModel.toArray();
}
updateControl(control) {
if (control instanceof DIStateControl) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
control.writeValueFromHost(this.proxyModel.has(resolveValue(control.value)) ? resolveValue(control.value) : control.config.uncheckValue);
}
else {
control.writeValueFromHost(this.model());
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DICollectionControl, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.5", type: DICollectionControl, isStandalone: true, usesInheritance: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DICollectionControl, decorators: [{
type: Directive
}], ctorParameters: () => [{ type: undefined }] });
/**
* `DIProxyControl` is very suitable.
* It is typically used as a host and works exclusively with objects. It is
* necessary to bind child controls to a specific property of the object,
* thereby ensuring that they update only that specific property and not the
* entire object as a whole. Please see `*DateRangePage`.
*
* ## Creating a control
* To create a control you need to extend your `@Component` or `@Directive` from `DIProxyControl` class
* and provide `getValue` and `setValue` functions that will be used to get and set value from the object
* to the child control.
*
* ```ts fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DIProxyControl<MyObject> {
* constructor() {
* super({
* getValue: (model) => model.objectProperty,
* setValue: (model, value) => ({...model, objectProperty: value}),
* });
* }
* }
* ```
*
* ## Registering as a host
* By default your control can work only with `NgModel` and `FormControl`. But you can register your control as a host
* for another controls, then your control will be able to update them and accept updates from them. To do that you need to
* use `provideHostControl` function.
*
* ```ts {2} fileName="custom-control.component.ts"
* @Component({
* providers: [provideHostControl(CustomControlComponent)],
* })
* export class CustomControlComponent extends DIProxyControl<MyObject> {
* constructor() {
* super({
* getValue: (model) => model.objectProperty,
* setValue: (model, value) => ({...model, objectProperty: value}),
* });
* }
* }
* ```
*
* ## Injecting host control
* By default your control doesn't communicate with host controls. But you can inject host control and put it
* into `super` call. This will register your control in the host control and start communication between them.
*
* > **Note**
* > If you register your control as a host for another controls, then you can inject it
* > only with `skipSelf` option.
*
* ```ts {5} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DIProxyControl<MyObject> {
* constructor() {
* // we add `optional` option to make it possible to use this control without host
* super({
* host: injectHostControl({optional: true}),
* getValue: (model) => model.objectProperty,
* setValue: (model, value) => ({...model, objectProperty: value}),
* });
* }
* }
* ```
*
* ## Getting model
* To get model you need to use `model` property. It will return model for the current control.
*
* ```ts {9} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DIProxyControl<MyObject> {
* constructor() {
* super(
* getValue: (model) => model.objectProperty,
* setValue: (model, value) => ({...model, objectProperty: value}),
* );
* }
*
* @HostListener('click')
* onClick() {
* console.log(this.model());
* }
* }
* ```
*
* ## Updating model
* To update model you need to call `updateModel` method. It will update model for the current control and all
* children controls, as well as for the `NgModel` or `FormControl`.
*
* ```ts {9} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DIProxyControl<MyObject> {
* constructor() {
* super(
* getValue: (model) => model.objectProperty,
* setValue: (model, value) => ({...model, objectProperty: value}),
* );
* }
*
* @HostListener('click')
* onClick() {
* this.updateModel({objectProperty: 'new value'});
* }
* }
* ```
* ## Catching updates
* Sometimes you may need to catch updates from different sources. For example, to update the value of the native
* input element. To do this, you can provide the `onIncomingUpdate` hook.
*
* ```ts {6} fileName="custom-control.component.ts"
* @Component({})
* export class CustomControlComponent extends DIProxyControl<MyObject> {
* constructor() {
* super({
* onIncomingUpdate: (value: MyObject | null) => {
* this.elementRef.nativeElement.value = value;
* },
* });
* }
* }
* ```
*/
class DIProxyControl extends DIControl {
config;
constructor(config) {
super(config);
this.config = config;
}
registerControl(control) {
this.children.add(control);
/*
* We have to update control because its can be created dynamically.
* We use Promise.resolve because NgModel uses it too to set first value (https://github.com/angular/angular/blob/7df9127088bda3c9d29937a04287b87dc2045ea7/packages/forms/src/directives/ng_model.ts#L314)
* so there's no need to use angular life cycle hooks
*/
Promise.resolve().then(() => control.writeValueFromHost(this.config.getValue(this.model())));
control.registerOnControlChange((value) => {
value = this.config.setValue(this.model(), value);
this.childControlChange(control, value);
this.config?.onChildControlChange?.(control, value);
});
control.registerRequestForUpdate(() => {
this.updateControl(control, this.config.getValue(this.model()));
});
}
updateControl(control, value) {
control.writeValueFromHost(this.config.getValue(value));
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DIProxyControl, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.5", type: DIProxyControl, isStandalone: true, usesInheritance: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DIProxyControl, decorators: [{
type: Directive
}], ctorParameters: () => [{ type: undefined }] });
/**
* Generated bundle index. Do not edit.
*/
export { DICollectionControl, DIControl, DIControlValueAccessor, DIProxyControl, DIStateControl };
//# sourceMappingURL=di-controls-controls.mjs.map