Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -381,38 +381,28 @@ export declare class StandaloneComponent {
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
export function FancyButton() { }
// @angular/core does not expose the `ForeignComponent` type this should return.
// @angular/core does not expose the `ForeignComponent` type this should return.
function frameworkImport(component) {
return () => { };
}
export class TestCmp {
title = 'Submit';
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestCmp, isStandalone: true, selector: "main", ngImport: i0, template: `
<FancyButton
class="btn-cls"
unsafe-attr="value"
[label]="title"
[unsafe-input]="title"
/>
<FancyButton class="btn-cls" unsafe-attr="value" [label]="title" [unsafe-input]="title" />
`, isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, decorators: [{
type: Component,
args: [{
selector: 'main',
template: `
<FancyButton
class="btn-cls"
unsafe-attr="value"
[label]="title"
[unsafe-input]="title"
/>
<FancyButton class="btn-cls" unsafe-attr="value" [label]="title" [unsafe-input]="title" />
`,
// @ts-ignore: @angular/core does not expose the `foreignImports` property.
foreignImports: [
// @ts-ignore: @angular/core does not expose the `ForeignComponent` type this expects.
frameworkImport(FancyButton)
frameworkImport(FancyButton),
],
}]
}] });
Expand Down Expand Up @@ -449,7 +439,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
// @ts-ignore: @angular/core does not expose the `foreignImports` property.
foreignImports: [
// @ts-ignore: @angular/core does not expose the `ForeignComponent` type this expects.
frameworkImport(FancyButton)
frameworkImport(FancyButton),
],
}]
}] });
Expand Down Expand Up @@ -478,7 +468,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
// @ts-ignore: @angular/core does not expose the `foreignImports` property.
foreignImports: [
// @ts-ignore: @angular/core does not expose the `ForeignComponent` type this expects.
frameworkImport(FancyButton)
frameworkImport(FancyButton),
],
}]
}] });
// Nest @if to demonstrate that multiple `nextContext()` calls are correctly merged into one.
export class TestCmpConditional {
title = 'Submit';
innerCondition = true;
outerCondition = true;
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmpConditional, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: TestCmpConditional, isStandalone: true, selector: "main-conditional", ngImport: i0, template: `
@if (outerCondition) {
@if (innerCondition) {
<FancyButton [label]="title" />
}
}
`, isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmpConditional, decorators: [{
type: Component,
args: [{
selector: 'main-conditional',
template: `
@if (outerCondition) {
@if (innerCondition) {
<FancyButton [label]="title" />
}
}
`,
// @ts-ignore: @angular/core does not expose the `foreignImports` property.
foreignImports: [
// @ts-ignore: @angular/core does not expose the `ForeignComponent` type this expects.
frameworkImport(FancyButton),
],
}]
}] });
Expand All @@ -503,4 +525,11 @@ export declare class TestCmpRenderProps {
static ɵfac: i0.ɵɵFactoryDeclaration<TestCmpRenderProps, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestCmpRenderProps, "main-render-props", never, {}, {}, never, never, true, never>;
}
export declare class TestCmpConditional {
title: string;
innerCondition: boolean;
outerCondition: boolean;
static ɵfac: i0.ɵɵFactoryDeclaration<TestCmpConditional, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestCmpConditional, "main-conditional", never, {}, {}, never, never, true, never>;
}

Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ function TestCmpRenderProps_Items_0_Template(rf, ctx) {
}
}

function TestCmpConditional_Conditional_0_Conditional_0_Template(rf, ctx) {
if (rf & 1) {
const ctx_r0 = i0.ɵɵnextContext(2);
i0.ɵɵforeignComponent(0, 0, { label: ctx_r0.title });
}
}

function TestCmpConditional_Conditional_0_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵconditionalCreate(0, TestCmpConditional_Conditional_0_Conditional_0_Template, 1, 0);
}
if (rf & 2) {
const ctx_r0 = i0.ɵɵnextContext();
i0.ɵɵconditional(ctx_r0.innerCondition ? 0 : -1);
}
}


