diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/checkbox_value_accessor.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/checkbox_value_accessor.ts index dabdfbd5d3..ebc2cfbe6b 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/checkbox_value_accessor.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/checkbox_value_accessor.ts @@ -1,6 +1,6 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/forms/src/directives/checkbox_value_accessor.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/forms/src/directives/checkbox_value_accessor.ts * with the following modifications: * - Update imports * - Remove all configuration from the CheckboxControlValueAccessor's `@Directive` decorator @@ -11,13 +11,15 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import {Directive, forwardRef, Provider} from '@angular/core'; +import { + BuiltInControlValueAccessor, +} from './control_value_accessor'; import type {ControlValueAccessor} from '@angular/forms'; -import {BuiltInControlValueAccessor} from './control_value_accessor'; /* [Nimble] Do not register as a value accessor provider const CHECKBOX_VALUE_ACCESSOR: Provider = { @@ -53,14 +55,16 @@ const CHECKBOX_VALUE_ACCESSOR: Provider = { /* [Nimble] Remove all configuration from @Directive decorator @Directive({ selector: - 'input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]', + 'input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]', host: {'(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()'}, - providers: [CHECKBOX_VALUE_ACCESSOR] + providers: [CHECKBOX_VALUE_ACCESSOR], }) */ @Directive() -export class CheckboxControlValueAccessor extends BuiltInControlValueAccessor implements - ControlValueAccessor { +export class CheckboxControlValueAccessor + extends BuiltInControlValueAccessor + implements ControlValueAccessor +{ /** * Sets the "checked" property on the input element. * @nodoc diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/control_value_accessor.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/control_value_accessor.ts index 771e9735f6..42b7c1b557 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/control_value_accessor.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/control_value_accessor.ts @@ -1,6 +1,6 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/forms/src/directives/control_value_accessor.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/forms/src/directives/control_value_accessor.ts * with the following modifications: * - Update imports * - Commented out ControlValueAccessor which is exported from @angular/forms @@ -12,7 +12,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import {Directive, ElementRef, InjectionToken, Renderer2} from '@angular/core'; @@ -164,7 +164,10 @@ export class BaseControlValueAccessor { */ onTouched = () => {}; - constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {} + constructor( + private _renderer: Renderer2, + private _elementRef: ElementRef, + ) {} /** * Helper method that sets a property on a target element using the current Renderer @@ -210,8 +213,7 @@ export class BaseControlValueAccessor { * applications code. */ @Directive() -export class BuiltInControlValueAccessor extends BaseControlValueAccessor { -} +export class BuiltInControlValueAccessor extends BaseControlValueAccessor {} /** * Used to provide a `ControlValueAccessor` for form controls. @@ -221,6 +223,7 @@ export class BuiltInControlValueAccessor extends BaseControlValueAccessor { * @publicApi */ /* [Nimble] Commenting out public injection token -export const NG_VALUE_ACCESSOR = - new InjectionToken>(ngDevMode ? 'NgValueAccessor' : ''); +export const NG_VALUE_ACCESSOR = new InjectionToken>( + ngDevMode ? 'NgValueAccessor' : '', +); */ \ No newline at end of file diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/default_value_accessor.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/default_value_accessor.ts index f511ec9135..9b64ada282 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/default_value_accessor.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/default_value_accessor.ts @@ -1,6 +1,6 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/forms/src/directives/default_value_accessor.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/forms/src/directives/default_value_accessor.ts * with the following modifications: * - Update imports * - Update implementation of `_isAndroid()` to not use private APIs @@ -12,20 +12,32 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import {ɵgetDOM as getDOM} from '@angular/common'; -import {Directive, ElementRef, forwardRef, Inject, InjectionToken, Optional, Provider, Renderer2} from '@angular/core'; +import { + Directive, + ElementRef, + forwardRef, + Inject, + InjectionToken, + Optional, + Provider, + Renderer2, +} from '@angular/core'; +import { + BaseControlValueAccessor, + // NG_VALUE_ACCESSOR, +} from './control_value_accessor'; import {ControlValueAccessor, COMPOSITION_BUFFER_MODE} from '@angular/forms'; -import {BaseControlValueAccessor} from './control_value_accessor'; /* [Nimble] Do not register as a default value accessor provider export const DEFAULT_VALUE_ACCESSOR: Provider = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DefaultValueAccessor), - multi: true + multi: true, }; */ @@ -47,8 +59,9 @@ function _isAndroid(): boolean { * the "compositionend" event occurs. * @publicApi * -export const COMPOSITION_BUFFER_MODE = - new InjectionToken(ngDevMode ? 'CompositionEventMode' : ''); +export const COMPOSITION_BUFFER_MODE = new InjectionToken( + ngDevMode ? 'CompositionEventMode' : '', +); */ /** @@ -56,7 +69,6 @@ export const COMPOSITION_BUFFER_MODE = * elements. The accessor is used by the `FormControlDirective`, `FormControlName`, and * `NgModel` directives. * - * {@searchKeywords ngDefaultControl} * * @usageNotes * @@ -89,7 +101,7 @@ export const COMPOSITION_BUFFER_MODE = /* [Nimble] Remove all configuration from @Directive decorator @Directive({ selector: - 'input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]', + 'input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]', // TODO: vsavkin replace the above selector with the one below it once // https://github.com/angular/angular/issues/3011 is implemented // selector: '[ngModel],[formControl],[formControlName]', @@ -97,9 +109,9 @@ export const COMPOSITION_BUFFER_MODE = '(input)': '$any(this)._handleInput($event.target.value)', '(blur)': 'onTouched()', '(compositionstart)': '$any(this)._compositionStart()', - '(compositionend)': '$any(this)._compositionEnd($event.target.value)' + '(compositionend)': '$any(this)._compositionEnd($event.target.value)', }, - providers: [DEFAULT_VALUE_ACCESSOR] + providers: [DEFAULT_VALUE_ACCESSOR], }) */ @Directive() @@ -108,8 +120,10 @@ export class DefaultValueAccessor extends BaseControlValueAccessor implements Co private _composing = false; constructor( - renderer: Renderer2, elementRef: ElementRef, - @Optional() @Inject(COMPOSITION_BUFFER_MODE) private _compositionMode: boolean) { + renderer: Renderer2, + elementRef: ElementRef, + @Optional() @Inject(COMPOSITION_BUFFER_MODE) private _compositionMode: boolean, + ) { super(renderer, elementRef); if (this._compositionMode == null) { this._compositionMode = !_isAndroid(); diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/number_value_accessor.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/number_value_accessor.ts index 274801c6f7..ebe3cc3d63 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/number_value_accessor.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/number_value_accessor.ts @@ -1,6 +1,6 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/forms/src/directives/number_value_accessor.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/forms/src/directives/number_value_accessor.ts * with the following modifications: * - Update imports * - Remove all configuration from NumberValueAccessor's `@Directive` decorator @@ -12,19 +12,23 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import {Directive, ElementRef, forwardRef, Provider} from '@angular/core'; import type {ControlValueAccessor} from '@angular/forms'; -import {BuiltInControlValueAccessor} from './control_value_accessor'; +import { + BuiltInControlValueAccessor, + // ControlValueAccessor, + // NG_VALUE_ACCESSOR, +} from './control_value_accessor'; /* [Nimble] Do not register as a value accessor provider const NUMBER_VALUE_ACCESSOR: Provider = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NumberValueAccessor), - multi: true + multi: true, }; */ @@ -55,14 +59,16 @@ const NUMBER_VALUE_ACCESSOR: Provider = { /* [Nimble] Remove all configuration from @Directive decorator @Directive({ selector: - 'input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]', + 'input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]', host: {'(input)': 'onChange($event.target.value)', '(blur)': 'onTouched()'}, - providers: [NUMBER_VALUE_ACCESSOR] + providers: [NUMBER_VALUE_ACCESSOR], }) */ @Directive() -export class NumberValueAccessor extends BuiltInControlValueAccessor implements - ControlValueAccessor { +export class NumberValueAccessor + extends BuiltInControlValueAccessor + implements ControlValueAccessor +{ /** * Sets the "value" property on the input element. * @nodoc @@ -77,7 +83,7 @@ export class NumberValueAccessor extends BuiltInControlValueAccessor implements * Registers a function called when the control value changes. * @nodoc */ - override registerOnChange(fn: (_: number|null) => void): void { + override registerOnChange(fn: (_: number | null) => void): void { this.onChange = (value) => { fn(value == '' ? null : parseFloat(value)); }; diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/radio_control_value_accessor.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/radio_control_value_accessor.ts index d2881caa20..16b75e8d1b 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/radio_control_value_accessor.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/radio_control_value_accessor.ts @@ -1,6 +1,6 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/forms/src/directives/radio_control_value_accessor.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/forms/src/directives/radio_control_value_accessor.ts * with the following modifications: * - Changed throwNameError() to throw Error instead of RuntimeError. This makes the file compile with Angular version 12. * - Removed now-unused import for RuntimeErrorCode and RuntimeError @@ -14,19 +14,40 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import {Directive, ElementRef, forwardRef, inject, Injectable, Injector, Input, OnDestroy, OnInit, Provider, Renderer2, ɵRuntimeError as RuntimeError} from '@angular/core'; +import { + Directive, + ElementRef, + forwardRef, + inject, + Injectable, + Injector, + Input, + OnDestroy, + OnInit, + Provider, + Renderer2, + ɵRuntimeError as RuntimeError, +} from '@angular/core'; -import {BuiltInControlValueAccessor} from './control_value_accessor'; +// import {RuntimeErrorCode} from '../errors'; + +import { + BuiltInControlValueAccessor, + // ControlValueAccessor, + // NG_VALUE_ACCESSOR, +} from './control_value_accessor'; import {ControlValueAccessor, NgControl, SetDisabledStateOption} from '@angular/forms'; +// import {NgControl} from './ng_control'; +// import {CALL_SET_DISABLED_STATE, setDisabledStateDefault} from './shared'; /* [Nimble] Do not register as a value accessor provider const RADIO_VALUE_ACCESSOR: Provider = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RadioControlValueAccessor), - multi: true + multi: true, }; */ @@ -34,10 +55,12 @@ function throwNameError() { /* [Nimble] RuntimeErrorCode is not exported from @angular/forms in version 12; falling back to version 12 behavior throw new RuntimeError(RuntimeErrorCode.NAME_AND_FORM_CONTROL_NAME_MUST_MATCH, ` */ - throw new Error(` + throw new Error( + ` If you define both a name and a formControlName attribute on your radio button, their values must match. Ex: - `); + ` + ); } /** @@ -82,12 +105,14 @@ export class RadioControlRegistry { } private _isSameGroup( - controlPair: [NgControl, RadioControlValueAccessor], - accessor: RadioControlValueAccessor): boolean { + controlPair: [NgControl, RadioControlValueAccessor], + accessor: RadioControlValueAccessor, + ): boolean { if (!controlPair[0].control) return false; - // @ts-expect-error: [Nimble] Use of internal NgControl member _parent - return controlPair[0]._parent === accessor._control._parent && - controlPair[1].name === accessor.name; + return ( + // @ts-expect-error: [Nimble] Use of internal NgControl member _parent + controlPair[0]._parent === accessor._control._parent && controlPair[1].name === accessor.name + ); } } @@ -114,14 +139,16 @@ export class RadioControlRegistry { /* [Nimble] Remove all configuration from @Directive decorator @Directive({ selector: - 'input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]', + 'input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]', host: {'(change)': 'onChange()', '(blur)': 'onTouched()'}, - providers: [RADIO_VALUE_ACCESSOR] + providers: [RADIO_VALUE_ACCESSOR], }) */ @Directive() -export class RadioControlValueAccessor extends BuiltInControlValueAccessor implements - ControlValueAccessor, OnDestroy, OnInit { +export class RadioControlValueAccessor + extends BuiltInControlValueAccessor + implements ControlValueAccessor, OnDestroy, OnInit +{ /** @internal */ // TODO(issue/24571): remove '!'. _state!: boolean; @@ -165,12 +192,15 @@ export class RadioControlValueAccessor extends BuiltInControlValueAccessor imple @Input() value: any; // [Nimble]: Can't override default behavior by injection token, because it is not exported. Inlining value of setDisabledStateDefault. - private callSetDisabledState: SetDisabledStateOption = 'always'; - //inject(CALL_SET_DISABLED_STATE, {optional: true}) ?? setDisabledStateDefault; + private callSetDisabledState = 'always'; + // inject(CALL_SET_DISABLED_STATE, {optional: true}) ?? setDisabledStateDefault; constructor( - renderer: Renderer2, elementRef: ElementRef, private _registry: RadioControlRegistry, - private _injector: Injector) { + renderer: Renderer2, + elementRef: ElementRef, + private _registry: RadioControlRegistry, + private _injector: Injector, + ) { super(renderer, elementRef); } @@ -227,8 +257,11 @@ export class RadioControlValueAccessor extends BuiltInControlValueAccessor imple * continues to work. Specifically, we drop the first call to `setDisabledState` if `disabled` * is `false`, and we are not in legacy mode. */ - if (this.setDisabledStateFired || isDisabled || - this.callSetDisabledState === 'whenDisabledForLegacyCode') { + if ( + this.setDisabledStateFired || + isDisabled || + this.callSetDisabledState === 'whenDisabledForLegacyCode' + ) { this.setProperty('disabled', isDisabled); } this.setDisabledStateFired = true; @@ -244,9 +277,13 @@ export class RadioControlValueAccessor extends BuiltInControlValueAccessor imple } private _checkName(): void { - if (this.name && this.formControlName && this.name !== this.formControlName && - // @ts-expect-error: [Nimble] ngDevMode is not defined - (typeof ngDevMode === 'undefined' || ngDevMode)) { + if ( + this.name && + this.formControlName && + this.name !== this.formControlName && + // @ts-expect-error: [Nimble] ngDevMode is not defined + (typeof ngDevMode === 'undefined' || ngDevMode) + ) { throwNameError(); } if (!this.name && this.formControlName) this.name = this.formControlName; diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/router_link.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/router_link.ts index a64f66d4e9..adc12465ac 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/router_link.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/router_link.ts @@ -1,7 +1,8 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/router/src/directives/router_link.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/router/src/directives/router_link.ts * with the following modifications: + * - Copy `isUrlTree` function from url_tree.ts into this file instead of importing * - Hardcode `isAnchorElement` to `true` so that the directive will correctly set the `href` on elements within nimble that represent anchors * - Make `href` a `@HostBindinding` to avoid using Angular's private sanitization APIs because `href` bindings automatically are sanitized by * Angular (see https://angular.io/guide/security#sanitization-and-security-contexts). Implementations leveraging RouterLink should have a test @@ -15,7 +16,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import {LocationStrategy} from '@angular/common'; @@ -31,13 +32,24 @@ import { OnChanges, OnDestroy, Renderer2, + ɵRuntimeError as RuntimeError, SimpleChanges, ɵɵsanitizeUrlOrResourceUrl, } from '@angular/core'; import {Subject, Subscription} from 'rxjs'; -import { Event, Params, QueryParamsHandling, ActivatedRoute, Router, NavigationEnd, UrlTree } from '@angular/router'; - +import {Event, NavigationEnd} from '@angular/router'; +import {QueryParamsHandling} from '@angular/router'; +import {Router} from '@angular/router'; +import {ActivatedRoute} from '@angular/router'; +import {Params} from '@angular/router'; +import {UrlTree} from '@angular/router'; +// import {RuntimeErrorCode} from '../errors'; + +// [Nimble] Copied from https://github.com/angular/angular/blob/18.2.13/packages/router/src/url_tree.ts +function isUrlTree(v: any): v is UrlTree { + return v instanceof UrlTree; +} /** * @description @@ -106,6 +118,9 @@ import { Event, Params, QueryParamsHandling, ActivatedRoute, Router, NavigationE * * ``` * + * `queryParams`, `fragment`, `queryParamsHandling`, `preserveFragment`, and `relativeTo` + * cannot be used when the `routerLink` input is a `UrlTree`. + * * See {@link UrlCreationOptions#queryParamsHandling}. * * ### Preserving navigation history @@ -204,8 +219,6 @@ export class RouterLink implements OnChanges, OnDestroy { */ @Input() relativeTo?: ActivatedRoute | null; - private commands: any[] | null = null; - /** Whether a host element is an `` tag. */ private isAnchorElement: boolean; @@ -275,7 +288,24 @@ export class RouterLink implements OnChanges, OnDestroy { } /** @nodoc */ - ngOnChanges(changes: SimpleChanges) { + // TODO(atscott): Remove changes parameter in major version as a breaking change. + ngOnChanges(changes?: SimpleChanges) { + /* [Nimble] Comment out extra error handling that uses ngDevMode and RuntimeErrorCode + if ( + ngDevMode && + isUrlTree(this.routerLinkInput) && + (this.fragment !== undefined || + this.queryParams || + this.queryParamsHandling || + this.preserveFragment || + this.relativeTo) + ) { + throw new RuntimeError( + RuntimeErrorCode.INVALID_ROUTER_LINK_INPUTS, + 'Cannot configure queryParams or fragment when using a UrlTree as the routerLink input value.', + ); + } + */ if (this.isAnchorElement) { this.updateHref(); } @@ -284,21 +314,31 @@ export class RouterLink implements OnChanges, OnDestroy { this.onChanges.next(this); } + private routerLinkInput: any[] | UrlTree | null = null; + /** - * Commands to pass to {@link Router#createUrlTree}. + * Commands to pass to {@link Router#createUrlTree} or a `UrlTree`. * - **array**: commands to pass to {@link Router#createUrlTree}. * - **string**: shorthand for array of commands with just the string, i.e. `['/route']` + * - **UrlTree**: a `UrlTree` for this link rather than creating one from the commands + * and other inputs that correspond to properties of `UrlCreationOptions`. * - **null|undefined**: effectively disables the `routerLink` * @see {@link Router#createUrlTree} */ @Input() - set routerLink(commands: any[] | string | null | undefined) { - if (commands != null) { - this.commands = Array.isArray(commands) ? commands : [commands]; - this.setTabIndexIfNotOnNativeEl('0'); - } else { - this.commands = null; + set routerLink(commandsOrUrlTree: any[] | string | UrlTree | null | undefined) { + if (commandsOrUrlTree == null) { + this.routerLinkInput = null; this.setTabIndexIfNotOnNativeEl(null); + } else { + if (isUrlTree(commandsOrUrlTree)) { + this.routerLinkInput = commandsOrUrlTree; + } else { + this.routerLinkInput = Array.isArray(commandsOrUrlTree) + ? commandsOrUrlTree + : [commandsOrUrlTree]; + } + this.setTabIndexIfNotOnNativeEl('0'); } } @@ -364,23 +404,23 @@ export class RouterLink implements OnChanges, OnDestroy { * to be a `@HostBinding`. This avoids the need of calling into private Angular sanitization code. */ // const sanitizedValue = - // this.href === null - // ? null - // : // This class represents a directive that can be added to both `` elements, - // // as well as other elements. As a result, we can't define security context at - // // compile time. So the security context is deferred to runtime. - // // The `ɵɵsanitizeUrlOrResourceUrl` selects the necessary sanitizer function - // // based on the tag and property names. The logic mimics the one from - // // `packages/compiler/src/schema/dom_security_schema.ts`, which is used at compile time. - // // - // // Note: we should investigate whether we can switch to using `@HostBinding('attr.href')` - // // instead of applying a value via a renderer, after a final merge of the - // // `RouterLinkWithHref` directive. - // ɵɵsanitizeUrlOrResourceUrl( - // this.href, - // this.el.nativeElement.tagName.toLowerCase(), - // 'href', - // ); + // this.href === null + // ? null + // : // This class represents a directive that can be added to both `` elements, + // // as well as other elements. As a result, we can't define security context at + // // compile time. So the security context is deferred to runtime. + // // The `ɵɵsanitizeUrlOrResourceUrl` selects the necessary sanitizer function + // // based on the tag and property names. The logic mimics the one from + // // `packages/compiler/src/schema/dom_security_schema.ts`, which is used at compile time. + // // + // // Note: we should investigate whether we can switch to using `@HostBinding('attr.href')` + // // instead of applying a value via a renderer, after a final merge of the + // // `RouterLinkWithHref` directive. + // ɵɵsanitizeUrlOrResourceUrl( + // this.href, + // this.el.nativeElement.tagName.toLowerCase(), + // 'href', + // ); // this.applyAttributeValue('href', sanitizedValue); } @@ -395,10 +435,12 @@ export class RouterLink implements OnChanges, OnDestroy { } get urlTree(): UrlTree | null { - if (this.commands === null) { + if (this.routerLinkInput === null) { return null; + } else if (isUrlTree(this.routerLinkInput)) { + return this.routerLinkInput; } - return this.router.createUrlTree(this.commands, { + return this.router.createUrlTree(this.routerLinkInput, { // If the `relativeTo` input is not defined, we want to use `this.route` by default. // Otherwise, we should use the value provided by the user in the input. relativeTo: this.relativeTo !== undefined ? this.relativeTo : this.route, @@ -419,4 +461,4 @@ export class RouterLink implements OnChanges, OnDestroy { // * @deprecated use `RouterLink` directive instead. // * @publicApi // */ -// export {RouterLink as RouterLinkWithHref}; +// export {RouterLink as RouterLinkWithHref}; \ No newline at end of file diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/select_control_value_accessor.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/select_control_value_accessor.ts index 63384091c0..3a3201a68b 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/select_control_value_accessor.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/select_control_value_accessor.ts @@ -1,6 +1,6 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/forms/src/directives/select_control_value_accessor.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/forms/src/directives/select_control_value_accessor.ts * with the following modifications: * - Update imports * - Remove all configuration from SelectControlValueAccessor's `@Directive` decorator @@ -14,23 +14,41 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import {Directive, ElementRef, forwardRef, Host, Input, OnDestroy, Optional, Provider, Renderer2, ɵRuntimeError as RuntimeError, isDevMode} from '@angular/core'; +import { + Directive, + ElementRef, + forwardRef, + Host, + Input, + OnDestroy, + Optional, + Provider, + Renderer2, + ɵRuntimeError as RuntimeError, + isDevMode, +} from '@angular/core'; + +// import {RuntimeErrorCode} from '../errors'; import type {ControlValueAccessor} from '@angular/forms'; -import {BuiltInControlValueAccessor} from './control_value_accessor'; +import { + BuiltInControlValueAccessor, + // ControlValueAccessor, + // NG_VALUE_ACCESSOR, +} from './control_value_accessor'; /* [Nimble] Do not register as a value accessor provider const SELECT_VALUE_ACCESSOR: Provider = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SelectControlValueAccessor), - multi: true + multi: true, }; */ -function _buildValueString(id: string|null, value: any): string { +function _buildValueString(id: string | null, value: any): string { if (id == null) return `${value}`; if (value && typeof value === 'object') value = 'Object'; return `${id}: ${value}`.slice(0, 50); @@ -99,14 +117,16 @@ function _extractId(valueString: string): string { /* [Nimble] Remove all configuration from @Directive decorator @Directive({ selector: - 'select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]', + 'select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]', host: {'(change)': 'onChange($event.target.value)', '(blur)': 'onTouched()'}, - providers: [SELECT_VALUE_ACCESSOR] + providers: [SELECT_VALUE_ACCESSOR], }) */ @Directive() -export class SelectControlValueAccessor extends BuiltInControlValueAccessor implements - ControlValueAccessor { +export class SelectControlValueAccessor + extends BuiltInControlValueAccessor + implements ControlValueAccessor +{ /** @nodoc */ value: any; @@ -126,8 +146,9 @@ export class SelectControlValueAccessor extends BuiltInControlValueAccessor impl // [Nimble] Update to use public APIs // if (typeof fn !== 'function' && (typeof ngDevMode === 'undefined' || ngDevMode)) { // throw new RuntimeError( - // RuntimeErrorCode.COMPAREWITH_NOT_A_FN, - // `compareWith must be a function, but received ${JSON.stringify(fn)}`); + // RuntimeErrorCode.COMPAREWITH_NOT_A_FN, + // `compareWith must be a function, but received ${JSON.stringify(fn)}`, + // ); // } if (typeof fn !== 'function' && isDevMode()) { throw new Error(`compareWith must be a function, but received ${JSON.stringify(fn)}`); @@ -143,7 +164,7 @@ export class SelectControlValueAccessor extends BuiltInControlValueAccessor impl */ writeValue(value: any): void { this.value = value; - const id: string|null = this._getOptionId(value); + const id: string | null = this._getOptionId(value); const valueString = _buildValueString(id, value); this.setProperty('value', valueString); } @@ -165,7 +186,7 @@ export class SelectControlValueAccessor extends BuiltInControlValueAccessor impl } /** @internal */ - _getOptionId(value: any): string|null { + _getOptionId(value: any): string | null { for (const id of this._optionMap.keys()) { if (this._compareWith(this._optionMap.get(id), value)) return id; } @@ -202,8 +223,10 @@ export class NgSelectOption implements OnDestroy { id!: string; constructor( - private _element: ElementRef, private _renderer: Renderer2, - @Optional() @Host() private _select: SelectControlValueAccessor) { + private _element: ElementRef, + private _renderer: Renderer2, + @Optional() @Host() private _select: SelectControlValueAccessor, + ) { if (this._select) this.id = this._select._registerOption(); } diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/tests/router_integration.spec.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/tests/router_integration.spec.ts index dd596cbb69..ae95c54686 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/tests/router_integration.spec.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/tests/router_integration.spec.ts @@ -1,6 +1,6 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/router/test/integration.spec.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/router/test/integration.spec.ts * with the following modifications: * - comment out all tests not involving RouterLink * - modify imports @@ -13,7 +13,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ // [Nimble] Update imports @@ -36,9 +36,10 @@ import { ViewChildren, ɵConsole as Console, ɵNoopNgZone as NoopNgZone, - Directive } from '@angular/core'; import {ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import '../../testing/matchers'; import { ActivatedRoute, ActivatedRouteSnapshot, @@ -70,6 +71,7 @@ import { Router, RouteReuseStrategy, RouterEvent, + // RouterLink, // [Nimble] Import from local copy (below) RouterLinkActive, RouterModule, RouterOutlet, @@ -81,14 +83,27 @@ import { UrlSegment, UrlSegmentGroup, UrlTree, - provideRouter } from '@angular/router'; +import {RouterLink} from '../router_link'; import {RouterTestingHarness} from '@angular/router/testing'; import type {concat, EMPTY, firstValueFrom, Observable, Observer, of, Subscription} from 'rxjs'; import {delay, filter, first, last, map, mapTo, takeWhile, tap} from 'rxjs/operators'; -import {RouterLink} from '../router_link'; -import {By} from '@angular/platform-browser'; -import '../../testing/matchers'; + +/* +import { + CanActivateChildFn, + CanActivateFn, + CanMatchFn, + Data, + ResolveFn, + RedirectCommand, + GuardResult, + MaybeAsync, +} from '../src/models'; +import {provideRouter, withNavigationErrorHandler, withRouterConfig} from '../src/provide_router'; +import {wrapIntoObservable} from '../src/utils/collection'; +import {getLoadedRoutes} from '../src/utils/config'; +*/ // [Nimble] Defining test directive and module to use instead of RouterLink @Directive({ selector: '[routerLink]' }) @@ -559,66 +574,6 @@ for (const browserAPI of ['history'] as const) { )); }); - describe('navigation warning', () => { - const isInAngularZoneFn = NgZone.isInAngularZone; - let warnings: string[] = []; - let isInAngularZone = true; - - class MockConsole { - warn(message: string) { - warnings.push(message); - } - } - - beforeEach(() => { - warnings = []; - isInAngularZone = true; - NgZone.isInAngularZone = () => isInAngularZone; - TestBed.overrideProvider(Console, {useValue: new MockConsole()}); - }); - - afterEach(() => { - NgZone.isInAngularZone = isInAngularZoneFn; - }); - - describe('with NgZone enabled', () => { - it('should warn when triggered outside Angular zone', fakeAsync( - inject([Router], (router: Router) => { - isInAngularZone = false; - router.navigateByUrl('/simple'); - - expect(warnings.length).toBe(1); - expect(warnings[0]).toBe( - `Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?`, - ); - }), - )); - - it('should not warn when triggered inside Angular zone', fakeAsync( - inject([Router], (router: Router) => { - router.navigateByUrl('/simple'); - - expect(warnings.length).toBe(0); - }), - )); - }); - - describe('with NgZone disabled', () => { - beforeEach(() => { - TestBed.overrideProvider(NgZone, {useValue: new NoopNgZone()}); - }); - - it('should not warn when triggered outside Angular zone', fakeAsync( - inject([Router], (router: Router) => { - isInAngularZone = false; - router.navigateByUrl('/simple'); - - expect(warnings.length).toBe(0); - }), - )); - }); - }); - describe('should execute navigations serially', () => { let log: Array = []; @@ -1965,15 +1920,82 @@ for (const browserAPI of ['history'] as const) { component: BlankCmp, }, ], + withRouterConfig({resolveNavigationPromiseOnError: true}), withNavigationErrorHandler(() => (coreInject(Handler).handlerCalled = true)), ), ], }); const router = TestBed.inject(Router); - await expectAsync(router.navigateByUrl('/throw')).toBeRejected(); + await router.navigateByUrl('/throw'); expect(TestBed.inject(Handler).handlerCalled).toBeTrue(); }); + it('can redirect from error handler', async () => { + TestBed.configureTestingModule({ + providers: [ + provideRouter( + [ + { + path: 'throw', + canMatch: [ + () => { + throw new Error(''); + }, + ], + component: BlankCmp, + }, + {path: 'error', component: BlankCmp}, + ], + withRouterConfig({resolveNavigationPromiseOnError: true}), + withNavigationErrorHandler( + () => new RedirectCommand(coreInject(Router).parseUrl('/error')), + ), + ), + ], + }); + const router = TestBed.inject(Router); + let emitNavigationError = false; + let emitNavigationCancelWithRedirect = false; + router.events.subscribe((e) => { + if (e instanceof NavigationError) { + emitNavigationError = true; + } + if (e instanceof NavigationCancel && e.code === NavigationCancellationCode.Redirect) { + emitNavigationCancelWithRedirect = true; + } + }); + await router.navigateByUrl('/throw'); + expect(router.url).toEqual('/error'); + expect(emitNavigationError).toBe(false); + expect(emitNavigationCancelWithRedirect).toBe(true); + }); + + it('should not break navigation if an error happens in NavigationErrorHandler', async () => { + TestBed.configureTestingModule({ + providers: [ + provideRouter( + [ + { + path: 'throw', + canMatch: [ + () => { + throw new Error(''); + }, + ], + component: BlankCmp, + }, + {path: '**', component: BlankCmp}, + ], + withRouterConfig({resolveNavigationPromiseOnError: true}), + withNavigationErrorHandler(() => { + throw new Error('e'); + }), + ), + ], + }); + const router = TestBed.inject(Router); + }); + // Errors should behave the same for both deferred and eager URL update strategies (['deferred', 'eager'] as const).forEach((urlUpdateStrategy) => { it('should dispatch NavigationError after the url has been reset back', fakeAsync(() => { @@ -2158,7 +2180,7 @@ for (const browserAPI of ['history'] as const) { let e: any; router.navigateByUrl('/invalid')!.then((_) => (e = _)); advance(fixture); - expect(e).toEqual('resolvedValue'); + expect(e).toEqual(true); expectEvents(recordedEvents, [ [NavigationStart, '/invalid'], @@ -3987,6 +4009,121 @@ for (const browserAPI of ['history'] as const) { tick(); expect(resolvedPath).toBe('/redirected'); })); + + it('can redirect to 404 without changing the URL', fakeAsync(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))], + }); + const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + router.resetConfig([ + {path: '', component: SimpleCmp}, + { + path: 'one', + component: RouteCmp, + canActivate: [ + () => new RedirectCommand(router.parseUrl('/404'), {skipLocationChange: true}), + ], + }, + {path: '404', component: SimpleCmp}, + ]); + const fixture = createRoot(router, RootCmp); + router.navigateByUrl('/one'); + + advance(fixture); + + expect(location.path()).toEqual('/one'); + expect(router.url.toString()).toEqual('/404'); + })); + + it('can redirect while changing state object', fakeAsync(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))], + }); + const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + router.resetConfig([ + {path: '', component: SimpleCmp}, + { + path: 'one', + component: RouteCmp, + canActivate: [ + () => new RedirectCommand(router.parseUrl('/redirected'), {state: {test: 1}}), + ], + }, + {path: 'redirected', component: SimpleCmp}, + ]); + const fixture = createRoot(router, RootCmp); + router.navigateByUrl('/one'); + + advance(fixture); + + expect(location.path()).toEqual('/redirected'); + expect(location.getState()).toEqual(jasmine.objectContaining({test: 1})); + })); + }); + + it('can redirect to 404 without changing the URL', async () => { + TestBed.configureTestingModule({ + providers: [ + provideRouter([ + { + path: 'one', + component: RouteCmp, + canActivate: [ + () => { + const router = coreInject(Router); + router.navigateByUrl('/404', { + browserUrl: router.getCurrentNavigation()?.finalUrl, + }); + return false; + }, + ], + }, + {path: '404', component: SimpleCmp}, + ]), + ], + }); + const location = TestBed.inject(Location); + await RouterTestingHarness.create('/one'); + + expect(location.path()).toEqual('/one'); + expect(TestBed.inject(Router).url.toString()).toEqual('/404'); + }); + + it('can navigate to same internal route with different browser url', async () => { + TestBed.configureTestingModule({ + providers: [provideRouter([{path: 'one', component: RouteCmp}])], + }); + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); + await RouterTestingHarness.create('/one'); + await router.navigateByUrl('/one', {browserUrl: '/two'}); + + expect(location.path()).toEqual('/two'); + expect(router.url.toString()).toEqual('/one'); + }); + + it('retains browserUrl through UrlTree redirects', async () => { + TestBed.configureTestingModule({ + providers: [ + provideRouter([ + { + path: 'one', + component: RouteCmp, + canActivate: [() => coreInject(Router).parseUrl('/404')], + }, + {path: '404', component: SimpleCmp}, + ]), + ], + }); + const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + await RouterTestingHarness.create(); + await router.navigateByUrl('/one', {browserUrl: router.parseUrl('abc123')}); + + expect(location.path()).toEqual('/abc123'); + expect(TestBed.inject(Router).url.toString()).toEqual('/404'); }); describe('runGuardsAndResolvers', () => { diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/tests/router_link.spec.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/tests/router_link.spec.ts index 714bfb3789..cba900f00d 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/tests/router_link.spec.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/tests/router_link.spec.ts @@ -1,6 +1,6 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/router/test/router_link_spec.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/router/test/router_link_spec.ts * with the following modifications: * - replace import of Angular's RouterLink with our forked version * - define TestRouterLinkDirective to use in tests, and add it to declarations of testing modules @@ -12,20 +12,24 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import {Component, Directive} from '@angular/core'; +import {Component, Directive, inject, signal, provideExperimentalZonelessChangeDetection} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; -import {Router, RouterLink, RouterModule} from '@angular/router'; +import {Router, RouterLink, RouterModule, provideRouter} from '@angular/router'; describe('RouterLink', () => { // [Nimble] Defining test directive to use instead of RouterLink @Directive({ selector: '[routerLink]' }) class TestRouterLinkDirective extends RouterLink {} - it('does not modify tabindex if already set on non-anchor element', () => { + beforeEach(() => { + TestBed.configureTestingModule({providers: [provideExperimentalZonelessChangeDetection()]}); + }); + + it('does not modify tabindex if already set on non-anchor element', async () => { @Component({template: `
`}) class LinkComponent { link: string | null | undefined = '/'; @@ -36,12 +40,12 @@ describe('RouterLink', () => { declarations: [LinkComponent, TestRouterLinkDirective], }); const fixture = TestBed.createComponent(LinkComponent); - fixture.detectChanges(); + await fixture.whenStable(); const link = fixture.debugElement.query(By.css('div')).nativeElement; expect(link.tabIndex).toEqual(1); fixture.nativeElement.link = null; - fixture.detectChanges(); + await fixture.whenStable(); expect(link.tabIndex).toEqual(1); }); @@ -49,30 +53,30 @@ describe('RouterLink', () => { @Component({ template: `
+ [routerLink]="link()" + [preserveFragment]="preserveFragment()" + [skipLocationChange]="skipLocationChange()" + [replaceUrl]="replaceUrl()"> `, }) class LinkComponent { - link: string | null | undefined = '/'; - preserveFragment: unknown; - skipLocationChange: unknown; - replaceUrl: unknown; + link = signal('/'); + preserveFragment = signal(undefined); + skipLocationChange = signal(undefined); + replaceUrl = signal(undefined); } let fixture: ComponentFixture; let link: HTMLDivElement; let router: Router; - beforeEach(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [RouterModule.forRoot([])], // [Nimble] Declare TestRouterLinkDirective declarations: [LinkComponent, TestRouterLinkDirective], }); fixture = TestBed.createComponent(LinkComponent); - fixture.detectChanges(); + await fixture.whenStable(); link = fixture.debugElement.query(By.css('div')).nativeElement; router = TestBed.inject(Router); @@ -82,43 +86,43 @@ describe('RouterLink', () => { (router.navigateByUrl as jasmine.Spy).calls.reset(); }); - it('null, removes tabIndex and does not navigate', () => { - fixture.componentInstance.link = null; - fixture.detectChanges(); + it('null, removes tabIndex and does not navigate', async () => { + fixture.componentInstance.link.set(null); + await fixture.whenStable(); expect(link.tabIndex).toEqual(-1); link.click(); expect(router.navigateByUrl).not.toHaveBeenCalled(); }); - it('undefined, removes tabIndex and does not navigate', () => { - fixture.componentInstance.link = undefined; - fixture.detectChanges(); + it('undefined, removes tabIndex and does not navigate', async () => { + fixture.componentInstance.link.set(undefined); + await fixture.whenStable(); expect(link.tabIndex).toEqual(-1); link.click(); expect(router.navigateByUrl).not.toHaveBeenCalled(); }); - it('should coerce boolean input values', () => { + it('should coerce boolean input values', async () => { // [Nimble] Use directive TestRouterLinkDirective instead of RouterLink const dir = fixture.debugElement.query(By.directive(TestRouterLinkDirective)).injector.get(TestRouterLinkDirective); for (const truthy of [true, '', 'true', 'anything']) { - fixture.componentInstance.preserveFragment = truthy; - fixture.componentInstance.skipLocationChange = truthy; - fixture.componentInstance.replaceUrl = truthy; - fixture.detectChanges(); + fixture.componentInstance.preserveFragment.set(truthy); + fixture.componentInstance.skipLocationChange.set(truthy); + fixture.componentInstance.replaceUrl.set(truthy); + await fixture.whenStable(); expect(dir.preserveFragment).toBeTrue(); expect(dir.skipLocationChange).toBeTrue(); expect(dir.replaceUrl).toBeTrue(); } for (const falsy of [false, null, undefined, 'false']) { - fixture.componentInstance.preserveFragment = falsy; - fixture.componentInstance.skipLocationChange = falsy; - fixture.componentInstance.replaceUrl = falsy; - fixture.detectChanges(); + fixture.componentInstance.preserveFragment.set(falsy); + fixture.componentInstance.skipLocationChange.set(falsy); + fixture.componentInstance.replaceUrl.set(falsy); + await fixture.whenStable(); expect(dir.preserveFragment).toBeFalse(); expect(dir.skipLocationChange).toBeFalse(); expect(dir.replaceUrl).toBeFalse(); @@ -131,65 +135,65 @@ describe('RouterLink', () => { @Component({ template: `
+ [routerLink]="link()" + [preserveFragment]="preserveFragment()" + [skipLocationChange]="skipLocationChange()" + [replaceUrl]="replaceUrl()"> `, }) class LinkComponent { - link: string | null | undefined = '/'; - preserveFragment: unknown; - skipLocationChange: unknown; - replaceUrl: unknown; + link = signal('/'); + preserveFragment = signal(undefined); + skipLocationChange = signal(undefined); + replaceUrl = signal(undefined); } let fixture: ComponentFixture; let link: HTMLAnchorElement; - beforeEach(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [RouterModule.forRoot([])], // [Nimble] Declare TestRouterLinkDirective declarations: [LinkComponent, TestRouterLinkDirective], }); fixture = TestBed.createComponent(LinkComponent); - fixture.detectChanges(); + await fixture.whenStable(); link = fixture.debugElement.query(By.css('a')).nativeElement; }); - it('null, removes href', () => { + it('null, removes href', async () => { expect(link.outerHTML).toContain('href'); - fixture.componentInstance.link = null; - fixture.detectChanges(); + fixture.componentInstance.link.set(null); + await fixture.whenStable(); expect(link.outerHTML).not.toContain('href'); }); - it('undefined, removes href', () => { + it('undefined, removes href', async () => { expect(link.outerHTML).toContain('href'); - fixture.componentInstance.link = undefined; - fixture.detectChanges(); + fixture.componentInstance.link.set(undefined); + await fixture.whenStable(); expect(link.outerHTML).not.toContain('href'); }); - it('should coerce boolean input values', () => { + it('should coerce boolean input values', async () => { // [Nimble] Use directive TestRouterLinkDirective instead of RouterLink const dir = fixture.debugElement.query(By.directive(TestRouterLinkDirective)).injector.get(TestRouterLinkDirective); for (const truthy of [true, '', 'true', 'anything']) { - fixture.componentInstance.preserveFragment = truthy; - fixture.componentInstance.skipLocationChange = truthy; - fixture.componentInstance.replaceUrl = truthy; - fixture.detectChanges(); + fixture.componentInstance.preserveFragment.set(truthy); + fixture.componentInstance.skipLocationChange.set(truthy); + fixture.componentInstance.replaceUrl.set(truthy); + await fixture.whenStable(); expect(dir.preserveFragment).toBeTrue(); expect(dir.skipLocationChange).toBeTrue(); expect(dir.replaceUrl).toBeTrue(); } for (const falsy of [false, null, undefined, 'false']) { - fixture.componentInstance.preserveFragment = falsy; - fixture.componentInstance.skipLocationChange = falsy; - fixture.componentInstance.replaceUrl = falsy; - fixture.detectChanges(); + fixture.componentInstance.preserveFragment.set(falsy); + fixture.componentInstance.skipLocationChange.set(falsy); + fixture.componentInstance.replaceUrl.set(falsy); + await fixture.whenStable(); expect(dir.preserveFragment).toBeFalse(); expect(dir.skipLocationChange).toBeFalse(); expect(dir.replaceUrl).toBeFalse(); @@ -197,7 +201,7 @@ describe('RouterLink', () => { }); }); - it('should handle routerLink in svg templates', () => { + it('should handle routerLink in svg templates', async () => { @Component({template: ``}) class LinkComponent {} @@ -207,10 +211,41 @@ describe('RouterLink', () => { declarations: [LinkComponent, TestRouterLinkDirective], }); const fixture = TestBed.createComponent(LinkComponent); - fixture.detectChanges(); + await fixture.whenStable(); const link = fixture.debugElement.query(By.css('a')).nativeElement; expect(link.outerHTML).toContain('href'); }); }); + + it('can use a UrlTree as the input', async () => { + @Component({ + standalone: true, + template: 'link', + imports: [RouterLink], + }) + class WithUrlTree { + urlTree = inject(Router).createUrlTree(['/a/b/c']); + } + TestBed.configureTestingModule({providers: [provideRouter([])]}); + + const fixture = TestBed.createComponent(WithUrlTree); + await fixture.whenStable(); + expect(fixture.nativeElement.innerHTML).toContain('href="/a/b/c"'); + }); + + it('cannnot use a UrlTree with queryParams', () => { + @Component({ + standalone: true, + template: 'link', + imports: [RouterLink], + }) + class WithUrlTree { + urlTree = inject(Router).createUrlTree(['/a/b/c']); + } + TestBed.configureTestingModule({providers: [provideRouter([])]}); + + const fixture = TestBed.createComponent(WithUrlTree); + expect(() => fixture.changeDetectorRef.detectChanges()).toThrow(); + }); }); \ No newline at end of file diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/tests/value_accessor_integration.spec.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/tests/value_accessor_integration.spec.ts index 91599b366c..656957786c 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/directives/tests/value_accessor_integration.spec.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/directives/tests/value_accessor_integration.spec.ts @@ -1,6 +1,6 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/forms/test/value_accessor_integration_spec.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/forms/test/value_accessor_integration_spec.ts * with the following modifications: * - Clear the selector for built-in CVAs to keep those directives from being used within the tests * - Create test CVAs that extend the ones copied into `thirdparty/directives` so that those directives will be used in the tests @@ -13,13 +13,44 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ // [Nimble] Update imports -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, EventEmitter, Input, Output, Type, ViewChild, forwardRef} from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Directive, + EventEmitter, + Input, + Output, + Type, + ViewChild, + forwardRef, +} from '@angular/core'; import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; -import {AbstractControl, ControlValueAccessor, FormControl, FormGroup, FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgControl, NgForm, NgModel, ReactiveFormsModule, Validators, DefaultValueAccessor as AngularDefaultValueAccessor, CheckboxControlValueAccessor as AngularCheckboxControlValueAccessor, NumberValueAccessor as AngularNumberValueAccessor, RadioControlValueAccessor as AngularRadioControlValueAccessor, SelectControlValueAccessor as AngularSelectControlValueAccessor, NgSelectOption as AngularNgSelectOption} from '@angular/forms'; +import { + AbstractControl, + ControlValueAccessor, + FormControl, + FormGroup, + FormsModule, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + NgControl, + NgForm, + NgModel, + ReactiveFormsModule, + Validators, + // [Nimble] additions + DefaultValueAccessor as AngularDefaultValueAccessor, + CheckboxControlValueAccessor as AngularCheckboxControlValueAccessor, + NumberValueAccessor as AngularNumberValueAccessor, + RadioControlValueAccessor as AngularRadioControlValueAccessor, + SelectControlValueAccessor as AngularSelectControlValueAccessor, + NgSelectOption as AngularNgSelectOption +} from '@angular/forms'; import {By} from '@angular/platform-browser'; import {CheckboxControlValueAccessor} from '../checkbox_value_accessor'; import {DefaultValueAccessor} from '../default_value_accessor'; @@ -117,12 +148,10 @@ const angularDirectivesToOverwrite = [ describe('value accessors', () => { function initTest(component: Type, ...directives: Type[]): ComponentFixture { - TestBed.configureTestingModule( - { - // [Nimble] update declarations to add necessary directives - declarations: [component, TestCheckboxControlValueAccessor, TestDefaultValueAccessor, TestNumberValueAccessor, TestRadioControlValueAccessor, TestSelectControlValueAccessor, TestNgSelectOption, ...directives], - imports: [FormsModule, ReactiveFormsModule] - }); + TestBed.configureTestingModule({ + declarations: [component, TestCheckboxControlValueAccessor, TestDefaultValueAccessor, TestNumberValueAccessor, TestRadioControlValueAccessor, TestSelectControlValueAccessor, TestNgSelectOption, ...directives], + imports: [FormsModule, ReactiveFormsModule], + }); // [Nimble] Overwrite the selector for each Angular directive that shouldn't be used in the tests angularDirectivesToOverwrite.forEach(x => { TestBed.overrideDirective(x, { set: { selector: 'matches-nothing' } }); @@ -131,8 +160,9 @@ describe('value accessors', () => { } it('should support without type', () => { - TestBed.overrideComponent( - FormControlComp, {set: {template: ``}}); + TestBed.overrideComponent(FormControlComp, { + set: {template: ``}, + }); const fixture = initTest(FormControlComp); const control = new FormControl('old'); fixture.componentInstance.control = control; @@ -176,7 +206,7 @@ describe('value accessors', () => { form.valueChanges.subscribe({ next: (value) => { throw 'Should not happen'; - } + }, }); input.nativeElement.value = 'updatedValue'; @@ -186,8 +216,9 @@ describe('value accessors', () => { }); it('should support `}}); + TestBed.overrideComponent(FormControlComp, { + set: {template: ``}, + }); const fixture = initTest(FormControlComp); const control = new FormControl('old'); fixture.componentInstance.control = control; @@ -205,8 +236,9 @@ describe('value accessors', () => { }); it('should support ', () => { - TestBed.overrideComponent( - FormControlComp, {set: {template: ``}}); + TestBed.overrideComponent(FormControlComp, { + set: {template: ``}, + }); const fixture = initTest(FormControlComp); const control = new FormControl(true); fixture.componentInstance.control = control; @@ -270,7 +302,7 @@ describe('value accessors', () => { control.valueChanges.subscribe({ next: (value) => { throw 'Input[number] should not react to change event'; - } + }, }); const input = fixture.debugElement.query(By.css('input')); @@ -332,8 +364,9 @@ describe('value accessors', () => { it('should throw an error if compareWith is not a function', () => { const fixture = initTest(FormControlSelectWithCompareFn); fixture.componentInstance.compareFn = null!; - expect(() => fixture.detectChanges()) - .toThrowError(/compareWith must be a function, but received null/); + expect(() => fixture.detectChanges()).toThrowError( + /compareWith must be a function, but received null/, + ); }); it('should compare options using provided compareWith function', () => { @@ -363,7 +396,10 @@ describe('value accessors', () => { expect(fixture.componentInstance.form.value).toEqual({city: {id: 2, name: 'NY'}}); - fixture.componentInstance.cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; + fixture.componentInstance.cities = [ + {id: 1, name: 'SF'}, + {id: 2, name: 'NY'}, + ]; fixture.detectChanges(); // Now that the options array has been re-assigned, new option instances will @@ -378,166 +414,176 @@ describe('value accessors', () => { describe('in template-driven forms', () => { it('with option values that are objects', fakeAsync(() => { - // [Nimble] Remove isNode check - // if (isNode) return; - const fixture = initTest(NgModelSelectForm); - const comp = fixture.componentInstance; - comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}]; - comp.selectedCity = comp.cities[1]; - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - const nycOption = fixture.debugElement.queryAll(By.css('option'))[1]; - - // model -> view - expect(select.nativeElement.value).toEqual('1: Object'); - expect(nycOption.nativeElement.selected).toBe(true); - - select.nativeElement.value = '2: Object'; - dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); - tick(); - - // view -> model - expect(comp.selectedCity['name']).toEqual('Buffalo'); - })); + // [Nimble] Remove isNode check + // if (isNode) return; + const fixture = initTest(NgModelSelectForm); + const comp = fixture.componentInstance; + comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}]; + comp.selectedCity = comp.cities[1]; + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + const nycOption = fixture.debugElement.queryAll(By.css('option'))[1]; + + // model -> view + expect(select.nativeElement.value).toEqual('1: Object'); + expect(nycOption.nativeElement.selected).toBe(true); + + select.nativeElement.value = '2: Object'; + dispatchEvent(select.nativeElement, 'change'); + fixture.detectChanges(); + tick(); + + // view -> model + expect(comp.selectedCity['name']).toEqual('Buffalo'); + })); it('when new options are added', fakeAsync(() => { - // [Nimble] Remove isNode check - // if (isNode) return; - const fixture = initTest(NgModelSelectForm); - const comp = fixture.componentInstance; - comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; - comp.selectedCity = comp.cities[1]; - fixture.detectChanges(); - tick(); - - comp.cities.push({'name': 'Buffalo'}); - comp.selectedCity = comp.cities[2]; - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - const buffalo = fixture.debugElement.queryAll(By.css('option'))[2]; - expect(select.nativeElement.value).toEqual('2: Object'); - expect(buffalo.nativeElement.selected).toBe(true); - })); + // [Nimble] Remove isNode check + // if (isNode) return; + const fixture = initTest(NgModelSelectForm); + const comp = fixture.componentInstance; + comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; + comp.selectedCity = comp.cities[1]; + fixture.detectChanges(); + tick(); + + comp.cities.push({'name': 'Buffalo'}); + comp.selectedCity = comp.cities[2]; + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + const buffalo = fixture.debugElement.queryAll(By.css('option'))[2]; + expect(select.nativeElement.value).toEqual('2: Object'); + expect(buffalo.nativeElement.selected).toBe(true); + })); it('when options are removed', fakeAsync(() => { - const fixture = initTest(NgModelSelectForm); - const comp = fixture.componentInstance; - comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; - comp.selectedCity = comp.cities[1]; - fixture.detectChanges(); - tick(); + const fixture = initTest(NgModelSelectForm); + const comp = fixture.componentInstance; + comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; + comp.selectedCity = comp.cities[1]; + fixture.detectChanges(); + tick(); - const select = fixture.debugElement.query(By.css('select')); - expect(select.nativeElement.value).toEqual('1: Object'); + const select = fixture.debugElement.query(By.css('select')); + expect(select.nativeElement.value).toEqual('1: Object'); - comp.cities.pop(); - fixture.detectChanges(); - tick(); + comp.cities.pop(); + fixture.detectChanges(); + tick(); - expect(select.nativeElement.value).not.toEqual('1: Object'); - })); + expect(select.nativeElement.value).not.toEqual('1: Object'); + })); it('when option values have same content, but different identities', fakeAsync(() => { - // [Nimble] Remove isNode check - // if (isNode) return; - const fixture = initTest(NgModelSelectForm); - const comp = fixture.componentInstance; - comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'NYC'}]; - comp.selectedCity = comp.cities[0]; - fixture.detectChanges(); - - comp.selectedCity = comp.cities[2]; - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - const secondNYC = fixture.debugElement.queryAll(By.css('option'))[2]; - expect(select.nativeElement.value).toEqual('2: Object'); - expect(secondNYC.nativeElement.selected).toBe(true); - })); + // [Nimble] Remove isNode check + // if (isNode) return; + const fixture = initTest(NgModelSelectForm); + const comp = fixture.componentInstance; + comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'NYC'}]; + comp.selectedCity = comp.cities[0]; + fixture.detectChanges(); + + comp.selectedCity = comp.cities[2]; + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + const secondNYC = fixture.debugElement.queryAll(By.css('option'))[2]; + expect(select.nativeElement.value).toEqual('2: Object'); + expect(secondNYC.nativeElement.selected).toBe(true); + })); it('should work with null option', fakeAsync(() => { - const fixture = initTest(NgModelSelectWithNullForm); - const comp = fixture.componentInstance; - comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; - comp.selectedCity = null; - fixture.detectChanges(); - - const select = fixture.debugElement.query(By.css('select')); - - select.nativeElement.value = '2: Object'; - dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); - tick(); - expect(comp.selectedCity!['name']).toEqual('NYC'); - - select.nativeElement.value = '0: null'; - dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); - tick(); - expect(comp.selectedCity).toEqual(null); - })); + const fixture = initTest(NgModelSelectWithNullForm); + const comp = fixture.componentInstance; + comp.cities = [{'name': 'SF'}, {'name': 'NYC'}]; + comp.selectedCity = null; + fixture.detectChanges(); + + const select = fixture.debugElement.query(By.css('select')); + + select.nativeElement.value = '2: Object'; + dispatchEvent(select.nativeElement, 'change'); + fixture.detectChanges(); + tick(); + expect(comp.selectedCity!['name']).toEqual('NYC'); + + select.nativeElement.value = '0: null'; + dispatchEvent(select.nativeElement, 'change'); + fixture.detectChanges(); + tick(); + expect(comp.selectedCity).toEqual(null); + })); it('should throw an error when compareWith is not a function', () => { const fixture = initTest(NgModelSelectWithCustomCompareFnForm); const comp = fixture.componentInstance; comp.compareFn = null!; - expect(() => fixture.detectChanges()) - .toThrowError(/compareWith must be a function, but received null/); + expect(() => fixture.detectChanges()).toThrowError( + /compareWith must be a function, but received null/, + ); }); it('should compare options using provided compareWith function', fakeAsync(() => { - // [Nimble] Remove isNode check - // if (isNode) return; - const fixture = initTest(NgModelSelectWithCustomCompareFnForm); - const comp = fixture.componentInstance; - comp.selectedCity = {id: 1, name: 'SF'}; - comp.cities = [{id: 1, name: 'SF'}, {id: 2, name: 'LA'}]; - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - const sfOption = fixture.debugElement.query(By.css('option')); - expect(select.nativeElement.value).toEqual('0: Object'); - expect(sfOption.nativeElement.selected).toBe(true); - })); + // [Nimble] Remove isNode check + // if (isNode) return; + const fixture = initTest(NgModelSelectWithCustomCompareFnForm); + const comp = fixture.componentInstance; + comp.selectedCity = {id: 1, name: 'SF'}; + comp.cities = [ + {id: 1, name: 'SF'}, + {id: 2, name: 'LA'}, + ]; + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + const sfOption = fixture.debugElement.query(By.css('option')); + expect(select.nativeElement.value).toEqual('0: Object'); + expect(sfOption.nativeElement.selected).toBe(true); + })); it('should support re-assigning the options array with compareWith', fakeAsync(() => { - // [Nimble] Remove isNode check - // if (isNode) return; - const fixture = initTest(NgModelSelectWithCustomCompareFnForm); - fixture.componentInstance.selectedCity = {id: 1, name: 'SF'}; - fixture.componentInstance.cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; - fixture.detectChanges(); - tick(); - - // Option IDs start out as 0 and 1, so setting the select value to "1: Object" - // will select the second option (NY). - const select = fixture.debugElement.query(By.css('select')); - select.nativeElement.value = '1: Object'; - dispatchEvent(select.nativeElement, 'change'); - fixture.detectChanges(); - - const model = fixture.debugElement.children[0].injector.get(NgModel); - expect(model.value).toEqual({id: 2, name: 'NY'}); - - fixture.componentInstance.cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; - fixture.detectChanges(); - tick(); - - // Now that the options array has been re-assigned, new option instances will - // be created by ngFor. These instances will have different option IDs, subsequent - // to the first: 2 and 3. For the second option to stay selected, the select - // value will need to have the ID of the current second option: 3. - const nyOption = fixture.debugElement.queryAll(By.css('option'))[1]; - expect(select.nativeElement.value).toEqual('3: Object'); - expect(nyOption.nativeElement.selected).toBe(true); - })); + // [Nimble] Remove isNode check + // if (isNode) return; + const fixture = initTest(NgModelSelectWithCustomCompareFnForm); + fixture.componentInstance.selectedCity = {id: 1, name: 'SF'}; + fixture.componentInstance.cities = [ + {id: 1, name: 'SF'}, + {id: 2, name: 'NY'}, + ]; + fixture.detectChanges(); + tick(); + + // Option IDs start out as 0 and 1, so setting the select value to "1: Object" + // will select the second option (NY). + const select = fixture.debugElement.query(By.css('select')); + select.nativeElement.value = '1: Object'; + dispatchEvent(select.nativeElement, 'change'); + fixture.detectChanges(); + + const model = fixture.debugElement.children[0].injector.get(NgModel); + expect(model.value).toEqual({id: 2, name: 'NY'}); + + fixture.componentInstance.cities = [ + {id: 1, name: 'SF'}, + {id: 2, name: 'NY'}, + ]; + fixture.detectChanges(); + tick(); + + // Now that the options array has been re-assigned, new option instances will + // be created by ngFor. These instances will have different option IDs, subsequent + // to the first: 2 and 3. For the second option to stay selected, the select + // value will need to have the ID of the current second option: 3. + const nyOption = fixture.debugElement.queryAll(By.css('option'))[1]; + expect(select.nativeElement.value).toEqual('3: Object'); + expect(nyOption.nativeElement.selected).toBe(true); + })); }); }); @@ -569,21 +615,22 @@ describe('value accessors', () => { it('should throw an error when compareWith is not a function', () => { const fixture = initTest(FormControlSelectMultipleWithCompareFn); fixture.componentInstance.compareFn = null!; - expect(() => fixture.detectChanges()) - .toThrowError(/compareWith must be a function, but received null/); + expect(() => fixture.detectChanges()).toThrowError( + /compareWith must be a function, but received null/, + ); }); it('should compare options using provided compareWith function', fakeAsync(() => { - if (isNode) return; - const fixture = initTest(FormControlSelectMultipleWithCompareFn); - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - const sfOption = fixture.debugElement.query(By.css('option')); - expect(select.nativeElement.value).toEqual('0: Object'); - expect(sfOption.nativeElement.selected).toBe(true); - })); + if (isNode) return; + const fixture = initTest(FormControlSelectMultipleWithCompareFn); + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + const sfOption = fixture.debugElement.query(By.css('option')); + expect(select.nativeElement.value).toEqual('0: Object'); + expect(sfOption.nativeElement.selected).toBe(true); + })); }); describe('in template-driven forms', () => { @@ -623,69 +670,73 @@ describe('value accessors', () => { } }; - it('verify that native `selectedOptions` field is used while detecting the list of selected options', - fakeAsync(() => { - if (isNode || !HTMLSelectElement.prototype.hasOwnProperty('selectedOptions')) return; - const spy = spyOnProperty(HTMLSelectElement.prototype, 'selectedOptions', 'get') - .and.callThrough(); - setSelectedCities([]); - - selectOptionViaUI('1: Object'); - assertOptionElementSelectedState([false, true, false]); - expect(spy).toHaveBeenCalled(); - })); - - it('should reflect state of model after option selected and new options subsequently added', - fakeAsync(() => { - if (isNode) return; - setSelectedCities([]); + it('verify that native `selectedOptions` field is used while detecting the list of selected options', fakeAsync(() => { + if (isNode || !HTMLSelectElement.prototype.hasOwnProperty('selectedOptions')) return; + const spy = spyOnProperty( + HTMLSelectElement.prototype, + 'selectedOptions', + 'get', + ).and.callThrough(); + setSelectedCities([]); + + selectOptionViaUI('1: Object'); + assertOptionElementSelectedState([false, true, false]); + expect(spy).toHaveBeenCalled(); + })); + + it('should reflect state of model after option selected and new options subsequently added', fakeAsync(() => { + if (isNode) return; + setSelectedCities([]); - selectOptionViaUI('1: Object'); - assertOptionElementSelectedState([false, true, false]); + selectOptionViaUI('1: Object'); + assertOptionElementSelectedState([false, true, false]); - comp.cities.push({'name': 'Chicago'}); - detectChangesAndTick(); + comp.cities.push({'name': 'Chicago'}); + detectChangesAndTick(); - assertOptionElementSelectedState([false, true, false, false]); - })); + assertOptionElementSelectedState([false, true, false, false]); + })); - it('should reflect state of model after option selected and then other options removed', - fakeAsync(() => { - if (isNode) return; - setSelectedCities([]); + it('should reflect state of model after option selected and then other options removed', fakeAsync(() => { + if (isNode) return; + setSelectedCities([]); - selectOptionViaUI('1: Object'); - assertOptionElementSelectedState([false, true, false]); + selectOptionViaUI('1: Object'); + assertOptionElementSelectedState([false, true, false]); - comp.cities.pop(); - detectChangesAndTick(); + comp.cities.pop(); + detectChangesAndTick(); - assertOptionElementSelectedState([false, true]); - })); + assertOptionElementSelectedState([false, true]); + })); }); it('should throw an error when compareWith is not a function', () => { const fixture = initTest(NgModelSelectMultipleWithCustomCompareFnForm); const comp = fixture.componentInstance; comp.compareFn = null!; - expect(() => fixture.detectChanges()) - .toThrowError(/compareWith must be a function, but received null/); + expect(() => fixture.detectChanges()).toThrowError( + /compareWith must be a function, but received null/, + ); }); it('should compare options using provided compareWith function', fakeAsync(() => { - if (isNode) return; - const fixture = initTest(NgModelSelectMultipleWithCustomCompareFnForm); - const comp = fixture.componentInstance; - comp.cities = [{id: 1, name: 'SF'}, {id: 2, name: 'LA'}]; - comp.selectedCities = [comp.cities[0]]; - fixture.detectChanges(); - tick(); - - const select = fixture.debugElement.query(By.css('select')); - const sfOption = fixture.debugElement.query(By.css('option')); - expect(select.nativeElement.value).toEqual('0: Object'); - expect(sfOption.nativeElement.selected).toBe(true); - })); + if (isNode) return; + const fixture = initTest(NgModelSelectMultipleWithCustomCompareFnForm); + const comp = fixture.componentInstance; + comp.cities = [ + {id: 1, name: 'SF'}, + {id: 2, name: 'LA'}, + ]; + comp.selectedCities = [comp.cities[0]]; + fixture.detectChanges(); + tick(); + + const select = fixture.debugElement.query(By.css('select')); + const sfOption = fixture.debugElement.query(By.css('option')); + expect(select.nativeElement.value).toEqual('0: Object'); + expect(sfOption.nativeElement.selected).toBe(true); + })); }); */ @@ -693,8 +744,10 @@ describe('value accessors', () => { describe('in reactive forms', () => { it('should support basic functionality', () => { const fixture = initTest(FormControlRadioButtons); - const form = - new FormGroup({'food': new FormControl('fish'), 'drink': new FormControl('sprite')}); + const form = new FormGroup({ + 'food': new FormControl('fish'), + 'drink': new FormControl('sprite'), + }); fixture.componentInstance.form = form; fixture.detectChanges(); @@ -731,8 +784,10 @@ describe('value accessors', () => { it('should reset properly', () => { const fixture = initTest(FormControlRadioButtons); - const form = - new FormGroup({'food': new FormControl('fish'), 'drink': new FormControl('sprite')}); + const form = new FormGroup({ + 'food': new FormControl('fish'), + 'drink': new FormControl('sprite'), + }); fixture.componentInstance.form = form; fixture.detectChanges(); @@ -746,8 +801,10 @@ describe('value accessors', () => { it('should properly set value to null and undefined', () => { const fixture = initTest(FormControlRadioButtons); - const form: FormGroup = - new FormGroup({'food': new FormControl('chicken'), 'drink': new FormControl('sprite')}); + const form: FormGroup = new FormGroup({ + 'food': new FormControl('chicken'), + 'drink': new FormControl('sprite'), + }); fixture.componentInstance.form = form; fixture.detectChanges(); @@ -800,13 +857,16 @@ describe('value accessors', () => { it('should support removing controls from ', () => { const fixture = initTest(FormControlRadioButtons); const showRadio = new FormControl('yes'); - const form: FormGroup = - new FormGroup({'food': new FormControl('fish'), 'drink': new FormControl('sprite')}); + const form: FormGroup = new FormGroup({ + 'food': new FormControl('fish'), + 'drink': new FormControl('sprite'), + }); fixture.componentInstance.form = form; fixture.componentInstance.showRadio = showRadio; showRadio.valueChanges.subscribe((change) => { - (change === 'yes') ? form.addControl('food', new FormControl('fish')) : - form.removeControl('food'); + change === 'yes' + ? form.addControl('food', new FormControl('fish')) + : form.removeControl('food'); }); fixture.detectChanges(); @@ -829,13 +889,13 @@ describe('value accessors', () => { - ` - } + `, + }, }); const fixture = initTest(FormControlRadioButtons); const form = new FormGroup({ food: new FormControl('fish'), - nested: new FormGroup({food: new FormControl('fish')}) + nested: new FormGroup({food: new FormControl('fish')}), }); fixture.componentInstance.form = form; fixture.detectChanges(); @@ -894,7 +954,7 @@ describe('value accessors', () => { const fixture = initTest(FormControlRadioButtons); const form = new FormGroup({ food: new FormControl({value: 'fish', disabled: true}), - drink: new FormControl('cola') + drink: new FormControl('cola'), }); fixture.componentInstance.form = form; fixture.detectChanges(); @@ -909,8 +969,10 @@ describe('value accessors', () => { it('should work with reusing controls', () => { const fixture = initTest(FormControlRadioButtons); const food = new FormControl('chicken'); - fixture.componentInstance.form = - new FormGroup({'food': food, 'drink': new FormControl('')}); + fixture.componentInstance.form = new FormGroup({ + 'food': food, + 'drink': new FormControl(''), + }); fixture.detectChanges(); const newForm = new FormGroup({'food': food, 'drink': new FormControl('')}); @@ -927,132 +989,132 @@ describe('value accessors', () => { describe('in template-driven forms', () => { it('should support basic functionality', fakeAsync(() => { - const fixture = initTest(NgModelRadioForm); - fixture.componentInstance.food = 'fish'; - fixture.detectChanges(); - tick(); + const fixture = initTest(NgModelRadioForm); + fixture.componentInstance.food = 'fish'; + fixture.detectChanges(); + tick(); - // model -> view - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(true); + // model -> view + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(true); - dispatchEvent(inputs[0].nativeElement, 'change'); - tick(); + dispatchEvent(inputs[0].nativeElement, 'change'); + tick(); - // view -> model - expect(fixture.componentInstance.food).toEqual('chicken'); - expect(inputs[1].nativeElement.checked).toEqual(false); - })); + // view -> model + expect(fixture.componentInstance.food).toEqual('chicken'); + expect(inputs[1].nativeElement.checked).toEqual(false); + })); it('should support multiple named groups', fakeAsync(() => { - const fixture = initTest(NgModelRadioForm); - fixture.componentInstance.food = 'fish'; - fixture.componentInstance.drink = 'sprite'; - fixture.detectChanges(); - tick(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(true); - expect(inputs[2].nativeElement.checked).toEqual(false); - expect(inputs[3].nativeElement.checked).toEqual(true); - - dispatchEvent(inputs[0].nativeElement, 'change'); - tick(); - - expect(fixture.componentInstance.food).toEqual('chicken'); - expect(fixture.componentInstance.drink).toEqual('sprite'); - expect(inputs[1].nativeElement.checked).toEqual(false); - expect(inputs[2].nativeElement.checked).toEqual(false); - expect(inputs[3].nativeElement.checked).toEqual(true); - })); + const fixture = initTest(NgModelRadioForm); + fixture.componentInstance.food = 'fish'; + fixture.componentInstance.drink = 'sprite'; + fixture.detectChanges(); + tick(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(true); + expect(inputs[2].nativeElement.checked).toEqual(false); + expect(inputs[3].nativeElement.checked).toEqual(true); + + dispatchEvent(inputs[0].nativeElement, 'change'); + tick(); + + expect(fixture.componentInstance.food).toEqual('chicken'); + expect(fixture.componentInstance.drink).toEqual('sprite'); + expect(inputs[1].nativeElement.checked).toEqual(false); + expect(inputs[2].nativeElement.checked).toEqual(false); + expect(inputs[3].nativeElement.checked).toEqual(true); + })); it('should support initial undefined value', fakeAsync(() => { - const fixture = initTest(NgModelRadioForm); - fixture.detectChanges(); - tick(); + const fixture = initTest(NgModelRadioForm); + fixture.detectChanges(); + tick(); - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(false); - expect(inputs[2].nativeElement.checked).toEqual(false); - expect(inputs[3].nativeElement.checked).toEqual(false); - })); + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(false); + expect(inputs[2].nativeElement.checked).toEqual(false); + expect(inputs[3].nativeElement.checked).toEqual(false); + })); it('should support resetting properly', fakeAsync(() => { - const fixture = initTest(NgModelRadioForm); - fixture.componentInstance.food = 'chicken'; - fixture.detectChanges(); - tick(); + const fixture = initTest(NgModelRadioForm); + fixture.componentInstance.food = 'chicken'; + fixture.detectChanges(); + tick(); - const form = fixture.debugElement.query(By.css('form')); - dispatchEvent(form.nativeElement, 'reset'); - fixture.detectChanges(); - tick(); + const form = fixture.debugElement.query(By.css('form')); + dispatchEvent(form.nativeElement, 'reset'); + fixture.detectChanges(); + tick(); - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(false); - })); + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(false); + })); it('should support setting value to null and undefined', fakeAsync(() => { - const fixture = initTest(NgModelRadioForm); - fixture.componentInstance.food = 'chicken'; - fixture.detectChanges(); - tick(); - - fixture.componentInstance.food = null!; - fixture.detectChanges(); - tick(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(false); - - fixture.componentInstance.food = 'chicken'; - fixture.detectChanges(); - tick(); - - fixture.componentInstance.food = undefined!; - fixture.detectChanges(); - tick(); - expect(inputs[0].nativeElement.checked).toEqual(false); - expect(inputs[1].nativeElement.checked).toEqual(false); - })); + const fixture = initTest(NgModelRadioForm); + fixture.componentInstance.food = 'chicken'; + fixture.detectChanges(); + tick(); + + fixture.componentInstance.food = null!; + fixture.detectChanges(); + tick(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(false); + + fixture.componentInstance.food = 'chicken'; + fixture.detectChanges(); + tick(); + + fixture.componentInstance.food = undefined!; + fixture.detectChanges(); + tick(); + expect(inputs[0].nativeElement.checked).toEqual(false); + expect(inputs[1].nativeElement.checked).toEqual(false); + })); it('should disable radio controls properly with programmatic call', fakeAsync(() => { - const fixture = initTest(NgModelRadioForm); - fixture.componentInstance.food = 'fish'; - fixture.detectChanges(); - tick(); - - const form = fixture.debugElement.children[0].injector.get(NgForm); - form.control.get('food')!.disable(); - tick(); - - const inputs = fixture.debugElement.queryAll(By.css('input')); - expect(inputs[0].nativeElement.disabled).toBe(true); - expect(inputs[1].nativeElement.disabled).toBe(true); - expect(inputs[2].nativeElement.disabled).toBe(false); - expect(inputs[3].nativeElement.disabled).toBe(false); - - form.control.disable(); - tick(); - - expect(inputs[0].nativeElement.disabled).toBe(true); - expect(inputs[1].nativeElement.disabled).toBe(true); - expect(inputs[2].nativeElement.disabled).toBe(true); - expect(inputs[3].nativeElement.disabled).toBe(true); - - form.control.enable(); - tick(); - - expect(inputs[0].nativeElement.disabled).toBe(false); - expect(inputs[1].nativeElement.disabled).toBe(false); - expect(inputs[2].nativeElement.disabled).toBe(false); - expect(inputs[3].nativeElement.disabled).toBe(false); - })); + const fixture = initTest(NgModelRadioForm); + fixture.componentInstance.food = 'fish'; + fixture.detectChanges(); + tick(); + + const form = fixture.debugElement.children[0].injector.get(NgForm); + form.control.get('food')!.disable(); + tick(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.disabled).toBe(true); + expect(inputs[1].nativeElement.disabled).toBe(true); + expect(inputs[2].nativeElement.disabled).toBe(false); + expect(inputs[3].nativeElement.disabled).toBe(false); + + form.control.disable(); + tick(); + + expect(inputs[0].nativeElement.disabled).toBe(true); + expect(inputs[1].nativeElement.disabled).toBe(true); + expect(inputs[2].nativeElement.disabled).toBe(true); + expect(inputs[3].nativeElement.disabled).toBe(true); + + form.control.enable(); + tick(); + + expect(inputs[0].nativeElement.disabled).toBe(false); + expect(inputs[1].nativeElement.disabled).toBe(false); + expect(inputs[2].nativeElement.disabled).toBe(false); + expect(inputs[3].nativeElement.disabled).toBe(false); + })); }); }); @@ -1111,22 +1173,22 @@ describe('value accessors', () => { describe('in template-driven forms', () => { it('with basic use case', fakeAsync(() => { - const fixture = initTest(NgModelRangeForm); - // model -> view - fixture.componentInstance.val = 4; - fixture.detectChanges(); - tick(); - const input = fixture.debugElement.query(By.css('input')); - expect(input.nativeElement.value).toBe('4'); - fixture.detectChanges(); - tick(); - const newVal = '4'; - input.triggerEventHandler('input', {target: {value: newVal}}); - tick(); - // view -> model - fixture.detectChanges(); - expect(typeof (fixture.componentInstance.val)).toBe('number'); - })); + const fixture = initTest(NgModelRangeForm); + // model -> view + fixture.componentInstance.val = 4; + fixture.detectChanges(); + tick(); + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.value).toBe('4'); + fixture.detectChanges(); + tick(); + const newVal = '4'; + input.triggerEventHandler('input', {target: {value: newVal}}); + tick(); + // view -> model + fixture.detectChanges(); + expect(typeof fixture.componentInstance.val).toBe('number'); + })); }); }); */ @@ -1159,21 +1221,20 @@ describe('value accessors', () => { expect(form.get('login')!.errors).toEqual(null); }); - it('should support non builtin input elements that fire a change event without a \'target\' property', - () => { - const fixture = initTest(MyInputForm, MyInput); - fixture.componentInstance.form = new FormGroup({'login': new FormControl('aa')}); - fixture.detectChanges(); + it("should support non builtin input elements that fire a change event without a 'target' property", () => { + const fixture = initTest(MyInputForm, MyInput); + fixture.componentInstance.form = new FormGroup({'login': new FormControl('aa')}); + fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('my-input')); - expect(input.componentInstance.value).toEqual('!aa!'); + const input = fixture.debugElement.query(By.css('my-input')); + expect(input.componentInstance.value).toEqual('!aa!'); - input.componentInstance.value = '!bb!'; - input.componentInstance.onInput.subscribe((value: any) => { - expect(fixture.componentInstance.form.value).toEqual({'login': 'bb'}); - }); - input.componentInstance.dispatchChangeEvent(); - }); + input.componentInstance.value = '!bb!'; + input.componentInstance.onInput.subscribe((value: any) => { + expect(fixture.componentInstance.form.value).toEqual({'login': 'bb'}); + }); + input.componentInstance.dispatchChangeEvent(); + }); it('should support custom accessors without setDisabledState - formControlName', () => { // [Nimble] Remove TestDefaultValueAccessor because there can only be one custom accessor on each control @@ -1190,9 +1251,9 @@ describe('value accessors', () => { }); it('should support custom accessors without setDisabledState - formControlDirective', () => { - TestBed.overrideComponent( - FormControlComp, - {set: {template: ``}}); + TestBed.overrideComponent(FormControlComp, { + set: {template: ``}, + }); const fixture = initTest(FormControlComp); fixture.componentInstance.control = new FormControl({value: 'aa', disabled: true}); fixture.detectChanges(); @@ -1206,7 +1267,7 @@ describe('value accessors', () => { fixture = initTest(CvaWithDisabledStateForm, CvaWithDisabledState); }); - it('sets the disabled state when the control is initally disabled', () => { + it('sets the disabled state when the control is initially disabled', () => { fixture.componentInstance.form = new FormGroup({ 'login': new FormControl({value: 'aa', disabled: true}), }); @@ -1214,12 +1275,13 @@ describe('value accessors', () => { expect(fixture.componentInstance.form.status).toEqual('DISABLED'); expect(fixture.componentInstance.form.get('login')!.status).toEqual('DISABLED'); - expect(fixture.debugElement.query(By.directive(CvaWithDisabledState)) - .nativeElement.textContent) - .toContain('DISABLED'); + expect( + fixture.debugElement.query(By.directive(CvaWithDisabledState)).nativeElement + .textContent, + ).toContain('DISABLED'); }); - it('sets the enabled state when the control is initally enabled', () => { + it('sets the enabled state when the control is initially enabled', () => { fixture.componentInstance.form = new FormGroup({ 'login': new FormControl({value: 'aa', disabled: false}), }); @@ -1227,9 +1289,10 @@ describe('value accessors', () => { expect(fixture.componentInstance.form.status).toEqual('VALID'); expect(fixture.componentInstance.form.get('login')!.status).toEqual('VALID'); - expect(fixture.debugElement.query(By.directive(CvaWithDisabledState)) - .nativeElement.textContent) - .toContain('ENABLED'); + expect( + fixture.debugElement.query(By.directive(CvaWithDisabledState)).nativeElement + .textContent, + ).toContain('ENABLED'); }); }); @@ -1239,102 +1302,104 @@ describe('value accessors', () => { fixture.detectChanges(); expect(fixture.componentInstance.myInput!.control).toBeDefined(); - expect(fixture.componentInstance.myInput!.control) - .toEqual(fixture.componentInstance.myInput!.controlDir.control); + expect(fixture.componentInstance.myInput!.control).toEqual( + fixture.componentInstance.myInput!.controlDir.control, + ); }); }); describe('in template-driven forms', () => { it('should support standard writing to view and model', waitForAsync(() => { - const fixture = initTest(NgModelCustomWrapper, NgModelCustomComp); - fixture.componentInstance.name = 'Nancy'; - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - fixture.whenStable().then(() => { - // model -> view - const customInput = fixture.debugElement.query(By.css('[name="custom"]')); - expect(customInput.nativeElement.value).toEqual('Nancy'); - - customInput.nativeElement.value = 'Carson'; - dispatchEvent(customInput.nativeElement, 'input'); - fixture.detectChanges(); - - // view -> model - expect(fixture.componentInstance.name).toEqual('Carson'); - }); - }); - })); + const fixture = initTest(NgModelCustomWrapper, NgModelCustomComp); + fixture.componentInstance.name = 'Nancy'; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + fixture.whenStable().then(() => { + // model -> view + const customInput = fixture.debugElement.query(By.css('[name="custom"]')); + expect(customInput.nativeElement.value).toEqual('Nancy'); + + customInput.nativeElement.value = 'Carson'; + dispatchEvent(customInput.nativeElement, 'input'); + fixture.detectChanges(); + + // view -> model + expect(fixture.componentInstance.name).toEqual('Carson'); + }); + }); + })); }); describe('`ngModel` value accessor inside an OnPush component', () => { it('should run change detection and update the value', fakeAsync(async () => { - @Component({ - selector: 'parent', - template: '', - changeDetection: ChangeDetectionStrategy.OnPush, - }) - class Parent { - value!: string; - - constructor(private ref: ChangeDetectorRef) {} - - setTimeoutAndChangeValue(): void { - setTimeout(() => { - this.value = 'Carson'; - this.ref.detectChanges(); - }, 50); - } - } - - @Component({ - selector: 'child', - template: 'Value: {{ value }}', - providers: [{provide: NG_VALUE_ACCESSOR, useExisting: Child, multi: true}] - }) - class Child implements ControlValueAccessor { - value!: string; - - writeValue(value: string): void { - this.value = value; - } - - registerOnChange(): void {} - - registerOnTouched(): void {} - } - - const fixture = initTest(Parent, Child); - fixture.componentInstance.value = 'Nancy'; - fixture.detectChanges(); - - await fixture.whenStable(); - fixture.detectChanges(); - await fixture.whenStable(); - - const child = fixture.debugElement.query(By.css('child')); - // Let's ensure that the initial value has been set, because previously - // it wasn't set inside an `OnPush` component. - expect(child.nativeElement.innerHTML).toEqual('Value: Nancy'); - - fixture.componentInstance.setTimeoutAndChangeValue(); - tick(50); - - fixture.detectChanges(); - await fixture.whenStable(); - - expect(child.nativeElement.innerHTML).toEqual('Value: Carson'); - })); + @Component({ + selector: 'parent', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Parent { + value!: string; + + constructor(private ref: ChangeDetectorRef) {} + + setTimeoutAndChangeValue(): void { + setTimeout(() => { + this.value = 'Carson'; + this.ref.detectChanges(); + }, 50); + } + } + + @Component({ + selector: 'child', + template: 'Value: {{ value }}', + providers: [{provide: NG_VALUE_ACCESSOR, useExisting: Child, multi: true}], + }) + class Child implements ControlValueAccessor { + value!: string; + + writeValue(value: string): void { + this.value = value; + } + + registerOnChange(): void {} + + registerOnTouched(): void {} + } + + const fixture = initTest(Parent, Child); + fixture.componentInstance.value = 'Nancy'; + fixture.detectChanges(); + + await fixture.whenStable(); + fixture.detectChanges(); + await fixture.whenStable(); + + const child = fixture.debugElement.query(By.css('child')); + // Let's ensure that the initial value has been set, because previously + // it wasn't set inside an `OnPush` component. + expect(child.nativeElement.innerHTML).toEqual('Value: Nancy'); + + fixture.componentInstance.setTimeoutAndChangeValue(); + tick(50); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(child.nativeElement.innerHTML).toEqual('Value: Carson'); + })); }); }); }); - describe('value accessors in reactive forms with custom options', () => { function initTest(component: Type, ...directives: Type[]): ComponentFixture { TestBed.configureTestingModule({ declarations: [component, ...directives], - imports: [ReactiveFormsModule.withConfig({callSetDisabledState: 'whenDisabledForLegacyCode'})] + imports: [ + ReactiveFormsModule.withConfig({callSetDisabledState: 'whenDisabledForLegacyCode'}), + ], }); return TestBed.createComponent(component); } @@ -1346,7 +1411,7 @@ describe('value accessors in reactive forms with custom options', () => { fixture = initTest(CvaWithDisabledStateForm, CvaWithDisabledState); }); - it('does not set the enabled state when the control is initally enabled', () => { + it('does not set the enabled state when the control is initially enabled', () => { fixture.componentInstance.form = new FormGroup({ 'login': new FormControl({value: 'aa', disabled: false}), }); @@ -1355,8 +1420,8 @@ describe('value accessors in reactive forms with custom options', () => { expect(fixture.componentInstance.form.status).toEqual('VALID'); expect(fixture.componentInstance.form.get('login')!.status).toEqual('VALID'); expect( - fixture.debugElement.query(By.directive(CvaWithDisabledState)).nativeElement.textContent) - .toContain('UNSET'); + fixture.debugElement.query(By.directive(CvaWithDisabledState)).nativeElement.textContent, + ).toContain('UNSET'); }); }); }); @@ -1371,7 +1436,7 @@ export class FormControlComp { template: `
-
` + `, }) export class FormGroupComp { control!: FormControl; @@ -1382,7 +1447,7 @@ export class FormGroupComp { @Component({ selector: 'form-control-number-input', - template: `` + template: ``, }) class FormControlNumberInput { control!: FormControl; @@ -1395,7 +1460,7 @@ class FormControlNumberInput { - ` + `, }) class FormControlNameSelect { cities = ['SF', 'NY']; @@ -1409,10 +1474,13 @@ class FormControlNameSelect { - ` + `, }) class FormControlSelectNgValue { - cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; + cities = [ + {id: 1, name: 'SF'}, + {id: 2, name: 'NY'}, + ]; form = new FormGroup({city: new FormControl(this.cities[0])}); } @@ -1423,12 +1491,15 @@ class FormControlSelectNgValue { - ` + `, }) class FormControlSelectWithCompareFn { - compareFn: - (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2 ? o1.id === o2.id : o1 === o2 - cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; + compareFn: (o1: any, o2: any) => boolean = (o1: any, o2: any) => + o1 && o2 ? o1.id === o2.id : o1 === o2; + cities = [ + {id: 1, name: 'SF'}, + {id: 2, name: 'NY'}, + ]; form = new FormGroup({city: new FormControl({id: 1, name: 'SF'})}); } @@ -1439,7 +1510,7 @@ class FormControlSelectWithCompareFn { - ` + `, }) class FormControlSelectMultiple { cities = ['SF', 'NY']; @@ -1453,10 +1524,13 @@ class FormControlSelectMultiple { - ` + `, }) class FormControlSelectMultipleNgValue { - cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; + cities = [ + {id: 1, name: 'SF'}, + {id: 2, name: 'NY'}, + ]; form = new FormGroup({city: new FormControl([this.cities[0]])}); } @@ -1467,23 +1541,25 @@ class FormControlSelectMultipleNgValue { - ` + `, }) class FormControlSelectMultipleWithCompareFn { - compareFn: - (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2 ? o1.id === o2.id : o1 === o2 - cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}]; + compareFn: (o1: any, o2: any) => boolean = (o1: any, o2: any) => + o1 && o2 ? o1.id === o2.id : o1 === o2; + cities = [ + {id: 1, name: 'SF'}, + {id: 2, name: 'NY'}, + ]; form = new FormGroup({city: new FormControl([{id: 1, name: 'SF'}])}); } - @Component({ selector: 'ng-model-select-form', template: ` - ` + `, }) class NgModelSelectForm { selectedCity: {[k: string]: string} = {}; @@ -1497,10 +1573,10 @@ class NgModelSelectForm { - ` + `, }) class NgModelSelectWithNullForm { - selectedCity: {[k: string]: string}|null = {}; + selectedCity: {[k: string]: string} | null = {}; cities: any[] = []; } @@ -1510,27 +1586,26 @@ class NgModelSelectWithNullForm { - ` + `, }) class NgModelSelectWithCustomCompareFnForm { - compareFn: - (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2 ? o1.id === o2.id : o1 === o2 + compareFn: (o1: any, o2: any) => boolean = (o1: any, o2: any) => + o1 && o2 ? o1.id === o2.id : o1 === o2; selectedCity: any = {}; cities: any[] = []; } - @Component({ selector: 'ng-model-select-multiple-compare-with', template: ` - ` + `, }) class NgModelSelectMultipleWithCustomCompareFnForm { - compareFn: - (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 && o2 ? o1.id === o2.id : o1 === o2 + compareFn: (o1: any, o2: any) => boolean = (o1: any, o2: any) => + o1 && o2 ? o1.id === o2.id : o1 === o2; selectedCities: any[] = []; cities: any[] = []; } @@ -1541,7 +1616,7 @@ class NgModelSelectMultipleWithCustomCompareFnForm { - ` + `, }) class NgModelSelectMultipleForm { selectedCities!: any[]; @@ -1550,7 +1625,7 @@ class NgModelSelectMultipleForm { @Component({ selector: 'form-control-range-input', - template: `` + template: ``, }) class FormControlRangeInput { control!: FormControl; @@ -1571,7 +1646,7 @@ class NgModelRangeForm { - ` + `, }) export class FormControlRadioButtons { form!: FormGroup; @@ -1588,7 +1663,7 @@ export class FormControlRadioButtons { - ` + `, }) class NgModelRadioForm { food!: string; @@ -1600,8 +1675,8 @@ class NgModelRadioForm { host: {'(input)': 'handleOnInput($event.target.value)', '[value]': 'value'}, providers: [ {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: WrappedValue}, - {provide: NG_VALIDATORS, multi: true, useExisting: WrappedValue} - ] + {provide: NG_VALIDATORS, multi: true, useExisting: WrappedValue}, + ], }) class WrappedValue implements ControlValueAccessor { value: any; @@ -1631,9 +1706,7 @@ class WrappedValue implements ControlValueAccessor {
CALLED WITH {{disabled ? 'DISABLED' : 'ENABLED'}}
UNSET
`, - providers: [ - {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: CvaWithDisabledState}, - ] + providers: [{provide: NG_VALUE_ACCESSOR, multi: true, useExisting: CvaWithDisabledState}], }) class CvaWithDisabledState implements ControlValueAccessor { disabled?: boolean; @@ -1654,7 +1727,7 @@ class CvaWithDisabledState implements ControlValueAccessor { template: `
-
` + `, }) class CvaWithDisabledStateForm { form!: FormGroup; @@ -1665,7 +1738,7 @@ export class MyInput implements ControlValueAccessor { @Output('input') onInput = new EventEmitter(); value!: string; - control: AbstractControl|null = null; + control: AbstractControl | null = null; constructor(public controlDir: NgControl) { controlDir.valueAccessor = this; @@ -1695,11 +1768,11 @@ export class MyInput implements ControlValueAccessor { template: `
-
` + `, }) export class MyInputForm { form!: FormGroup; - @ViewChild(MyInput) myInput: MyInput|null = null; + @ViewChild(MyInput) myInput: MyInput | null = null; } @Component({ @@ -1707,7 +1780,7 @@ export class MyInputForm { template: `
-
` + `, }) class WrappedValueForm { form!: FormGroup; @@ -1718,7 +1791,7 @@ class WrappedValueForm { template: ` `, - providers: [{provide: NG_VALUE_ACCESSOR, multi: true, useExisting: NgModelCustomComp}] + providers: [{provide: NG_VALUE_ACCESSOR, multi: true, useExisting: NgModelCustomComp}], }) export class NgModelCustomComp implements ControlValueAccessor { model!: string; @@ -1746,7 +1819,7 @@ export class NgModelCustomComp implements ControlValueAccessor {
- ` + `, }) export class NgModelCustomWrapper { name!: string; diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/testing/browser_util.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/testing/browser_util.ts index 5dca80472b..cbadcff7ad 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/testing/browser_util.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/testing/browser_util.ts @@ -1,6 +1,6 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/platform-browser/testing/src/browser_util.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/platform-browser/testing/src/browser_util.ts * with the following modifications: * - Comment out everything except childNodesAsList */ @@ -10,7 +10,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ /* [Nimble] Comment out code that is not needed @@ -65,7 +65,12 @@ export function stringifyElement(el: Element): string { } else { // Browsers order style rules differently. Order them alphabetically for consistency. if (lowerCaseKey === 'style') { - attValue = attValue.split(/; ?/).filter(s => !!s).sort().map(s => `${s};`).join(' '); + attValue = attValue + .split(/; ?/) + .filter((s) => !!s) + .sort() + .map((s) => `${s};`) + .join(' '); } result += ` ${lowerCaseKey}="${attValue}"`; @@ -123,7 +128,7 @@ export function setCookie(name: string, value: string) { document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value); } -export function hasStyle(element: any, styleName: string, styleValue?: string|null): boolean { +export function hasStyle(element: any, styleName: string, styleValue?: string | null): boolean { const value = element.style[styleName] || ''; return styleValue ? value == styleValue : value.length > 0; } diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/testing/matchers.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/testing/matchers.ts index e3fa703c64..68e2646fe6 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/testing/matchers.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/testing/matchers.ts @@ -1,6 +1,6 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/platform-browser/testing/src/matchers.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/platform-browser/testing/src/matchers.ts * with the following modifications: * - Update imports * - Comment out everything other than what is needed to use `toHaveText` matcher because `toHaveText` is the only matcher required by the copied @@ -14,9 +14,13 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ +import {ɵgetDOM as getDOM} from '@angular/common'; +import {Type} from '@angular/core'; +import {ComponentFixture} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; // [Nimble] Update imports import {childNodesAsList} from './browser_util'; @@ -55,7 +59,7 @@ export interface NgMatchers extends jasmine.Matchers { // * // * {@example testing/ts/matchers.ts region='toHaveCssStyle'} // */ - // toHaveCssStyle(expected: {[k: string]: string}|string): boolean; + // toHaveCssStyle(expected: {[k: string]: string} | string): boolean; // /** // * Expect a class to implement the interface of the given class. @@ -88,48 +92,49 @@ export interface NgMatchers extends jasmine.Matchers { const _expect: (actual: T) => NgMatchers = expect as any; export {_expect as expect}; -beforeEach(function() { +beforeEach(function () { jasmine.addMatchers({ - toHaveText: function() { + toHaveText: function () { return { - compare: function(actual: any, expectedText: string) { + compare: function (actual: any, expectedText: string) { const actualText = elementText(actual); return { pass: actualText == expectedText, get message() { return 'Expected ' + actualText + ' to be equal to ' + expectedText; - } + }, }; - } + }, }; }, /* [Nimble] Comment out matchers that are not needed - toHaveCssClass: function() { + toHaveCssClass: function () { return {compare: buildError(false), negativeCompare: buildError(true)}; function buildError(isNot: boolean) { - return function(actual: any, className: string) { + return function (actual: any, className: string) { return { pass: hasClass(actual, className) == !isNot, get message() { return `Expected ${actual.outerHTML} ${ - isNot ? 'not ' : ''}to contain the CSS class "${className}"`; - } + isNot ? 'not ' : '' + }to contain the CSS class "${className}"`; + }, }; }; } }, - toHaveCssStyle: function() { + toHaveCssStyle: function () { return { - compare: function(actual: any, styles: {[k: string]: string}|string) { + compare: function (actual: any, styles: {[k: string]: string} | string) { let allPassed: boolean; if (typeof styles === 'string') { allPassed = hasStyle(actual, styles); } else { allPassed = Object.keys(styles).length !== 0; - Object.keys(styles).forEach(prop => { + Object.keys(styles).forEach((prop) => { allPassed = allPassed && hasStyle(actual, prop, styles[prop]); }); } @@ -139,17 +144,16 @@ beforeEach(function() { get message() { const expectedValueStr = typeof styles === 'string' ? styles : JSON.stringify(styles); return `Expected ${actual.outerHTML} ${!allPassed ? ' ' : 'not '}to contain the - CSS ${typeof styles === 'string' ? 'property' : 'styles'} "${ - expectedValueStr}"`; - } + CSS ${typeof styles === 'string' ? 'property' : 'styles'} "${expectedValueStr}"`; + }, }; - } + }, }; }, - toImplement: function() { + toImplement: function () { return { - compare: function(actualObject: any, expectedInterface: any) { + compare: function (actualObject: any, expectedInterface: any) { const intProps = Object.keys(expectedInterface.prototype); const missedMethods: any[] = []; @@ -160,17 +164,21 @@ beforeEach(function() { return { pass: missedMethods.length == 0, get message() { - return 'Expected ' + actualObject + - ' to have the following methods: ' + missedMethods.join(', '); - } + return ( + 'Expected ' + + actualObject + + ' to have the following methods: ' + + missedMethods.join(', ') + ); + }, }; - } + }, }; }, - toContainComponent: function() { + toContainComponent: function () { return { - compare: function(actualFixture: any, expectedComponentType: Type) { + compare: function (actualFixture: any, expectedComponentType: Type) { const failOutput = arguments[2]; const msgFn = (msg: string): string => [msg, failOutput].filter(Boolean).join(', '); @@ -178,18 +186,19 @@ beforeEach(function() { if (!(actualFixture instanceof ComponentFixture)) { return { pass: false, - message: msgFn(`Expected actual to be of type \'ComponentFixture\' [actual=${ - actualFixture.constructor.name}]`) + message: msgFn( + `Expected actual to be of type \'ComponentFixture\' [actual=${actualFixture.constructor.name}]`, + ), }; } const found = !!actualFixture.debugElement.query(By.directive(expectedComponentType)); - return found ? - {pass: true} : - {pass: false, message: msgFn(`Expected ${expectedComponentType.name} to show`)}; - } + return found + ? {pass: true} + : {pass: false, message: msgFn(`Expected ${expectedComponentType.name} to show`)}; + }, }; - } + }, */ }); }); diff --git a/packages/angular-workspace/nimble-angular/src/thirdparty/utils/coercion.ts b/packages/angular-workspace/nimble-angular/src/thirdparty/utils/coercion.ts index aee81ace51..8b7c9b71e9 100644 --- a/packages/angular-workspace/nimble-angular/src/thirdparty/utils/coercion.ts +++ b/packages/angular-workspace/nimble-angular/src/thirdparty/utils/coercion.ts @@ -1,6 +1,6 @@ /** * [Nimble] - * Copied from https://github.com/angular/angular/blob/17.3.11/packages/core/src/util/coercion.ts + * Copied from https://github.com/angular/angular/blob/18.2.13/packages/core/src/util/coercion.ts * with no modifications so that the `booleanAttribute` function can be used by the forked directive * in `router_link.ts` without depending on private Angular APIs. */ @@ -10,7 +10,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ /** @@ -26,7 +26,7 @@ * @publicApi */ export function booleanAttribute(value: unknown): boolean { - return typeof value === 'boolean' ? value : (value != null && value !== 'false'); + return typeof value === 'boolean' ? value : value != null && value !== 'false'; } /**