Skip to content
Closed
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 @@ -143,7 +143,9 @@ class TemplateVisitor<Code extends ErrorCode> extends RecursiveAstVisitor implem
this.visitAllNodes(template.references);
this.visitAllNodes(template.children);
}
visitContent(content: TmplAstContent): void {}
visitContent(content: TmplAstContent): void {
this.visitAllNodes(content.children);
}
visitVariable(variable: TmplAstVariable): void {}
visitReference(reference: TmplAstReference): void {}
visitTextAttribute(attribute: TmplAstTextAttribute): void {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {AST, BindingPipe, BindingType, BoundTarget, Call, createCssSelectorFromNode, CssSelector, DYNAMIC_TYPE, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, R3Identifiers, SafeCall, SafePropertyRead, SchemaMetadata, SelectorMatcher, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstElement, TmplAstForLoopBlock, TmplAstForLoopBlockEmpty, TmplAstHoverDeferredTrigger, TmplAstIcu, TmplAstIfBlock, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstText, TmplAstTextAttribute, TmplAstVariable, TmplAstViewportDeferredTrigger, TransplantedType} from '@angular/compiler';
import {AST, BindingPipe, BindingType, BoundTarget, Call, createCssSelectorFromNode, CssSelector, DYNAMIC_TYPE, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, R3Identifiers, SafeCall, SafePropertyRead, SchemaMetadata, SelectorMatcher, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstContent, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstElement, TmplAstForLoopBlock, TmplAstForLoopBlockEmpty, TmplAstHoverDeferredTrigger, TmplAstIcu, TmplAstIfBlock, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstText, TmplAstTextAttribute, TmplAstVariable, TmplAstViewportDeferredTrigger, TransplantedType} from '@angular/compiler';
import ts from 'typescript';

import {Reference} from '../../imports';
Expand Down Expand Up @@ -2034,6 +2034,8 @@ class Scope {
this.opQueue.push(new TcbExpressionOp(this.tcb, this, node.value));
} else if (node instanceof TmplAstIcu) {
this.appendIcuExpressions(node);
} else if (node instanceof TmplAstContent) {
this.appendChildren(node);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
****************************************************************************************************/
export {};

/****************************************************************************************************
* PARTIAL FILE: ng_content_fallback.js
****************************************************************************************************/
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComponent {
constructor() {
this.type = 'complex';
this.hasFooter = false;
this.hasStructural = false;
}
}
TestComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
TestComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: TestComponent, isStandalone: true, selector: "test", ngImport: i0, template: `
<ng-content select="basic">Basic fallback</ng-content>

<div>
<ng-content>
<h1>This is {{type}} <strong>content</strong>!</h1>
</ng-content>
</div>

@if (hasFooter) {
<ng-content select="footer">
Inside control flow
</ng-content>
}

<ng-content select="structural" *ngIf="hasStructural">
<h2>With a structural directive</h2>
</ng-content>
`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, decorators: [{
type: Component,
args: [{
selector: 'test',
standalone: true,
template: `
<ng-content select="basic">Basic fallback</ng-content>

<div>
<ng-content>
<h1>This is {{type}} <strong>content</strong>!</h1>
</ng-content>
</div>

@if (hasFooter) {
<ng-content select="footer">
Inside control flow
</ng-content>
}

<ng-content select="structural" *ngIf="hasStructural">
<h2>With a structural directive</h2>
</ng-content>
`
}]
}] });