export class TestCmp {
Expand Down Expand Up @@ -95,3 +112,26 @@ export class TestCmpRenderProps {
});
}


export class TestCmpConditional {
// ...
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({
type: TestCmpConditional,
selectors: [["main-conditional"]],
decls: 1,
vars: 1,
consts: [frameworkImport(FancyButton)],
template: function TestCmpConditional_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵconditionalCreate(0, TestCmpConditional_Conditional_0_Template, 1, 1);
}
if (rf & 2) {
i0.ɵɵconditional(ctx.outerCondition ? 0 : -1);
}
},
encapsulation: 2
});
}


Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,20 @@ import {Component} from '@angular/core';

export function FancyButton() {}

// @angular/core does not expose the `ForeignComponent` type this should return.
// @angular/core does not expose the `ForeignComponent` type this should return.
function frameworkImport(component: {}): Function {
return () => {};
}

@Component({
selector: 'main',
template: `
<FancyButton
class="btn-cls"
unsafe-attr="value"
[label]="title"
[unsafe-input]="title"
/>
<FancyButton class="btn-cls" unsafe-attr="value" [label]="title" [unsafe-input]="title" />
`,
// @ts-ignore: @angular/core does not expose the `foreignImports` property.
foreignImports: [
// @ts-ignore: @angular/core does not expose the `ForeignComponent` type this expects.
frameworkImport(FancyButton)
frameworkImport(FancyButton),
],
})
export class TestCmp {
Expand All @@ -43,7 +38,7 @@ export class TestCmp {
// @ts-ignore: @angular/core does not expose the `foreignImports` property.
foreignImports: [
// @ts-ignore: @angular/core does not expose the `ForeignComponent` type this expects.
frameworkImport(FancyButton)
frameworkImport(FancyButton),
],
})
export class TestCmpChildren {
Expand All @@ -62,10 +57,31 @@ export class TestCmpChildren {
// @ts-ignore: @angular/core does not expose the `foreignImports` property.
foreignImports: [
// @ts-ignore: @angular/core does not expose the `ForeignComponent` type this expects.
frameworkImport(FancyButton)
frameworkImport(FancyButton),
],
})
export class TestCmpRenderProps {
title = 'Submit';
}

// Nest @if to demonstrate that multiple `nextContext()` calls are correctly merged into one.
@Component({
selector: 'main-conditional',
template: `
@if (outerCondition) {
@if (innerCondition) {
<FancyButton [label]="title" />
}
}
`,
// @ts-ignore: @angular/core does not expose the `foreignImports` property.
foreignImports: [
// @ts-ignore: @angular/core does not expose the `ForeignComponent` type this expects.
frameworkImport(FancyButton),
],
})
export class TestCmpConditional {
title = 'Submit';
innerCondition = true;
outerCondition = true;
}
5 changes: 4 additions & 1 deletion packages/compiler/src/template/pipeline/src/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1961,7 +1961,10 @@ function ingestControlFlowInsertionPoint(
}

