mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
feat(core): conditional built-in control flow (#51346)
Initial PoC of the built-in control flow support. The goal is to unblock compiler work. PR Close #51346
This commit is contained in:
parent
6a60a29ff2
commit
93675dc797
5 changed files with 289 additions and 11 deletions
|
|
@ -90,6 +90,7 @@ export {
|
|||
ɵɵclassMapInterpolateV,
|
||||
ɵɵclassProp,
|
||||
ɵɵComponentDeclaration,
|
||||
ɵɵconditional,
|
||||
ɵɵcontentQuery,
|
||||
ɵɵCopyDefinitionFeature,
|
||||
ɵɵdefineComponent,
|
||||
|
|
|
|||
|
|
@ -118,6 +118,8 @@ export {
|
|||
|
||||
ɵɵtemplate,
|
||||
|
||||
ɵɵconditional,
|
||||
|
||||
ɵɵdefer,
|
||||
ɵɵdeferWhen,
|
||||
ɵɵdeferOnIdle,
|
||||
|
|
|
|||
|
|
@ -25,30 +25,31 @@
|
|||
*
|
||||
* Jira Issue = FW-1184
|
||||
*/
|
||||
export * from './advance';
|
||||
export * from './attribute';
|
||||
export * from './attribute_interpolation';
|
||||
export * from './change_detection';
|
||||
export * from './template';
|
||||
export * from './storage';
|
||||
export * from './class_map_interpolation';
|
||||
export * from './control_flow';
|
||||
export * from './defer';
|
||||
export * from './di';
|
||||
export * from './di_attr';
|
||||
export * from './element';
|
||||
export * from './element_container';
|
||||
export {ɵgetUnknownElementStrictMode, ɵgetUnknownPropertyStrictMode, ɵsetUnknownElementStrictMode, ɵsetUnknownPropertyStrictMode} from './element_validation';
|
||||
export * from './get_current_view';
|
||||
export * from './host_property';
|
||||
export * from './i18n';
|
||||
export * from './listener';
|
||||
export * from './namespace';
|
||||
export * from './next_context';
|
||||
export * from './projection';
|
||||
export * from './property';
|
||||
export * from './property_interpolation';
|
||||
export * from './advance';
|
||||
export * from './styling';
|
||||
export * from './text';
|
||||
export * from './text_interpolation';
|
||||
export * from './class_map_interpolation';
|
||||
export * from './storage';
|
||||
export * from './style_map_interpolation';
|
||||
export * from './style_prop_interpolation';
|
||||
export * from './host_property';
|
||||
export * from './i18n';
|
||||
export * from './defer';
|
||||
export {ɵgetUnknownElementStrictMode, ɵsetUnknownElementStrictMode, ɵgetUnknownPropertyStrictMode, ɵsetUnknownPropertyStrictMode} from './element_validation';
|
||||
export * from './styling';
|
||||
export * from './template';
|
||||
export * from './text';
|
||||
export * from './text_interpolation';
|
||||
|
|
|
|||
70
packages/core/src/render3/instructions/control_flow.ts
Normal file
70
packages/core/src/render3/instructions/control_flow.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* @license
|
||||
* 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
|
||||
*/
|
||||
|
||||
import {assertLContainer, assertTNode} from '../assert';
|
||||
import {bindingUpdated} from '../bindings';
|
||||
import {LContainer} from '../interfaces/container';
|
||||
import {TNode} from '../interfaces/node';
|
||||
import {CONTEXT, HEADER_OFFSET, LView, TVIEW, TView} from '../interfaces/view';
|
||||
import {getLView, nextBindingIndex} from '../state';
|
||||
import {getTNode} from '../util/view_utils';
|
||||
import {addLViewToLContainer, createAndRenderEmbeddedLView, getLViewFromLContainer, removeLViewFromLContainer} from '../view_manipulation';
|
||||
|
||||
/**
|
||||
* The conditional instruction represents the basic building block on the runtime side to support
|
||||
* built-in "if" and "switch". On the high level this instruction is responsible for adding and
|
||||
* removing views selected by a conditional expression.
|
||||
*
|
||||
* @param containerIndex index of a container in a host view (indexed from HEADER_OFFSET) where
|
||||
* conditional views should be inserted.
|
||||
* @param matchingTemplateIndex index of a template TNode representing a conditional view to be
|
||||
* inserted; -1 represents a special case when there is no view to insert.
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵconditional<T>(containerIndex: number, matchingTemplateIndex: number, value?: T) {
|
||||
const hostLView = getLView();
|
||||
const bindingIndex = nextBindingIndex();
|
||||
const lContainer = getLContainer(hostLView, HEADER_OFFSET + containerIndex);
|
||||
const viewInContainerIdx = 0;
|
||||
|
||||
if (bindingUpdated(hostLView, bindingIndex, matchingTemplateIndex)) {
|
||||
// The index of the view to show changed - remove the previously displayed one
|
||||
// (it is a noop if there are no active views in a container).
|
||||
removeLViewFromLContainer(lContainer, viewInContainerIdx);
|
||||
|
||||
// Index -1 is a special case where none of the conditions evaluates to
|
||||
// a truthy value and as the consequence we've got no view to show.
|
||||
if (matchingTemplateIndex !== -1) {
|
||||
const templateTNode = getExistingTNode(hostLView[TVIEW], matchingTemplateIndex);
|
||||
const embeddedLView = createAndRenderEmbeddedLView(hostLView, templateTNode, value);
|
||||
|
||||
addLViewToLContainer(lContainer, embeddedLView, viewInContainerIdx);
|
||||
}
|
||||
} else {
|
||||
// We might keep displaying the same template but the actual value of the expression could have
|
||||
// changed - re-bind in context.
|
||||
const lView = getLViewFromLContainer<T|undefined>(lContainer, viewInContainerIdx);
|
||||
if (lView !== undefined) {
|
||||
lView[CONTEXT] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLContainer(lView: LView, index: number): LContainer {
|
||||
const lContainer = lView[index];
|
||||
ngDevMode && assertLContainer(lContainer);
|
||||
|
||||
return lContainer;
|
||||
}
|
||||
|
||||
function getExistingTNode(tView: TView, index: number): TNode {
|
||||
const tNode = getTNode(tView, index + HEADER_OFFSET);
|
||||
ngDevMode && assertTNode(tNode);
|
||||
|
||||
return tNode;
|
||||
}
|
||||
204
packages/core/test/acceptance/control_flow_exploration_spec.ts
Normal file
204
packages/core/test/acceptance/control_flow_exploration_spec.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
/**
|
||||
* @license
|
||||
* 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
|
||||
*/
|
||||
|
||||
|
||||
import {ɵɵconditional, ɵɵdefineComponent, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
describe('control flow', () => {
|
||||
describe('if', () => {
|
||||
function App_ng_template_0_Template(rf: number, ctx: unknown) {
|
||||
if (rf & 1) {
|
||||
ɵɵtext(0, 'Something');
|
||||
}
|
||||
}
|
||||
function App_ng_template_1_Template(rf: number, ctx: unknown) {
|
||||
if (rf & 1) {
|
||||
ɵɵtext(0, 'Nothing');
|
||||
}
|
||||
}
|
||||
|
||||
it('should add and remove views based on conditions change', () => {
|
||||
class TestComponent {
|
||||
show = true;
|
||||
static ɵcmp = ɵɵdefineComponent({
|
||||
type: TestComponent,
|
||||
selectors: [['some-cmp']],
|
||||
decls: 2,
|
||||
vars: 1,
|
||||
template:
|
||||
function TestComponent_Template(rf: number, ctx: any) {
|
||||
if (rf & 1) {
|
||||
ɵɵtemplate(0, App_ng_template_0_Template, 1, 0);
|
||||
ɵɵtemplate(1, App_ng_template_1_Template, 1, 0);
|
||||
}
|
||||
if (rf & 2) {
|
||||
ɵɵconditional(0, ctx.show ? 0 : 1);
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
});
|
||||
static ɵfac = function TestComponent_Factory(t: any) {
|
||||
return new (t || TestComponent)();
|
||||
};
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('Something');
|
||||
|
||||
fixture.componentInstance.show = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('Nothing');
|
||||
});
|
||||
|
||||
it('should expose expression value in context', () => {
|
||||
function App_ng_template_0_Template(rf: number, ctx: any) {
|
||||
if (rf & 1) {
|
||||
ɵɵtext(0, 'Something');
|
||||
}
|
||||
if (rf & 2) {
|
||||
ɵɵtextInterpolate(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
class TestComponent {
|
||||
show: any = true;
|
||||
static ɵcmp = ɵɵdefineComponent({
|
||||
type: TestComponent,
|
||||
selectors: [['some-cmp']],
|
||||
decls: 2,
|
||||
vars: 1,
|
||||
template:
|
||||
function TestComponent_Template(rf: number, ctx: any) {
|
||||
if (rf & 1) {
|
||||
ɵɵtemplate(0, App_ng_template_0_Template, 1, 1);
|
||||
}
|
||||
if (rf & 2) {
|
||||
let temp: any;
|
||||
ɵɵconditional(0, (temp = ctx.show) ? 0 : -1, temp);
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
});
|
||||
static ɵfac = function TestComponent_Factory(t: any) {
|
||||
return new (t || TestComponent)();
|
||||
};
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('true');
|
||||
|
||||
fixture.componentInstance.show = 1;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('1');
|
||||
});
|
||||
|
||||
it('should destroy all views if there is nothing to display', () => {
|
||||
class TestComponent {
|
||||
show = true;
|
||||
static ɵcmp = ɵɵdefineComponent({
|
||||
type: TestComponent,
|
||||
selectors: [['some-cmp']],
|
||||
decls: 1,
|
||||
vars: 1,
|
||||
template:
|
||||
function TestComponent_Template(rf: number, ctx: any) {
|
||||
if (rf & 1) {
|
||||
// QUESTION: fundamental mismatch between the "template" and "container" concepts
|
||||
// those 2 calls to the ɵɵtemplate instruction will generate comment nodes and
|
||||
// LContainer
|
||||
ɵɵtemplate(0, App_ng_template_0_Template, 1, 0);
|
||||
}
|
||||
if (rf & 2) {
|
||||
ɵɵconditional(0, ctx.show ? 0 : -1);
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
});
|
||||
static ɵfac = function TestComponent_Factory(t: any) {
|
||||
return new (t || TestComponent)();
|
||||
};
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('Something');
|
||||
|
||||
fixture.componentInstance.show = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('switch', () => {
|
||||
function App_ng_template_0_Template(rf: number, ctx: unknown) {
|
||||
if (rf & 1) {
|
||||
ɵɵtext(0, 'case 0');
|
||||
}
|
||||
}
|
||||
|
||||
function App_ng_template_1_Template(rf: number, ctx: unknown) {
|
||||
if (rf & 1) {
|
||||
ɵɵtext(0, 'case 1');
|
||||
}
|
||||
}
|
||||
|
||||
function App_ng_template_default_Template(rf: number, ctx: unknown) {
|
||||
if (rf & 1) {
|
||||
ɵɵtext(0, 'default');
|
||||
}
|
||||
}
|
||||
|
||||
it('should show a template based on a matching case', () => {
|
||||
class TestComponent {
|
||||
case = 0;
|
||||
static ɵcmp = ɵɵdefineComponent({
|
||||
type: TestComponent,
|
||||
selectors: [['some-cmp']],
|
||||
decls: 3,
|
||||
vars: 1,
|
||||
template:
|
||||
function TestComponent_Template(rf: number, ctx: any) {
|
||||
if (rf & 1) {
|
||||
ɵɵtemplate(0, App_ng_template_0_Template, 1, 0);
|
||||
ɵɵtemplate(1, App_ng_template_1_Template, 1, 0);
|
||||
ɵɵtemplate(2, App_ng_template_default_Template, 1, 0);
|
||||
}
|
||||
if (rf & 2) {
|
||||
const expValue = ctx.case;
|
||||
// Open question: == vs. === for comparison
|
||||
// == is the current Angular implementation
|
||||
// === is used by JavaScript semantics
|
||||
ɵɵconditional(0, expValue === 0 ? 0 : expValue === 1 ? 1 : 2);
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
});
|
||||
static ɵfac = function TestComponent_Factory(t: any) {
|
||||
return new (t || TestComponent)();
|
||||
};
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('case 0');
|
||||
|
||||
fixture.componentInstance.case = 1;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('case 1');
|
||||
|
||||
fixture.componentInstance.case = 5;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('default');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue