fix(core): properly move embedded views of dynamic component's projectable nodes (#37167)
This commit fixes the issue of the ASSERTION ERROR issue when a projected node(RNode) inside an array is checked against the types of TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer, TNodeType.IcuContainer, TNodeType.Projection. As it's inside an array, it doesn't fall into any of those types, as a result, it throws the ASSERTION ERROR. PR Close #37120 PR Close #37167
This commit is contained in:
parent
378da71f27
commit
0654c05c41
@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {assertDefined, assertEqual, assertNumber, throwError} from '../util/assert';
|
import {assertDefined, assertEqual, assertNumber, throwError} from '../util/assert';
|
||||||
|
|
||||||
import {getComponentDef, getNgModuleDef} from './definition';
|
import {getComponentDef, getNgModuleDef} from './definition';
|
||||||
import {LContainer} from './interfaces/container';
|
import {LContainer} from './interfaces/container';
|
||||||
import {DirectiveDef} from './interfaces/definition';
|
import {DirectiveDef} from './interfaces/definition';
|
||||||
@ -14,8 +15,7 @@ import {TIcu} from './interfaces/i18n';
|
|||||||
import {NodeInjectorOffset} from './interfaces/injector';
|
import {NodeInjectorOffset} from './interfaces/injector';
|
||||||
import {TNode} from './interfaces/node';
|
import {TNode} from './interfaces/node';
|
||||||
import {isLContainer, isLView} from './interfaces/type_checks';
|
import {isLContainer, isLView} from './interfaces/type_checks';
|
||||||
import {HEADER_OFFSET, LView, TVIEW, TView} from './interfaces/view';
|
import {DECLARATION_COMPONENT_VIEW, HEADER_OFFSET, LView, T_HOST, TVIEW, TView} from './interfaces/view';
|
||||||
|
|
||||||
|
|
||||||
// [Assert functions do not constraint type when they are guarded by a truthy
|
// [Assert functions do not constraint type when they are guarded by a truthy
|
||||||
// expression.](https://github.com/microsoft/TypeScript/issues/37295)
|
// expression.](https://github.com/microsoft/TypeScript/issues/37295)
|
||||||
@ -135,6 +135,20 @@ export function assertBetween(lower: number, upper: number, index: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assertProjectionSlots(lView: LView, errMessage?: string) {
|
||||||
|
assertDefined(lView[DECLARATION_COMPONENT_VIEW], 'Component views should exist.');
|
||||||
|
assertDefined(
|
||||||
|
lView[DECLARATION_COMPONENT_VIEW][T_HOST]!.projection,
|
||||||
|
errMessage ||
|
||||||
|
'Components with projection nodes (<ng-content>) must have projection slots defined.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertParentView(lView: LView|null, errMessage?: string) {
|
||||||
|
assertDefined(
|
||||||
|
lView,
|
||||||
|
errMessage || 'Component views should always have a parent view (component\'s host view)');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a basic sanity check that the `injectorIndex` seems to point to what looks like a
|
* This is a basic sanity check that the `injectorIndex` seems to point to what looks like a
|
||||||
|
@ -6,15 +6,15 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {assertDefined} from '../util/assert';
|
import {assertParentView} from './assert';
|
||||||
|
|
||||||
import {icuContainerIterate} from './i18n/i18n_tree_shaking';
|
import {icuContainerIterate} from './i18n/i18n_tree_shaking';
|
||||||
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
|
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
|
||||||
import {TElementNode, TIcuContainerNode, TNode, TNodeType} from './interfaces/node';
|
import {TIcuContainerNode, TNode, TNodeType} from './interfaces/node';
|
||||||
import {RNode} from './interfaces/renderer_dom';
|
import {RNode} from './interfaces/renderer_dom';
|
||||||
import {isLContainer} from './interfaces/type_checks';
|
import {isLContainer} from './interfaces/type_checks';
|
||||||
import {DECLARATION_COMPONENT_VIEW, LView, T_HOST, TVIEW, TView} from './interfaces/view';
|
import {DECLARATION_COMPONENT_VIEW, LView, T_HOST, TVIEW, TView} from './interfaces/view';
|
||||||
import {assertTNodeType} from './node_assert';
|
import {assertTNodeType} from './node_assert';
|
||||||
|
import {getProjectionNodes} from './node_manipulation';
|
||||||
import {getLViewParent} from './util/view_traversal_utils';
|
import {getLViewParent} from './util/view_traversal_utils';
|
||||||
import {unwrapRNode} from './util/view_utils';
|
import {unwrapRNode} from './util/view_utils';
|
||||||
|
|
||||||
@ -58,23 +58,12 @@ export function collectNativeNodes(
|
|||||||
result.push(rNode);
|
result.push(rNode);
|
||||||
}
|
}
|
||||||
} else if (tNodeType & TNodeType.Projection) {
|
} else if (tNodeType & TNodeType.Projection) {
|
||||||
const componentView = lView[DECLARATION_COMPONENT_VIEW];
|
const nodesInSlot = getProjectionNodes(lView, tNode);
|
||||||
const componentHost = componentView[T_HOST] as TElementNode;
|
|
||||||
const slotIdx = tNode.projection as number;
|
|
||||||
ngDevMode &&
|
|
||||||
assertDefined(
|
|
||||||
componentHost.projection,
|
|
||||||
'Components with projection nodes (<ng-content>) must have projection slots defined.');
|
|
||||||
|
|
||||||
const nodesInSlot = componentHost.projection![slotIdx];
|
|
||||||
if (Array.isArray(nodesInSlot)) {
|
if (Array.isArray(nodesInSlot)) {
|
||||||
result.push(...nodesInSlot);
|
result.push(...nodesInSlot);
|
||||||
} else {
|
} else {
|
||||||
const parentView = getLViewParent(componentView)!;
|
const parentView = getLViewParent(lView[DECLARATION_COMPONENT_VIEW])!;
|
||||||
ngDevMode &&
|
ngDevMode && assertParentView(parentView);
|
||||||
assertDefined(
|
|
||||||
parentView,
|
|
||||||
'Component views should always have a parent view (component\'s host view)');
|
|
||||||
collectNativeNodes(parentView[TVIEW], parentView, nodesInSlot, result, true);
|
collectNativeNodes(parentView[TVIEW], parentView, nodesInSlot, result, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,8 @@ import {RendererStyleFlags2} from '../render/api_flags';
|
|||||||
import {addToArray, removeFromArray} from '../util/array_utils';
|
import {addToArray, removeFromArray} from '../util/array_utils';
|
||||||
import {assertDefined, assertDomNode, assertEqual, assertFunction, assertString} from '../util/assert';
|
import {assertDefined, assertDomNode, assertEqual, assertFunction, assertString} from '../util/assert';
|
||||||
import {escapeCommentText} from '../util/dom';
|
import {escapeCommentText} from '../util/dom';
|
||||||
import {assertLContainer, assertLView, assertTNodeForLView} from './assert';
|
|
||||||
|
import {assertLContainer, assertLView, assertParentView, assertProjectionSlots, assertTNodeForLView} from './assert';
|
||||||
import {attachPatchData} from './context_discovery';
|
import {attachPatchData} from './context_discovery';
|
||||||
import {icuContainerIterate} from './i18n/i18n_tree_shaking';
|
import {icuContainerIterate} from './i18n/i18n_tree_shaking';
|
||||||
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS, NATIVE, unusedValueExportToPlacateAjd as unused1} from './interfaces/container';
|
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS, NATIVE, unusedValueExportToPlacateAjd as unused1} from './interfaces/container';
|
||||||
@ -772,14 +773,14 @@ function getFirstNativeNode(lView: LView, tNode: TNode|null): RNode|null {
|
|||||||
// If the ICU container has no nodes, than we use the ICU anchor as the node.
|
// If the ICU container has no nodes, than we use the ICU anchor as the node.
|
||||||
return rNode || unwrapRNode(lView[tNode.index]);
|
return rNode || unwrapRNode(lView[tNode.index]);
|
||||||
} else {
|
} else {
|
||||||
const componentView = lView[DECLARATION_COMPONENT_VIEW];
|
const projectionNodes = getProjectionNodes(lView, tNode);
|
||||||
const componentHost = componentView[T_HOST] as TElementNode;
|
if (projectionNodes !== null) {
|
||||||
const parentView = getLViewParent(componentView);
|
if (Array.isArray(projectionNodes)) {
|
||||||
const firstProjectedTNode: TNode|null =
|
return projectionNodes[0];
|
||||||
(componentHost.projection as (TNode | null)[])[tNode.projection as number];
|
}
|
||||||
|
const parentView = getLViewParent(lView[DECLARATION_COMPONENT_VIEW]);
|
||||||
if (firstProjectedTNode != null) {
|
ngDevMode && assertParentView(parentView);
|
||||||
return getFirstNativeNode(parentView!, firstProjectedTNode);
|
return getFirstNativeNode(parentView!, projectionNodes);
|
||||||
} else {
|
} else {
|
||||||
return getFirstNativeNode(lView, tNode.next);
|
return getFirstNativeNode(lView, tNode.next);
|
||||||
}
|
}
|
||||||
@ -789,6 +790,17 @@ function getFirstNativeNode(lView: LView, tNode: TNode|null): RNode|null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getProjectionNodes(lView: LView, tNode: TNode|null): TNode|RNode[]|null {
|
||||||
|
if (tNode !== null) {
|
||||||
|
const componentView = lView[DECLARATION_COMPONENT_VIEW];
|
||||||
|
const componentHost = componentView[T_HOST] as TElementNode;
|
||||||
|
const slotIdx = tNode.projection as number;
|
||||||
|
ngDevMode && assertProjectionSlots(lView);
|
||||||
|
return componentHost.projection![slotIdx];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function getBeforeNodeForView(viewIndexInContainer: number, lContainer: LContainer): RNode|
|
export function getBeforeNodeForView(viewIndexInContainer: number, lContainer: LContainer): RNode|
|
||||||
null {
|
null {
|
||||||
const nextViewIndex = CONTAINER_HEADER_OFFSET + viewIndexInContainer + 1;
|
const nextViewIndex = CONTAINER_HEADER_OFFSET + viewIndexInContainer + 1;
|
||||||
|
@ -1088,6 +1088,9 @@
|
|||||||
{
|
{
|
||||||
"name": "getPreviousIndex"
|
"name": "getPreviousIndex"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getProjectionNodes"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getPromiseCtor"
|
"name": "getPromiseCtor"
|
||||||
},
|
},
|
||||||
|
@ -1403,6 +1403,9 @@
|
|||||||
{
|
{
|
||||||
"name": "getPreviousIndex"
|
"name": "getPreviousIndex"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getProjectionNodes"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getPromiseCtor"
|
"name": "getPromiseCtor"
|
||||||
},
|
},
|
||||||
|
@ -437,6 +437,9 @@
|
|||||||
{
|
{
|
||||||
"name": "getPreviousIndex"
|
"name": "getPreviousIndex"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getProjectionNodes"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getSelectedIndex"
|
"name": "getSelectedIndex"
|
||||||
},
|
},
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {CommonModule, DOCUMENT, ɵgetDOM as getDOM} from '@angular/common';
|
import {CommonModule, DOCUMENT, ɵgetDOM as getDOM} from '@angular/common';
|
||||||
import {Compiler, ComponentFactory, ComponentRef, ErrorHandler, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, OnDestroy, SkipSelf, ViewRef, ɵivyEnabled as ivyEnabled} from '@angular/core';
|
import {Compiler, ComponentFactory, ComponentRef, ErrorHandler, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, OnDestroy, SkipSelf, ViewChild, ViewRef, ɵivyEnabled as ivyEnabled} from '@angular/core';
|
||||||
import {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection';
|
import {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection';
|
||||||
import {getDebugContext} from '@angular/core/src/errors';
|
import {getDebugContext} from '@angular/core/src/errors';
|
||||||
import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver';
|
import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver';
|
||||||
@ -1641,6 +1641,159 @@ function declareTests(config?: {useJit: boolean}) {
|
|||||||
expect(fixture.nativeElement).toHaveText('');
|
expect(fixture.nativeElement).toHaveText('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('moving embedded views of projectable nodes in a dynamic component', () => {
|
||||||
|
@Component({selector: 'menu-item', template: ''})
|
||||||
|
class DynamicMenuItem {
|
||||||
|
@ViewChild('templateRef', {static: true}) templateRef!: TemplateRef<any>;
|
||||||
|
itemContent!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [DynamicMenuItem],
|
||||||
|
entryComponents: [DynamicMenuItem],
|
||||||
|
})
|
||||||
|
class DynamicMenuItemModule {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'test', template: `<ng-container #menuItemsContainer></ng-container>`})
|
||||||
|
class TestCmp {
|
||||||
|
constructor(public cfr: ComponentFactoryResolver) {}
|
||||||
|
@ViewChild('menuItemsContainer', {static: true, read: ViewContainerRef})
|
||||||
|
menuItemsContainer!: ViewContainerRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [TestCmp],
|
||||||
|
imports: [DynamicMenuItemModule],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const createElWithContent = (content: string, tagName = 'span') => {
|
||||||
|
const element = document.createElement(tagName);
|
||||||
|
element.textContent = content;
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should support moving embedded views of projectable nodes', () => {
|
||||||
|
TestBed.overrideTemplate(
|
||||||
|
DynamicMenuItem, `<ng-template #templateRef><ng-content></ng-content></ng-template>`);
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(TestCmp);
|
||||||
|
const menuItemsContainer = fixture.componentInstance.menuItemsContainer;
|
||||||
|
const dynamicCmptFactory =
|
||||||
|
fixture.componentInstance.cfr.resolveComponentFactory(DynamicMenuItem);
|
||||||
|
|
||||||
|
const cmptRefWithAa =
|
||||||
|
dynamicCmptFactory.create(Injector.NULL, [[createElWithContent('Aa')]]);
|
||||||
|
const cmptRefWithBb =
|
||||||
|
dynamicCmptFactory.create(Injector.NULL, [[createElWithContent('Bb')]]);
|
||||||
|
const cmptRefWithCc =
|
||||||
|
dynamicCmptFactory.create(Injector.NULL, [[createElWithContent('Cc')]]);
|
||||||
|
|
||||||
|
menuItemsContainer.insert(cmptRefWithAa.instance.templateRef.createEmbeddedView({}));
|
||||||
|
menuItemsContainer.insert(cmptRefWithBb.instance.templateRef.createEmbeddedView({}));
|
||||||
|
menuItemsContainer.insert(cmptRefWithCc.instance.templateRef.createEmbeddedView({}));
|
||||||
|
|
||||||
|
menuItemsContainer.move(menuItemsContainer.get(0)!, 1);
|
||||||
|
expect(fixture.nativeElement.textContent).toBe('BbAaCc');
|
||||||
|
menuItemsContainer.move(menuItemsContainer.get(2)!, 1);
|
||||||
|
expect(fixture.nativeElement.textContent).toBe('BbCcAa');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support moving embedded views of projectable nodes in multiple slots', () => {
|
||||||
|
TestBed.overrideTemplate(
|
||||||
|
DynamicMenuItem,
|
||||||
|
`<ng-template #templateRef><ng-content select="span"></ng-content><ng-content select="button"></ng-content></ng-template>`);
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(TestCmp);
|
||||||
|
const menuItemsContainer = fixture.componentInstance.menuItemsContainer;
|
||||||
|
const dynamicCmptFactory =
|
||||||
|
fixture.componentInstance.cfr.resolveComponentFactory(DynamicMenuItem);
|
||||||
|
|
||||||
|
const cmptRefWithAa = dynamicCmptFactory.create(
|
||||||
|
Injector.NULL, [[createElWithContent('A')], [createElWithContent('a', 'button')]]);
|
||||||
|
const cmptRefWithBb = dynamicCmptFactory.create(
|
||||||
|
Injector.NULL, [[createElWithContent('B')], [createElWithContent('b', 'button')]]);
|
||||||
|
const cmptRefWithCc = dynamicCmptFactory.create(
|
||||||
|
Injector.NULL, [[createElWithContent('C')], [createElWithContent('c', 'button')]]);
|
||||||
|
|
||||||
|
menuItemsContainer.insert(cmptRefWithAa.instance.templateRef.createEmbeddedView({}));
|
||||||
|
menuItemsContainer.insert(cmptRefWithBb.instance.templateRef.createEmbeddedView({}));
|
||||||
|
menuItemsContainer.insert(cmptRefWithCc.instance.templateRef.createEmbeddedView({}));
|
||||||
|
|
||||||
|
menuItemsContainer.move(menuItemsContainer.get(0)!, 1);
|
||||||
|
expect(fixture.nativeElement.textContent).toBe('BbAaCc');
|
||||||
|
menuItemsContainer.move(menuItemsContainer.get(2)!, 1);
|
||||||
|
expect(fixture.nativeElement.textContent).toBe('BbCcAa');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support moving embedded views of projectable nodes in multiple slots and interpolations',
|
||||||
|
() => {
|
||||||
|
TestBed.overrideTemplate(
|
||||||
|
DynamicMenuItem,
|
||||||
|
`<ng-template #templateRef><ng-content select="span"></ng-content>{{itemContent}}<ng-content select="button"></ng-content></ng-template>`);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule(
|
||||||
|
{declarations: [TestCmp], imports: [DynamicMenuItemModule]});
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(TestCmp);
|
||||||
|
const menuItemsContainer = fixture.componentInstance.menuItemsContainer;
|
||||||
|
const dynamicCmptFactory =
|
||||||
|
fixture.componentInstance.cfr.resolveComponentFactory(DynamicMenuItem);
|
||||||
|
|
||||||
|
const cmptRefWithAa = dynamicCmptFactory.create(
|
||||||
|
Injector.NULL, [[createElWithContent('A')], [createElWithContent('a', 'button')]]);
|
||||||
|
const cmptRefWithBb = dynamicCmptFactory.create(
|
||||||
|
Injector.NULL, [[createElWithContent('B')], [createElWithContent('b', 'button')]]);
|
||||||
|
const cmptRefWithCc = dynamicCmptFactory.create(
|
||||||
|
Injector.NULL, [[createElWithContent('C')], [createElWithContent('c', 'button')]]);
|
||||||
|
|
||||||
|
menuItemsContainer.insert(cmptRefWithAa.instance.templateRef.createEmbeddedView({}));
|
||||||
|
menuItemsContainer.insert(cmptRefWithBb.instance.templateRef.createEmbeddedView({}));
|
||||||
|
menuItemsContainer.insert(cmptRefWithCc.instance.templateRef.createEmbeddedView({}));
|
||||||
|
|
||||||
|
cmptRefWithAa.instance.itemContent = '0';
|
||||||
|
cmptRefWithBb.instance.itemContent = '1';
|
||||||
|
cmptRefWithCc.instance.itemContent = '2';
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
menuItemsContainer.move(menuItemsContainer.get(0)!, 1);
|
||||||
|
expect(fixture.nativeElement.textContent).toBe('B1bA0aC2c');
|
||||||
|
menuItemsContainer.move(menuItemsContainer.get(2)!, 1);
|
||||||
|
expect(fixture.nativeElement.textContent).toBe('B1bC2cA0a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support moving embedded views with empty projectable slots', () => {
|
||||||
|
TestBed.overrideTemplate(
|
||||||
|
DynamicMenuItem, `<ng-template #templateRef><ng-content></ng-content></ng-template>`);
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(TestCmp);
|
||||||
|
const menuItemsContainer = fixture.componentInstance.menuItemsContainer;
|
||||||
|
const dynamicCmptFactory =
|
||||||
|
fixture.componentInstance.cfr.resolveComponentFactory(DynamicMenuItem);
|
||||||
|
|
||||||
|
const cmptRefWithAa = dynamicCmptFactory.create(Injector.NULL, [[]]);
|
||||||
|
const cmptRefWithBb =
|
||||||
|
dynamicCmptFactory.create(Injector.NULL, [[createElWithContent('Bb')]]);
|
||||||
|
const cmptRefWithCc =
|
||||||
|
dynamicCmptFactory.create(Injector.NULL, [[createElWithContent('Cc')]]);
|
||||||
|
|
||||||
|
menuItemsContainer.insert(cmptRefWithAa.instance.templateRef.createEmbeddedView({}));
|
||||||
|
menuItemsContainer.insert(cmptRefWithBb.instance.templateRef.createEmbeddedView({}));
|
||||||
|
menuItemsContainer.insert(cmptRefWithCc.instance.templateRef.createEmbeddedView({}));
|
||||||
|
|
||||||
|
menuItemsContainer.move(menuItemsContainer.get(0)!, 1); // [ Bb, NULL, Cc]
|
||||||
|
expect(fixture.nativeElement.textContent).toBe('BbCc');
|
||||||
|
menuItemsContainer.move(menuItemsContainer.get(2)!, 1); // [ Bb, Cc, NULL]
|
||||||
|
expect(fixture.nativeElement.textContent).toBe('BbCc');
|
||||||
|
menuItemsContainer.move(menuItemsContainer.get(0)!, 1); // [ Cc, Bb, NULL]
|
||||||
|
expect(fixture.nativeElement.textContent).toBe('CcBb');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Property bindings', () => {
|
describe('Property bindings', () => {
|
||||||
modifiedInIvy('Unknown property error throws an error instead of logging it')
|
modifiedInIvy('Unknown property error throws an error instead of logging it')
|
||||||
.it('should throw on bindings to unknown properties', () => {
|
.it('should throw on bindings to unknown properties', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user