// Root nodes can only elements or templates with a tag name (e.g. `<div *foo></div>`).
if (child instanceof t.Element || (child instanceof t.Template && child.tagName !== null)) {
if (
(child instanceof t.Element && unit.job.getForeignComponent(child) === null) ||
(child instanceof t.Template && child.tagName !== null)
) {
root = child;
} else {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ function recursivelyProcessView(view: ViewCompilationUnit, parentScope: Scope |
}
}

view.create.prepend(generateVariablesInScopeForView(view, scope, false));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is potentially problematic for variables which aren't available in the creation pass, e.g. @if (c) { <input #i /> <FancyButton [x]="i" /> }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great catch, and one that isn't trivial to solve. The issue is that these references truly aren't available until the update phase, so we can't simply expose them to the create phase like we did for the component context.

As a workaround, you could use viewQuery() signal to pass reactive references, although this similarly breaks if you use viewQuery.required().

We could implement diagnostics to prohibit passing references in this manner. Alternatively, we could reevaluate the timing of foreign component creation and move it to the update phase. That would be a pretty significant refactor though, so are you okay if we submit this as-is for now and address that in a follow up PR?

view.update.prepend(generateVariablesInScopeForView(view, scope, false));

for (const expr of view.functions) {
Expand Down Expand Up @@ -235,12 +236,12 @@ function getScopeForView(view: ViewCompilationUnit, parent: Scope | null): Scope
* This is a recursive process, as views inherit variables available from their parent view, which
* itself may have inherited variables, etc.
*/
function generateVariablesInScopeForView(
function generateVariablesInScopeForView<OpT extends ir.Op<OpT>>(
Comment thread
leonsenft marked this conversation as resolved.
view: ViewCompilationUnit,
scope: Scope,
isCallback: boolean,
): ir.VariableOp<ir.UpdateOp>[] {
const newOps: ir.VariableOp<ir.UpdateOp>[] = [];
): ir.VariableOp<OpT>[] {
const newOps: ir.VariableOp<OpT>[] = [];

if (scope.view !== view.xref) {
// Before generating variables for a parent view, we need to switch to the context of the parent
Expand Down Expand Up @@ -306,7 +307,7 @@ function generateVariablesInScopeForView(
if (scope.view !== view.xref || isCallback) {
for (const decl of scope.letDeclarations) {
newOps.push(
ir.createVariableOp<ir.UpdateOp>(
ir.createVariableOp<OpT>(
view.job.allocateXrefId(),
decl.variable,
new ir.ContextLetReferenceExpr(decl.targetId, decl.targetSlot),
Expand All @@ -318,7 +319,7 @@ function generateVariablesInScopeForView(

if (scope.parent !== null) {
// Recursively add variables from the parent scope.
newOps.push(...generateVariablesInScopeForView(view, scope.parent, false));
newOps.push(...generateVariablesInScopeForView<OpT>(view, scope.parent, false));
}
return newOps;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ export function mergeNextContextExpressions(job: CompilationJob): void {
mergeNextContextsInOps(op.handlerOps);
}
}
mergeNextContextsInOps(unit.create);
mergeNextContextsInOps(unit.update);
}
}

function mergeNextContextsInOps(ops: ir.OpList<ir.UpdateOp>): void {
function mergeNextContextsInOps(ops: ir.OpList<ir.CreateOp | ir.UpdateOp>): void {
for (const op of ops) {
// Look for a candidate operation to maybe merge.
if (
Expand Down Expand Up @@ -82,7 +83,7 @@ function mergeNextContextsInOps(ops: ir.OpList<ir.UpdateOp>): void {
case ir.ExpressionKind.NextContext:
// Merge the previous `ir.NextContextExpr` into this one.
expr.steps += mergeSteps;
ir.OpList.remove(op as ir.UpdateOp);
ir.OpList.remove(op as ir.CreateOp | ir.UpdateOp);
tryToMerge = false;
break;
case ir.ExpressionKind.GetCurrentView:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function optimizeVariables(job: CompilationJob): void {
}
}

// Note that we skip over arrow function operations, because they are considered
// Note that we skip over nested scopes, because they are considered
// separate boundaries that should not influence the surrounding create/update
// operations. This is a side-effect of not being able to control which nested
// ops `visitExpressionsInOp` will visit. Without this logic, variable references
Expand Down Expand Up @@ -383,7 +383,7 @@ function collectOpInfo(

if (expr.kind === ir.ExpressionKind.ReadVariable) {
variablesUsed.add(expr.xref);
} else {
} else if (!(flags & ir.VisitorContextFlag.InChildOperation)) {
fences |= fencesForIrExpression(expr);
}
});
Expand Down
Loading
Loading