/****************************************************************************************************
* PARTIAL FILE: ng_content_fallback.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestComponent {
type: string;
hasFooter: boolean;
hasStructural: boolean;
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "test", never, {}, {}, never, ["basic", "*", "footer", "structural"], true, never>;
}

Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,20 @@
]
}
]
},
{
"description": "should support fallback content in ng-content",
"inputFiles": [
"ng_content_fallback.ts"
],
"expectations": [
{
"failureMessage": "Incorrect projection",
"files": [
"ng_content_fallback.js"
]
}
]
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const $_c0$ = [[["basic"]], "*", [["footer"]], [["structural"]]];
const $_c1$ = ["basic", "*", "footer", "structural"];

function TestComponent_ProjectionFallback_0_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, "Basic fallback");
}
}

function TestComponent_ProjectionFallback_3_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "h1");
$r3$.ɵɵtext(1);
$r3$.ɵɵelementStart(2, "strong");
$r3$.ɵɵtext(3, "content");
$r3$.ɵɵelementEnd();
$r3$.ɵɵtext(4, "!");
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
const $ctx_r0$ = $r3$.ɵɵnextContext();
$r3$.ɵɵadvance();
$r3$.ɵɵtextInterpolate1("This is ", $ctx_r0$.type, " ");
}
}

function TestComponent_Conditional_5_ProjectionFallback_0_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Inside control flow ");
}
}

function TestComponent_Conditional_5_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵprojection(0, 2, null, TestComponent_Conditional_5_ProjectionFallback_0_Template, 1, 0);
}
}

function TestComponent_ng_content_6_ProjectionFallback_0_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "h2");
$r3$.ɵɵtext(1, "With a structural directive");
$r3$.ɵɵelementEnd();
}
}

function TestComponent_ng_content_6_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵprojection(0, 3, ["*ngIf", "hasStructural"], TestComponent_ng_content_6_ProjectionFallback_0_Template, 2, 0);
}
}

…

$r3$.ɵɵdefineComponent({
…
ngContentSelectors: $_c1$,
decls: 7,
vars: 2,
consts: [[4, "ngIf"]],
template: function TestComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵprojectionDef($_c0$);
$r3$.ɵɵprojection(0, 0, null, TestComponent_ProjectionFallback_0_Template, 1, 0);
$r3$.ɵɵelementStart(2, "div");
$r3$.ɵɵprojection(3, 1, null, TestComponent_ProjectionFallback_3_Template, 5, 1);
$r3$.ɵɵelementEnd();
$r3$.ɵɵtemplate(5, TestComponent_Conditional_5_Template, 2, 0)(6, TestComponent_ng_content_6_Template, 2, 0, "ng-content", 0);
}
if (rf & 2) {
$r3$.ɵɵadvance(5);
$r3$.ɵɵconditional(5, ctx.hasFooter ? 5 : -1);
$r3$.ɵɵadvance();
$r3$.ɵɵproperty("ngIf", ctx.hasStructural);
}
},
…
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {Component} from '@angular/core';

@Component({
selector: 'test',
standalone: true,
template: `
<ng-content select="basic">Basic fallback</ng-content>

<div>
<ng-content>
<h1>This is {{type}} <strong>content</strong>!</h1>
</ng-content>
</div>

@if (hasFooter) {
<ng-content select="footer">
Inside control flow
</ng-content>
}

<ng-content select="structural" *ngIf="hasStructural">
<h2>With a structural directive</h2>
</ng-content>
`
})
export class TestComponent {
type = 'complex';
hasFooter = false;
hasStructural = false;
}
68 changes: 57 additions & 11 deletions packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,19 @@ runInEachFileSystem(() => {

it('should check a simple component', () => {
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
import {Component, NgModule} from '@angular/core';

@Component({
selector: 'test',
template: 'I am a simple template with no type info',
})
class TestCmp {}
@Component({
selector: 'test',
template: 'I am a simple template with no type info',
})
class TestCmp {}

@NgModule({
declarations: [TestCmp],
})
class Module {}
`);
@NgModule({
declarations: [TestCmp],
})
class Module {}
`);

env.driveMain();
});
Expand Down Expand Up @@ -537,6 +537,52 @@ runInEachFileSystem(() => {
}));
});

it('should check the fallback content of ng-content', () => {
env.write('test.ts', `
import {Component} from '@angular/core';

@Component({
standalone: true,
template: \`
<ng-content>
<button (click)="acceptsNumber('hello')"></button>
</ng-content>
\`,
})
class TestCmp {
acceptsNumber(value: number) {}
}
`);

const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toContain(`Argument of type 'string' is not assignable to parameter of type 'number'.`);
});

it('should not allow references to the default content of ng-content', () => {
env.write('test.ts', `
import {Component} from '@angular/core';

@Component({
standalone: true,
template: \`
<ng-content>
<input #input/>
</ng-content>

{{input.value}}
\`,
})
class TestCmp {
}
`);

const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain(`Property 'input' does not exist on type 'TestCmp'.`);
});

describe('strictInputTypes', () => {
beforeEach(() => {
env.write('test.ts', `
Expand Down
6 changes: 4 additions & 2 deletions packages/compiler/src/render3/r3_ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ export class Content implements Node {
readonly name = 'ng-content';

constructor(
public selector: string, public attributes: TextAttribute[],
public selector: string, public attributes: TextAttribute[], public children: Node[],
public sourceSpan: ParseSourceSpan, public i18n?: I18nMeta) {}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitContent(this);
Expand Down Expand Up @@ -505,7 +505,9 @@ export class RecursiveVisitor implements Visitor<void> {
block.expressionAlias && blockItems.push(block.expressionAlias);
visitAll(this, blockItems);
}
visitContent(content: Content): void {}
visitContent(content: Content): void {
visitAll(this, content.children);
}
visitVariable(variable: Variable): void {}
visitReference(reference: Reference): void {}
visitTextAttribute(attribute: TextAttribute): void {}
Expand Down
17 changes: 1 addition & 16 deletions packages/compiler/src/render3/r3_template_transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,16 +206,9 @@ class HtmlAstToIvyAst implements html.Visitor {

let parsedElement: t.Content|t.Template|t.Element|undefined;
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
// `<ng-content>`
if (element.children &&
!element.children.every(
(node: html.Node) => isEmptyTextNode(node) || isCommentNode(node))) {
this.reportError(`<ng-content> element cannot have content.`, element.sourceSpan);
}
const selector = preparsedElement.selectAttr;
const attrs: t.TextAttribute[] = element.attrs.map(attr => this.visitAttribute(attr));
parsedElement = new t.Content(selector, attrs, element.sourceSpan, element.i18n);

parsedElement = new t.Content(selector, attrs, children, element.sourceSpan, element.i18n);
this.ngContentSelectors.push(selector);
} else if (isTemplateElement) {
// `<ng-template>`
Expand Down Expand Up @@ -689,14 +682,6 @@ function addEvents(events: ParsedEvent[], boundEvents: t.BoundEvent[]) {
boundEvents.push(...events.map(e => t.BoundEvent.fromParsedEvent(e)));
}

function isEmptyTextNode(node: html.Node): boolean {
return node instanceof html.Text && node.value.trim().length == 0;
}

function isCommentNode(node: html.Node): boolean {
return node instanceof html.Comment;
}

function textContents(node: html.Element): string|null {
if (node.children.length !== 1 || !(node.children[0] instanceof html.Text)) {
return null;
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler/src/render3/view/t2_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
*/

import {AST} from '../../expression_parser/ast';
import {BoundAttribute, BoundEvent, DeferredBlock, DeferredBlockError, DeferredBlockLoading, DeferredBlockPlaceholder, DeferredTrigger, Element, ForLoopBlock, ForLoopBlockEmpty, IfBlockBranch, Node, Reference, SwitchBlockCase, Template, TextAttribute, Variable} from '../r3_ast';
import {BoundAttribute, BoundEvent, Content, DeferredBlock, DeferredBlockError, DeferredBlockLoading, DeferredBlockPlaceholder, DeferredTrigger, Element, ForLoopBlock, ForLoopBlockEmpty, IfBlockBranch, Node, Reference, SwitchBlockCase, Template, TextAttribute, Variable} from '../r3_ast';

/** Node that has a `Scope` associated with it. */
export type ScopedNode = Template|SwitchBlockCase|IfBlockBranch|ForLoopBlock|ForLoopBlockEmpty|
DeferredBlock|DeferredBlockError|DeferredBlockLoading|DeferredBlockPlaceholder;
DeferredBlock|DeferredBlockError|DeferredBlockLoading|DeferredBlockPlaceholder|Content;

/** Possible values that a reference can be resolved to. */
export type ReferenceTarget<DirectiveT> = {
Expand Down
Loading