mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c4b6d474f8 | |||
| 78efc406ba | |||
| c277e88b22 | |||
| 574b1c276d | |||
| 40eb5e3207 | |||
| 8ef145fa1b | |||
| 94f2daaa1c | |||
| ffc61749ff | |||
| f70d80c8c7 | |||
| a4cb2e671c | |||
| dc7882cf97 | |||
| 5f9bd59b14 | |||
| cfd466dfbb | |||
| 61ce057a09 | |||
| 8dc6c9fe22 | |||
| e71e900708 | |||
| 1168fd585f | |||
| 18ea007390 | |||
| bd23af0639 | |||
| 6b0e1c7703 | |||
| 6b89ce9e27 | |||
| 0bf724f4d4 | |||
| 7acf163f51 | |||
| 6808375e50 | |||
| 97d6781be7 | |||
| f12e8e3ea2 | |||
| f5638c25e6 | |||
| d82eb27f69 | |||
| 3b5218b768 | |||
| 5fa6ce3e61 | |||
| d88ac6db12 | |||
| 46efe02e48 | |||
| cd76360ea8 | |||
| 09ad1ef5a2 | |||
| 48f4f86835 | |||
| 183c92baed | |||
| e03ccb60ed | |||
| c402bb81f5 | |||
| 0a7a8e496e | |||
| 779e1d872f | |||
| 193a32e5c1 | |||
| 41f8584aad | |||
| 4b8e6b36ab | |||
| 4c58b2f8cf | |||
| 0e96528a70 | |||
| 1d1338fced | |||
| 1344da62ad | |||
| 0c821b28f3 | |||
| ebef1a592a | |||
| 4c97f56223 | |||
| 71e8bd8791 | |||
| fcf9edbc78 | |||
| 626d3ab586 | |||
| 3a607f51d6 | |||
| ccf0dc1884 | |||
| ebf646a9c9 | |||
| 732e45a730 | |||
| 6697d6ba2f | |||
| 399e173d8c | |||
| 05ecc619fc | |||
| 52f508ab4d | |||
| 2a61fea64b | |||
| 10c1ae5b6e | |||
| 9208e64e74 | |||
| 65d45e4919 | |||
| 080bdfa77f | |||
| 977bd97717 | |||
| a90dc4310c | |||
| 2426b2e1f1 | |||
| 912c7252f8 | |||
| 8e64d9e1d8 | |||
| 4d7b23e99e | |||
| 59d278c07c | |||
| 7e1de8cb6a | |||
| f6bda0eb4d | |||
| c7c4b9e390 | |||
| a77924ff38 | |||
| d04408d67d | |||
| f556b29d05 | |||
| a9be2b3628 | |||
| a34a479ee5 | |||
| f0c9eeceba | |||
| e737d45ad0 | |||
| c7bede629b | |||
| 992f51bc6d | |||
| f2658e34a2 | |||
| 89813aea70 | |||
| e6e5021eb9 |
@@ -284,6 +284,8 @@ jobs:
|
||||
- store_artifacts:
|
||||
path: dist/bin/packages/core/test/bundling/todo/bundle.min.js.br
|
||||
destination: core/todo/bundle.br
|
||||
- notify_webhook_on_fail:
|
||||
webhook_url_env_var: SLACK_REACTIVITY_CHANNEL_WEBHOOK_URL
|
||||
|
||||
# NOTE: This is currently limited to main builds only. See the `monitoring` configuration.
|
||||
saucelabs:
|
||||
|
||||
@@ -72,6 +72,8 @@ export enum ErrorCode {
|
||||
PARAM_MISSING_TOKEN = 2003,
|
||||
// (undocumented)
|
||||
PIPE_MISSING_NAME = 2002,
|
||||
QUERY_DEFINITION_ARG_NOT_LITERAL = 9003,
|
||||
QUERY_DEFINITION_ARITY_WRONG = 9002,
|
||||
SCHEMA_INVALID_ATTRIBUTE = 8002,
|
||||
SCHEMA_INVALID_ELEMENT = 8001,
|
||||
SKIP_HYDRATION_NOT_STATIC = 8108,
|
||||
|
||||
@@ -201,6 +201,7 @@ export interface Component extends Directive {
|
||||
moduleId?: string;
|
||||
preserveWhitespaces?: boolean;
|
||||
schemas?: SchemaMetadata[];
|
||||
signals?: boolean;
|
||||
standalone?: boolean;
|
||||
styles?: string[];
|
||||
styleUrls?: string[];
|
||||
@@ -292,6 +293,13 @@ export type ContentChild = Query;
|
||||
// @public
|
||||
export const ContentChild: ContentChildDecorator;
|
||||
|
||||
// @public (undocumented)
|
||||
export function contentChild<T>(selector: ProviderToken<T> | string, opts?: {
|
||||
descendants?: boolean;
|
||||
read?: any;
|
||||
static?: boolean;
|
||||
}): Signal<T | undefined>;
|
||||
|
||||
// @public
|
||||
export interface ContentChildDecorator {
|
||||
(selector: ProviderToken<unknown> | Function | string, opts?: {
|
||||
@@ -313,6 +321,13 @@ export type ContentChildren = Query;
|
||||
// @public
|
||||
export const ContentChildren: ContentChildrenDecorator;
|
||||
|
||||
// @public (undocumented)
|
||||
export function contentChildren<T>(selector: ProviderToken<T> | string, opts?: {
|
||||
descendants?: boolean;
|
||||
read?: any;
|
||||
emitDistinctChangesOnly?: boolean;
|
||||
}): Signal<T[]>;
|
||||
|
||||
// @public
|
||||
export interface ContentChildrenDecorator {
|
||||
(selector: ProviderToken<unknown> | Function | string, opts?: {
|
||||
@@ -495,6 +510,7 @@ export interface Directive {
|
||||
[key: string]: any;
|
||||
};
|
||||
selector?: string;
|
||||
signals?: boolean;
|
||||
standalone?: boolean;
|
||||
}
|
||||
|
||||
@@ -834,6 +850,45 @@ export interface Input {
|
||||
// @public (undocumented)
|
||||
export const Input: InputDecorator;
|
||||
|
||||
// @public (undocumented)
|
||||
export function input(): InputSignal<undefined, undefined>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function input<T>(): InputSignal<T | undefined, T>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function input<T>(initialValue: T & (string | number | boolean), opts?: PrimaryInputOptions<T, T> & {
|
||||
transform?: undefined;
|
||||
}): InputSignal<T, T>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function input<ReadT, WriteT = ReadT>(initialValue: WriteT & (string | number | boolean), opts: PrimaryInputOptions<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function input<T>(opts: InputOptions<T, T> & {
|
||||
required: true;
|
||||
transform?: undefined;
|
||||
}): InputSignal<T, T>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function input<ReadT, WriteT = ReadT>(opts: InputOptions<ReadT, WriteT> & {
|
||||
required: true;
|
||||
}): InputSignal<ReadT, WriteT>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function input<T>(opts: InputOptions<T, T> & {
|
||||
initialValue: T;
|
||||
transform?: undefined;
|
||||
}): InputSignal<T, T>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function input<ReadT, WriteT = ReadT>(opts: InputOptions<ReadT, WriteT> & {
|
||||
initialValue: ReadT;
|
||||
}): InputSignal<ReadT, WriteT>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function input<ReadT, WriteT = ReadT>(opts: InputOptions<ReadT, WriteT>): InputSignal<ReadT | undefined, WriteT>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface InputDecorator {
|
||||
(arg?: string | Input): any;
|
||||
@@ -841,6 +896,11 @@ export interface InputDecorator {
|
||||
new (arg?: string | Input): any;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type InputSignal<ReadT, WriteT> = Signal<ReadT> & {
|
||||
[BRAND_WRITE_TYPE]: WriteT;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function isDevMode(): boolean;
|
||||
|
||||
@@ -960,6 +1020,9 @@ export enum MissingTranslationStrategy {
|
||||
Warning = 1
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ModelSignal<ReadT, WriteT> = InputSignal<ReadT, WriteT> & Pick<WritableSignal<WriteT>, 'set' | 'update' | 'mutate'>;
|
||||
|
||||
// @public @deprecated
|
||||
export class ModuleWithComponentFactories<T> {
|
||||
constructor(ngModuleFactory: NgModuleFactory<T>, componentFactories: ComponentFactory<any>[]);
|
||||
@@ -1103,6 +1166,12 @@ export interface Output {
|
||||
// @public (undocumented)
|
||||
export const Output: OutputDecorator;
|
||||
|
||||
// @public (undocumented)
|
||||
export function output<T>(opts?: {
|
||||
alias?: string;
|
||||
isAsync?: boolean;
|
||||
}): EventEmitter<T>;
|
||||
|
||||
// @public
|
||||
export interface OutputDecorator {
|
||||
(alias?: string): any;
|
||||
@@ -1204,7 +1273,7 @@ export class QueryList<T> implements Iterable<T> {
|
||||
get changes(): Observable<any>;
|
||||
destroy(): void;
|
||||
// (undocumented)
|
||||
readonly dirty = true;
|
||||
readonly dirty: boolean;
|
||||
filter<S extends T>(predicate: (value: T, index: number, array: readonly T[]) => value is S): S[];
|
||||
// (undocumented)
|
||||
filter(predicate: (value: T, index: number, array: readonly T[]) => unknown): T[];
|
||||
@@ -1219,6 +1288,7 @@ export class QueryList<T> implements Iterable<T> {
|
||||
readonly length: number;
|
||||
map<U>(fn: (item: T, index: number, array: T[]) => U): U[];
|
||||
notifyOnChanges(): void;
|
||||
onDirty(cb: () => void): void;
|
||||
reduce<U>(fn: (prevValue: U, curValue: T, curIndex: number, array: T[]) => U, init: U): U;
|
||||
reset(resultsTree: Array<T | any[]>, identityAccessor?: (value: T) => unknown): void;
|
||||
setDirty(): void;
|
||||
@@ -1521,6 +1591,12 @@ export type ViewChild = Query;
|
||||
// @public
|
||||
export const ViewChild: ViewChildDecorator;
|
||||
|
||||
// @public (undocumented)
|
||||
export function viewChild<T>(selector: ProviderToken<T> | string, opts?: {
|
||||
read?: any;
|
||||
static?: boolean;
|
||||
}): Signal<T | undefined>;
|
||||
|
||||
// @public
|
||||
export interface ViewChildDecorator {
|
||||
(selector: ProviderToken<unknown> | Function | string, opts?: {
|
||||
@@ -1540,6 +1616,12 @@ export type ViewChildren = Query;
|
||||
// @public
|
||||
export const ViewChildren: ViewChildrenDecorator;
|
||||
|
||||
// @public (undocumented)
|
||||
export function viewChildren<T>(selector: ProviderToken<T> | string, opts?: {
|
||||
read?: any;
|
||||
emitDistinctChangesOnly?: boolean;
|
||||
}): Signal<T[]>;
|
||||
|
||||
// @public
|
||||
export interface ViewChildrenDecorator {
|
||||
(selector: ProviderToken<unknown> | Function | string, opts?: {
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
"ng-dev": "ts-node --esm --project .ng-dev/tsconfig.json --transpile-only node_modules/@angular/ng-dev/bundles/cli.mjs",
|
||||
"build": "ts-node --esm --project scripts/tsconfig.json scripts/build/build-packages-dist.mts",
|
||||
"test": "bazelisk test",
|
||||
"test:ci": "bazelisk test -- //... -//devtools/... -//aio/... && bazelisk test --//packages/compiler:use_template_pipeline //packages/compiler-cli/test/compliance/full",
|
||||
"test:ci": "bazelisk test --test_tag_filters=-only-template-pipeline -- //... -//devtools/... -//aio/... && bazelisk test --build_tests_only --//packages/compiler:use_template_pipeline --test_tag_filters=only-template-pipeline,also-template-pipeline //packages/...",
|
||||
"test-tsec": "bazelisk test //... --build_tag_filters=tsec --test_tag_filters=tsec",
|
||||
"lint": "yarn -s tslint && yarn -s ng-dev format changed --check",
|
||||
"tslint": "tslint -c tslint.json --project tsconfig-tslint.json",
|
||||
|
||||
@@ -41,6 +41,9 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
|
||||
case '||':
|
||||
case '??':
|
||||
return t.logicalExpression(operator, leftOperand, rightOperand);
|
||||
case '=':
|
||||
// TODO(signals): Should `WriteVarExpr` be used instead??
|
||||
return this.createAssignment(leftOperand, rightOperand);
|
||||
default:
|
||||
return t.binaryExpression(operator, leftOperand, rightOperand);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {ErrorCode, FatalDiagnosticError, makeRelatedInformation} from '../../../
|
||||
import {assertSuccessfulReferenceEmit, ImportFlags, Reference, ReferenceEmitter} from '../../../imports';
|
||||
import {ClassPropertyMapping, HostDirectiveMeta, InputMapping, InputTransform} from '../../../metadata';
|
||||
import {DynamicValue, EnumValue, PartialEvaluator, ResolvedValue} from '../../../partial_evaluator';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral} from '../../../reflection';
|
||||
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, Import, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral} from '../../../reflection';
|
||||
import {createSourceSpan, createValueHasWrongTypeError, forwardRefResolver, getConstructorDependencies, ReferencesRegistry, toR3Reference, tryUnwrapForwardRef, unwrapConstructorDependencies, unwrapExpression, validateConstructorDependencies, wrapFunctionExpressionsInParens, wrapTypeReference,} from '../../common';
|
||||
|
||||
const EMPTY_OBJECT: {[key: string]: string} = {};
|
||||
@@ -76,17 +76,33 @@ export function extractDirectiveMetadata(
|
||||
// Construct the map of inputs both from the @Directive/@Component
|
||||
// decorator, and the decorated fields.
|
||||
const inputsFromMeta = parseInputsArray(clazz, directive, evaluator, reflector, refEmitter);
|
||||
const inputsFromFields = parseInputFields(
|
||||
let inputsFromFields = parseInputFields(
|
||||
clazz, filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), evaluator,
|
||||
reflector, refEmitter);
|
||||
|
||||
// TODO(signals)
|
||||
inputsFromFields = {
|
||||
...inputsFromFields,
|
||||
...findAndParseSignalInputs(reflector, evaluator, refEmitter, coreModule, clazz, members)
|
||||
};
|
||||
const inputs = ClassPropertyMapping.fromMappedObject({...inputsFromMeta, ...inputsFromFields});
|
||||
|
||||
// And outputs.
|
||||
const outputsFromMeta = parseOutputsArray(directive, evaluator);
|
||||
const outputsFromFields = parseOutputFields(
|
||||
let outputsFromFields = parseOutputFields(
|
||||
filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), evaluator);
|
||||
|
||||
// TODO(signals)
|
||||
outputsFromFields = {
|
||||
...outputsFromFields,
|
||||
...findAndParseSignalOutputs(reflector, evaluator, coreModule, members)
|
||||
};
|
||||
|
||||
const outputs = ClassPropertyMapping.fromMappedObject({...outputsFromMeta, ...outputsFromFields});
|
||||
|
||||
const signalQueryDefinitions =
|
||||
findAndParseSignalQueries(reflector, evaluator, coreModule, members);
|
||||
|
||||
// Construct the list of queries.
|
||||
const contentChildFromFields = queriesFromFields(
|
||||
filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), reflector,
|
||||
@@ -95,7 +111,8 @@ export function extractDirectiveMetadata(
|
||||
filterToMembersWithDecorator(decoratedElements, 'ContentChildren', coreModule), reflector,
|
||||
evaluator);
|
||||
|
||||
const queries = [...contentChildFromFields, ...contentChildrenFromFields];
|
||||
const queries =
|
||||
[...contentChildFromFields, ...contentChildrenFromFields, ...signalQueryDefinitions.content];
|
||||
|
||||
// Construct the list of view queries.
|
||||
const viewChildFromFields = queriesFromFields(
|
||||
@@ -104,7 +121,8 @@ export function extractDirectiveMetadata(
|
||||
const viewChildrenFromFields = queriesFromFields(
|
||||
filterToMembersWithDecorator(decoratedElements, 'ViewChildren', coreModule), reflector,
|
||||
evaluator);
|
||||
const viewQueries = [...viewChildFromFields, ...viewChildrenFromFields];
|
||||
const viewQueries =
|
||||
[...viewChildFromFields, ...viewChildrenFromFields, ...signalQueryDefinitions.view];
|
||||
|
||||
if (directive.has('queries')) {
|
||||
const queriesFromDecorator =
|
||||
@@ -241,13 +259,13 @@ export function extractDirectiveMetadata(
|
||||
}
|
||||
|
||||
export function extractQueryMetadata(
|
||||
exprNode: ts.Node, name: string, args: ReadonlyArray<ts.Expression>, propertyName: string,
|
||||
exprNode: ts.Node, name: QueryType, args: ReadonlyArray<ts.Expression>, propertyName: string,
|
||||
reflector: ReflectionHost, evaluator: PartialEvaluator): R3QueryMetadata {
|
||||
if (args.length === 0) {
|
||||
throw new FatalDiagnosticError(
|
||||
ErrorCode.DECORATOR_ARITY_WRONG, exprNode, `@${name} must have arguments`);
|
||||
ErrorCode.QUERY_DEFINITION_ARITY_WRONG, exprNode, `${name.forError} must have arguments`);
|
||||
}
|
||||
const first = name === 'ViewChild' || name === 'ContentChild';
|
||||
const first = name.first;
|
||||
const forwardReferenceTarget = tryUnwrapForwardRef(args[0], reflector);
|
||||
const node = forwardReferenceTarget ?? args[0];
|
||||
|
||||
@@ -265,23 +283,24 @@ export function extractQueryMetadata(
|
||||
forwardReferenceTarget !== null ? ForwardRefHandling.Unwrapped : ForwardRefHandling.None);
|
||||
} else if (typeof arg === 'string') {
|
||||
predicate = [arg];
|
||||
} else if (isStringArrayOrDie(arg, `@${name} predicate`, node)) {
|
||||
} else if (isStringArrayOrDie(arg, `${name.forError} predicate`, node)) {
|
||||
predicate = arg;
|
||||
} else {
|
||||
throw createValueHasWrongTypeError(node, arg, `@${name} predicate cannot be interpreted`);
|
||||
throw createValueHasWrongTypeError(
|
||||
node, arg, `${name.forError} predicate cannot be interpreted`);
|
||||
}
|
||||
|
||||
// Extract the read and descendants options.
|
||||
let read: Expression|null = null;
|
||||
// The default value for descendants is true for every decorator except @ContentChildren.
|
||||
let descendants: boolean = name !== 'ContentChildren';
|
||||
let descendants: boolean = !(name.type === 'content' && name.first === false);
|
||||
let emitDistinctChangesOnly: boolean = emitDistinctChangesOnlyDefaultValue;
|
||||
if (args.length === 2) {
|
||||
const optionsExpr = unwrapExpression(args[1]);
|
||||
if (!ts.isObjectLiteralExpression(optionsExpr)) {
|
||||
throw new FatalDiagnosticError(
|
||||
ErrorCode.DECORATOR_ARG_NOT_LITERAL, optionsExpr,
|
||||
`@${name} options must be an object literal`);
|
||||
ErrorCode.QUERY_DEFINITION_ARG_NOT_LITERAL, optionsExpr,
|
||||
`${name.forError} options must be an object literal`);
|
||||
}
|
||||
const options = reflectObjectLiteral(optionsExpr);
|
||||
if (options.has('read')) {
|
||||
@@ -293,7 +312,8 @@ export function extractQueryMetadata(
|
||||
const descendantsValue = evaluator.evaluate(descendantsExpr);
|
||||
if (typeof descendantsValue !== 'boolean') {
|
||||
throw createValueHasWrongTypeError(
|
||||
descendantsExpr, descendantsValue, `@${name} options.descendants must be a boolean`);
|
||||
descendantsExpr, descendantsValue,
|
||||
`${name.forError} options.descendants must be a boolean`);
|
||||
}
|
||||
descendants = descendantsValue;
|
||||
}
|
||||
@@ -304,7 +324,7 @@ export function extractQueryMetadata(
|
||||
if (typeof emitDistinctChangesOnlyValue !== 'boolean') {
|
||||
throw createValueHasWrongTypeError(
|
||||
emitDistinctChangesOnlyExpr, emitDistinctChangesOnlyValue,
|
||||
`@${name} options.emitDistinctChangesOnly must be a boolean`);
|
||||
`${name.forError} options.emitDistinctChangesOnly must be a boolean`);
|
||||
}
|
||||
emitDistinctChangesOnly = emitDistinctChangesOnlyValue;
|
||||
}
|
||||
@@ -313,7 +333,7 @@ export function extractQueryMetadata(
|
||||
const staticValue = evaluator.evaluate(options.get('static')!);
|
||||
if (typeof staticValue !== 'boolean') {
|
||||
throw createValueHasWrongTypeError(
|
||||
node, staticValue, `@${name} options.static must be a boolean`);
|
||||
node, staticValue, `${name.forError} options.static must be a boolean`);
|
||||
}
|
||||
isStatic = staticValue;
|
||||
}
|
||||
@@ -321,7 +341,7 @@ export function extractQueryMetadata(
|
||||
} else if (args.length > 2) {
|
||||
// Too many arguments.
|
||||
throw new FatalDiagnosticError(
|
||||
ErrorCode.DECORATOR_ARITY_WRONG, node, `@${name} has too many arguments`);
|
||||
ErrorCode.QUERY_DEFINITION_ARITY_WRONG, node, `${name.forError} has too many arguments`);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -414,6 +434,29 @@ export function extractHostBindings(
|
||||
return bindings;
|
||||
}
|
||||
|
||||
interface QueryType {
|
||||
forError: string;
|
||||
first: boolean;
|
||||
type: 'view'|'content';
|
||||
}
|
||||
|
||||
function categorizeQueryByDecoratorName(name: string): QueryType {
|
||||
const forError = `@${name}`;
|
||||
|
||||
switch (name) {
|
||||
case 'ViewChild':
|
||||
return {first: true, type: 'view', forError};
|
||||
case 'ViewChildren':
|
||||
return {first: false, type: 'view', forError};
|
||||
case 'ContentChild':
|
||||
return {first: true, type: 'content', forError};
|
||||
case 'ContentChildren':
|
||||
return {first: false, type: 'content', forError};
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected query name: ${name}`);
|
||||
}
|
||||
|
||||
function extractQueriesFromDecorator(
|
||||
queryData: ts.Expression, reflector: ReflectionHost, evaluator: PartialEvaluator,
|
||||
isCore: boolean): {
|
||||
@@ -450,7 +493,8 @@ function extractQueriesFromDecorator(
|
||||
}
|
||||
|
||||
const query = extractQueryMetadata(
|
||||
queryExpr, type.name, queryExpr.arguments || [], propertyName, reflector, evaluator);
|
||||
queryExpr, categorizeQueryByDecoratorName(type.name), queryExpr.arguments || [],
|
||||
propertyName, reflector, evaluator);
|
||||
if (type.name.startsWith('Content')) {
|
||||
content.push(query);
|
||||
} else {
|
||||
@@ -515,7 +559,8 @@ function queriesFromFields(
|
||||
'Query decorator must go on a property-type member');
|
||||
}
|
||||
return extractQueryMetadata(
|
||||
node, decorator.name, decorator.args || [], member.name, reflector, evaluator);
|
||||
node, categorizeQueryByDecoratorName(decorator.name), decorator.args || [], member.name,
|
||||
reflector, evaluator);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -728,8 +773,8 @@ function parseInputTransformFunction(
|
||||
}
|
||||
|
||||
// Skip over `this` parameters since they're typing the context, not the actual parameter.
|
||||
// `this` parameters are guaranteed to be first if they exist, and the only to distinguish them
|
||||
// is using the name, TS doesn't have a special AST for them.
|
||||
// `this` parameters are guaranteed to be first if they exist, and the only to distinguish
|
||||
// them is using the name, TS doesn't have a special AST for them.
|
||||
const firstParam = definition.parameters[0]?.name === 'this' ? definition.parameters[1] :
|
||||
definition.parameters[0];
|
||||
|
||||
@@ -861,6 +906,201 @@ function evaluateHostExpressionBindings(
|
||||
return bindings;
|
||||
}
|
||||
|
||||
function getOptionsExpressionForInputCall(call: ts.CallExpression): ts.Expression|null {
|
||||
if (call.arguments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (call.arguments.length === 2) {
|
||||
return call.arguments[1];
|
||||
}
|
||||
|
||||
// If the first argument is not an object expression, it's an initial value-
|
||||
// but not the options argument.
|
||||
// TODO(signals): Might be able to use partial evaluator here.. need to decide on that.
|
||||
if (!ts.isObjectLiteralExpression(unwrapExpression(call.arguments[0]))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return call.arguments[0];
|
||||
}
|
||||
|
||||
// TODO(signals)
|
||||
function findAndParseSignalInputs(
|
||||
reflector: ReflectionHost, evaluator: PartialEvaluator, refEmitter: ReferenceEmitter,
|
||||
coreModule: string|undefined, clazz: ClassDeclaration,
|
||||
members: ClassMember[]): Record<string, InputMapping> {
|
||||
const res: Record<string, InputMapping> = {};
|
||||
|
||||
for (const m of members) {
|
||||
if (m.value === null) {
|
||||
continue;
|
||||
}
|
||||
const value = unwrapExpression(m.value);
|
||||
if (!ts.isCallExpression(value)) {
|
||||
continue;
|
||||
}
|
||||
const callTarget = unwrapExpression(value.expression);
|
||||
if (!ts.isIdentifier(callTarget)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isCoreSymbolReference(callTarget, 'input', reflector, coreModule)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const optionsNode = getOptionsExpressionForInputCall(value);
|
||||
const options = optionsNode === null ? null : evaluator.evaluate(optionsNode);
|
||||
|
||||
if (options !== null && !(options instanceof Map)) {
|
||||
// TODO(signals): proper diagnostic
|
||||
throw new Error('Input options are not an object..');
|
||||
}
|
||||
|
||||
let transform: InputTransform|null = null;
|
||||
|
||||
if (options?.has('transform')) {
|
||||
const transformValue = options.get('transform');
|
||||
|
||||
if (!(transformValue instanceof DynamicValue) && !(transformValue instanceof Reference)) {
|
||||
throw createValueHasWrongTypeError(
|
||||
optionsNode!, transformValue, `Input transform must be a function`);
|
||||
}
|
||||
|
||||
transform = parseInputTransformFunction(clazz, m.name, transformValue, reflector, refEmitter);
|
||||
}
|
||||
|
||||
res[m.name] = {
|
||||
classPropertyName: m.name,
|
||||
bindingPropertyName: options?.get('alias')?.toString() /* TODO */ ?? m.name,
|
||||
required: !!options?.get('required'),
|
||||
transform,
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// TODO(signals)
|
||||
function findAndParseSignalOutputs(
|
||||
reflector: ReflectionHost, evaluator: PartialEvaluator, coreModule: string|undefined,
|
||||
members: ClassMember[]): Record<string, string> {
|
||||
const res: Record<string, string> = {};
|
||||
|
||||
for (const m of members) {
|
||||
if (m.value === null) {
|
||||
continue;
|
||||
}
|
||||
const value = unwrapExpression(m.value);
|
||||
if (!ts.isCallExpression(value)) {
|
||||
continue;
|
||||
}
|
||||
const callTarget = unwrapExpression(value.expression);
|
||||
if (!ts.isIdentifier(callTarget)) {
|
||||
continue;
|
||||
}
|
||||
if (!isCoreSymbolReference(callTarget, 'output', reflector, coreModule)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const propertyName = m.name;
|
||||
const optionsNode = value.arguments[0];
|
||||
let publicOutputName = propertyName;
|
||||
|
||||
// If there is an options object, we look for a potential `alias`.
|
||||
if (optionsNode !== undefined) {
|
||||
const options = evaluator.evaluate(optionsNode);
|
||||
if (!(options instanceof Map)) {
|
||||
throw createValueHasWrongTypeError(
|
||||
optionsNode, options, `Output options must be an object.`);
|
||||
}
|
||||
|
||||
const alias = options.get('alias');
|
||||
if (alias !== undefined) {
|
||||
if (typeof alias !== 'string') {
|
||||
throw createValueHasWrongTypeError(
|
||||
optionsNode, options, `Alias must resolve to a string`);
|
||||
}
|
||||
publicOutputName = alias;
|
||||
}
|
||||
}
|
||||
|
||||
res[propertyName] = publicOutputName;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function findAndParseSignalQueries(
|
||||
reflector: ReflectionHost, evaluator: PartialEvaluator, coreModule: string|undefined,
|
||||
members: ClassMember[]): {view: R3QueryMetadata[], content: R3QueryMetadata[]} {
|
||||
const res: {view: R3QueryMetadata[], content: R3QueryMetadata[]} = {view: [], content: []};
|
||||
|
||||
for (const m of members) {
|
||||
if (m.value === null) {
|
||||
continue;
|
||||
}
|
||||
const value = unwrapExpression(m.value);
|
||||
if (!ts.isCallExpression(value)) {
|
||||
continue;
|
||||
}
|
||||
const callTarget = unwrapExpression(value.expression);
|
||||
if (!ts.isIdentifier(callTarget)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const viewChild: QueryType|false =
|
||||
isCoreSymbolReference(callTarget, 'viewChild', reflector, coreModule) && {
|
||||
first: true,
|
||||
forError: 'viewChild()',
|
||||
type: 'view',
|
||||
};
|
||||
|
||||
const viewChildren: QueryType|false =
|
||||
isCoreSymbolReference(callTarget, 'viewChildren', reflector, coreModule) && {
|
||||
first: false,
|
||||
forError: 'viewChildren()',
|
||||
type: 'view',
|
||||
};
|
||||
|
||||
const contentChild: QueryType|false =
|
||||
isCoreSymbolReference(callTarget, 'contentChild', reflector, coreModule) && {
|
||||
first: true,
|
||||
forError: 'contentChild()',
|
||||
type: 'content',
|
||||
};
|
||||
|
||||
const contentChildren: QueryType|false =
|
||||
isCoreSymbolReference(callTarget, 'contentChildren', reflector, coreModule) && {
|
||||
first: false,
|
||||
forError: 'contentChildren()',
|
||||
type: 'content',
|
||||
};
|
||||
|
||||
const query = viewChild || viewChildren || contentChild || contentChildren;
|
||||
|
||||
if (query === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const metadata =
|
||||
extractQueryMetadata(callTarget, query, value.arguments, m.name, reflector, evaluator);
|
||||
|
||||
if (query.type === 'view') {
|
||||
res.view.push(metadata);
|
||||
} else {
|
||||
res.content.push(metadata);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function isCoreSymbolReference(
|
||||
callTarget: ts.Identifier, name: string, reflector: ReflectionHost,
|
||||
coreModule: string|undefined): boolean {
|
||||
const imp = reflector.getImportOfIdentifier(callTarget);
|
||||
return imp !== null ? imp.from === coreModule && imp.name === name :
|
||||
callTarget.text === name && coreModule === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and prepares the host directives metadata from an array literal expression.
|
||||
* @param rawHostDirectives Expression that defined the `hostDirectives`.
|
||||
|
||||
@@ -367,6 +367,14 @@ export enum ErrorCode {
|
||||
*/
|
||||
INJECTABLE_DUPLICATE_PROV = 9001,
|
||||
|
||||
/** Invalid number of arguments when defining a query, either using decorator or signal APIs. */
|
||||
QUERY_DEFINITION_ARITY_WRONG = 9002,
|
||||
|
||||
/**
|
||||
* An argument for defining a query is expected to be a literal, but turned out to be different.
|
||||
*/
|
||||
QUERY_DEFINITION_ARG_NOT_LITERAL = 9003,
|
||||
|
||||
// 10XXX error codes are reserved for diagnostics with categories other than
|
||||
// `ts.DiagnosticCategory.Error`. These diagnostics are generated by the compiler when configured
|
||||
// to do so by a tool such as the Language Service, or by the Language Service itself.
|
||||
|
||||
@@ -119,3 +119,45 @@ export interface OnDestroy {
|
||||
export interface TrackByFunction<T> {
|
||||
<U extends T>(index: number, item: T&U): any;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* SIGNALS / REACTIVITY
|
||||
*/
|
||||
|
||||
export type Signal<T> = () => T&{__signal: true};
|
||||
export type WritableSignal<T> = Signal<T>&{
|
||||
set(v: T): void;
|
||||
};
|
||||
|
||||
export function signal<T>(_initialValue: T): WritableSignal<T> {
|
||||
return null!;
|
||||
}
|
||||
|
||||
export function viewChild<T>(
|
||||
_selector: object|InjectionToken<T>|Function|string,
|
||||
_opts?: {read?: any, static?: boolean}): Signal<T|undefined> {
|
||||
return null!;
|
||||
}
|
||||
|
||||
export function viewChildren<T>(
|
||||
_selector: object|InjectionToken<T>|Function|string,
|
||||
_opts?: {read?: any, emitDistinctChangesOnly?: boolean}): Signal<T[]> {
|
||||
return null!;
|
||||
}
|
||||
|
||||
export function contentChild<T>(
|
||||
_selector: object|InjectionToken<T>|string,
|
||||
_opts?: {descendants?: boolean, read?: any, static?: boolean}): Signal<T|undefined> {
|
||||
return null!;
|
||||
}
|
||||
|
||||
export function contentChildren<T>(
|
||||
_selector: object|InjectionToken<T>|string,
|
||||
_opts?: {descendants?: boolean, read?: any, emitDistinctChangesOnly?: boolean}): Signal<T[]> {
|
||||
return null!;
|
||||
}
|
||||
|
||||
export function output<T>(_opts?: {isAsync?: boolean, alias?: string}): EventEmitter<T> {
|
||||
return null!;
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ export type UnaryOperator = '+'|'-'|'!';
|
||||
* The binary operators supported by the `AstFactory`.
|
||||
*/
|
||||
export type BinaryOperator =
|
||||
'&&'|'>'|'>='|'&'|'/'|'=='|'==='|'<'|'<='|'-'|'%'|'*'|'!='|'!=='|'||'|'+'|'??';
|
||||
'&&'|'>'|'>='|'&'|'/'|'='|'=='|'==='|'<'|'<='|'-'|'%'|'*'|'!='|'!=='|'||'|'+'|'??';
|
||||
|
||||
/**
|
||||
* The original location of the start or end of a node created by the `AstFactory`.
|
||||
|
||||
@@ -18,6 +18,7 @@ const UNARY_OPERATORS = new Map<o.UnaryOperator, UnaryOperator>([
|
||||
|
||||
const BINARY_OPERATORS = new Map<o.BinaryOperator, BinaryOperator>([
|
||||
[o.BinaryOperator.And, '&&'],
|
||||
[o.BinaryOperator.Assign, '='],
|
||||
[o.BinaryOperator.Bigger, '>'],
|
||||
[o.BinaryOperator.BiggerEquals, '>='],
|
||||
[o.BinaryOperator.BitwiseAnd, '&'],
|
||||
|
||||
@@ -31,6 +31,7 @@ const UNARY_OPERATORS: Record<UnaryOperator, ts.PrefixUnaryOperator> = {
|
||||
};
|
||||
|
||||
const BINARY_OPERATORS: Record<BinaryOperator, ts.BinaryOperator> = {
|
||||
'=': ts.SyntaxKind.EqualsToken,
|
||||
'&&': ts.SyntaxKind.AmpersandAmpersandToken,
|
||||
'>': ts.SyntaxKind.GreaterThanToken,
|
||||
'>=': ts.SyntaxKind.GreaterThanEqualsToken,
|
||||
|
||||
@@ -182,6 +182,11 @@ export class Environment implements ReferenceEmitEnvironment {
|
||||
type, this.contextFile, this.reflector, this.refEmitter, this.importManager);
|
||||
}
|
||||
|
||||
referenceExternalSymbol(moduleName: string, name: string): ts.Expression {
|
||||
const external = new ExternalExpr({moduleName, name});
|
||||
return translateExpression(external, this.importManager);
|
||||
}
|
||||
|
||||
getPreludeStatements(): ts.Statement[] {
|
||||
return [
|
||||
...this.pipeInstStatements,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, BindingPipe, BindingType, BoundTarget, Call, DYNAMIC_TYPE, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, SafeCall, SafePropertyRead, SchemaMetadata, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstElement, TmplAstIcu, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, TransplantedType} from '@angular/compiler';
|
||||
import {AST, BindingPipe, BindingType, BoundTarget, Call, DYNAMIC_TYPE, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, R3Identifiers, SafeCall, SafePropertyRead, SchemaMetadata, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstElement, TmplAstIcu, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, TransplantedType} from '@angular/compiler';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {Reference} from '../../imports';
|
||||
@@ -753,6 +753,8 @@ class TcbDirectiveInputsOp extends TcbOp {
|
||||
dirId = this.scope.resolve(this.node, this.dir);
|
||||
}
|
||||
|
||||
// TODO(signals): signal input with a restricted modifier
|
||||
|
||||
const id = this.tcb.allocateId();
|
||||
const dirTypeRef = this.tcb.env.referenceType(this.dir.ref);
|
||||
if (!ts.isTypeReferenceNode(dirTypeRef)) {
|
||||
@@ -770,14 +772,42 @@ class TcbDirectiveInputsOp extends TcbOp {
|
||||
dirId = this.scope.resolve(this.node, this.dir);
|
||||
}
|
||||
|
||||
// To get errors assign directly to the fields on the instance, using property access
|
||||
// when possible. String literal fields may not be valid JS identifiers so we use
|
||||
// literal element access instead for those cases.
|
||||
target = this.dir.stringLiteralInputFields.has(fieldName) ?
|
||||
ts.factory.createElementAccessExpression(
|
||||
dirId, ts.factory.createStringLiteral(fieldName)) :
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
dirId, ts.factory.createIdentifier(fieldName));
|
||||
if (this.dir.isSignal) {
|
||||
const dirTypeRef = this.tcb.env.referenceType(this.dir.ref);
|
||||
if (!ts.isTypeReferenceNode(dirTypeRef)) {
|
||||
throw new Error(
|
||||
`Expected TypeReferenceNode from reference to ${this.dir.ref.debugName}`);
|
||||
}
|
||||
|
||||
const getSignalWriteType = this.tcb.env.referenceExternalType(
|
||||
R3Identifiers.getInputSignalWriteType.moduleName,
|
||||
R3Identifiers.getInputSignalWriteType.name);
|
||||
if (!ts.isTypeReferenceNode(getSignalWriteType)) {
|
||||
throw new Error(`Expected TypeReferenceNode from reference to ${
|
||||
R3Identifiers.getInputSignalWriteType.name}`);
|
||||
}
|
||||
|
||||
const inputFieldType = ts.factory.createIndexedAccessTypeNode(
|
||||
ts.factory.createTypeQueryNode(dirId as ts.Identifier),
|
||||
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(fieldName)));
|
||||
|
||||
const inputWriteTypeId = this.tcb.allocateId();
|
||||
const inputWriteTypeSt = tsDeclareVariable(
|
||||
inputWriteTypeId,
|
||||
ts.factory.createTypeReferenceNode(getSignalWriteType.typeName, [inputFieldType]));
|
||||
this.scope.addStatement(inputWriteTypeSt);
|
||||
|
||||
target = inputWriteTypeId;
|
||||
} else {
|
||||
// To get errors assign directly to the fields on the instance, using property access
|
||||
// when possible. String literal fields may not be valid JS identifiers so we use
|
||||
// literal element access instead for those cases.
|
||||
target = this.dir.stringLiteralInputFields.has(fieldName) ?
|
||||
ts.factory.createElementAccessExpression(
|
||||
dirId, ts.factory.createStringLiteral(fieldName)) :
|
||||
ts.factory.createPropertyAccessExpression(
|
||||
dirId, ts.factory.createIdentifier(fieldName));
|
||||
}
|
||||
}
|
||||
|
||||
if (attr.attribute.keySpan !== undefined) {
|
||||
|
||||
@@ -18,6 +18,8 @@ jasmine_node_test(
|
||||
"//packages/compiler-cli/test/compliance/test_cases",
|
||||
],
|
||||
shard_count = 2,
|
||||
# Also run these tests with the template pipeline.
|
||||
tags = ["also-template-pipeline"],
|
||||
deps = [
|
||||
":test_lib",
|
||||
],
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: output_definition.js
|
||||
****************************************************************************************************/
|
||||
import { Component, output } from '@angular/core';
|
||||
import * as i0 from "@angular/core";
|
||||
export class SensorComp {
|
||||
constructor() {
|
||||
this.pressed = output();
|
||||
this._internalName = output({ alias: 'touched' });
|
||||
}
|
||||
}
|
||||
SensorComp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SensorComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
SensorComp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: SensorComp, isStandalone: true, isSignal: true, selector: "ng-component", outputs: { pressed: "pressed", _internalName: "touched" }, ngImport: i0, template: '', isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SensorComp, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
signals: true,
|
||||
standalone: true,
|
||||
template: '',
|
||||
}]
|
||||
}] });
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: output_definition.d.ts
|
||||
****************************************************************************************************/
|
||||
import * as i0 from "@angular/core";
|
||||
export declare class SensorComp {
|
||||
pressed: import("@angular/core").EventEmitter<void>;
|
||||
_internalName: import("@angular/core").EventEmitter<void>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<SensorComp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<SensorComp, "ng-component", never, {}, { "pressed": "pressed"; "_internalName": "touched"; }, never, never, true, never, true>;
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "../../../test_case_schema.json",
|
||||
"cases": [
|
||||
{
|
||||
"onlyForTemplatePipeline": true,
|
||||
"description": "should detect output in a signal component",
|
||||
"inputFiles": ["output_definition.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"files": [{"generated": "output_definition.js", "expected": "output_definition.js"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
SensorComp.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({
|
||||
// ...
|
||||
outputs: {
|
||||
pressed: "pressed",
|
||||
_internalName: "touched"
|
||||
},
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import {Component, output} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
signals: true,
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
export class SensorComp {
|
||||
pressed = output<void>();
|
||||
_internalName = output<void>({alias: 'touched'});
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: simple_binding.js
|
||||
****************************************************************************************************/
|
||||
import { Component, signal } from '@angular/core';
|
||||
import * as i0 from "@angular/core";
|
||||
export class LiteralValueBinding {
|
||||
}
|
||||
LiteralValueBinding.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: LiteralValueBinding, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
LiteralValueBinding.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: LiteralValueBinding, isSignal: true, selector: "app", ngImport: i0, template: `<button [disabled]="true"></button>`, isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: LiteralValueBinding, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button [disabled]="true"></button>`,
|
||||
}]
|
||||
}] });
|
||||
export class FromContextBindingStatic {
|
||||
constructor() {
|
||||
this.isDisabled = true;
|
||||
}
|
||||
}
|
||||
FromContextBindingStatic.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingStatic, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
FromContextBindingStatic.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: FromContextBindingStatic, isSignal: true, selector: "app", ngImport: i0, template: `<button [disabled]="isDisabled"></button>`, isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingStatic, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button [disabled]="isDisabled"></button>`,
|
||||
}]
|
||||
}] });
|
||||
export class FromContextBindingSignal {
|
||||
constructor() {
|
||||
this.isDisabled = signal(true);
|
||||
}
|
||||
}
|
||||
FromContextBindingSignal.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingSignal, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
FromContextBindingSignal.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: FromContextBindingSignal, isSignal: true, selector: "app", ngImport: i0, template: `<button [disabled]="isDisabled()"></button>`, isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingSignal, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button [disabled]="isDisabled()"></button>`,
|
||||
}]
|
||||
}] });
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: simple_binding.d.ts
|
||||
****************************************************************************************************/
|
||||
import * as i0 from "@angular/core";
|
||||
export declare class LiteralValueBinding {
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<LiteralValueBinding, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<LiteralValueBinding, "app", never, {}, {}, never, never, false, never, true>;
|
||||
}
|
||||
export declare class FromContextBindingStatic {
|
||||
isDisabled: boolean;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<FromContextBindingStatic, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<FromContextBindingStatic, "app", never, {}, {}, never, never, false, never, true>;
|
||||
}
|
||||
export declare class FromContextBindingSignal {
|
||||
isDisabled: import("@angular/core").WritableSignal<boolean>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<FromContextBindingSignal, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<FromContextBindingSignal, "app", never, {}, {}, never, never, false, never, true>;
|
||||
}
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: with_interpolation.js
|
||||
****************************************************************************************************/
|
||||
import { Component, signal } from '@angular/core';
|
||||
import * as i0 from "@angular/core";
|
||||
export class LiteralValueBinding {
|
||||
}
|
||||
LiteralValueBinding.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: LiteralValueBinding, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
LiteralValueBinding.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: LiteralValueBinding, isSignal: true, selector: "app", ngImport: i0, template: `<button title="This is a {{'submit'}} button"></button>`, isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: LiteralValueBinding, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button title="This is a {{'submit'}} button"></button>`,
|
||||
}]
|
||||
}] });
|
||||
export class FromContextBindingStatic {
|
||||
constructor() {
|
||||
this.type = 'submit';
|
||||
}
|
||||
}
|
||||
FromContextBindingStatic.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingStatic, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
FromContextBindingStatic.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: FromContextBindingStatic, isSignal: true, selector: "app", ngImport: i0, template: `<button title="This is a {{type}} button"></button>`, isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingStatic, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button title="This is a {{type}} button"></button>`,
|
||||
}]
|
||||
}] });
|
||||
export class FromContextBindingSignal {
|
||||
constructor() {
|
||||
this.type = signal('submit');
|
||||
}
|
||||
}
|
||||
FromContextBindingSignal.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingSignal, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
FromContextBindingSignal.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: FromContextBindingSignal, isSignal: true, selector: "app", ngImport: i0, template: `<button title="This is a {{type()}} button"></button>`, isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FromContextBindingSignal, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button title="This is a {{type()}} button"></button>`,
|
||||
}]
|
||||
}] });
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: with_interpolation.d.ts
|
||||
****************************************************************************************************/
|
||||
import * as i0 from "@angular/core";
|
||||
export declare class LiteralValueBinding {
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<LiteralValueBinding, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<LiteralValueBinding, "app", never, {}, {}, never, never, false, never, true>;
|
||||
}
|
||||
export declare class FromContextBindingStatic {
|
||||
type: string;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<FromContextBindingStatic, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<FromContextBindingStatic, "app", never, {}, {}, never, never, false, never, true>;
|
||||
}
|
||||
export declare class FromContextBindingSignal {
|
||||
type: import("@angular/core").WritableSignal<string>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<FromContextBindingSignal, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<FromContextBindingSignal, "app", never, {}, {}, never, never, false, never, true>;
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "../../test_case_schema.json",
|
||||
"cases": [
|
||||
{
|
||||
"onlyForTemplatePipeline": true,
|
||||
"description": "should support binding to DOM properties",
|
||||
"inputFiles": ["simple_binding.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"files": [{"generated": "simple_binding.js", "expected": "simple_binding.js"}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"onlyForTemplatePipeline": true,
|
||||
"description": "should support interpolation binding to DOM properties",
|
||||
"inputFiles": ["with_interpolation.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"files": [{"generated": "with_interpolation.js", "expected": "with_interpolation.js"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
template: function LiteralValueBinding_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelementStart(0, "button");
|
||||
i0.ɵɵpropertyCreate(1, "disabled", function () { return true; });
|
||||
i0.ɵɵelementEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
template: function FromContextBindingStatic_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelementStart(0, "button");
|
||||
i0.ɵɵpropertyCreate(1, "disabled", function () { return ctx.isDisabled; });
|
||||
i0.ɵɵelementEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
template: function FromContextBindingSignal_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelementStart(0, "button");
|
||||
i0.ɵɵpropertyCreate(1, "disabled", function () { return ctx.isDisabled(); });
|
||||
i0.ɵɵelementEnd();
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import {Component, signal} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button [disabled]="true"></button>`,
|
||||
})
|
||||
export class LiteralValueBinding {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button [disabled]="isDisabled"></button>`,
|
||||
})
|
||||
export class FromContextBindingStatic {
|
||||
isDisabled = true;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button [disabled]="isDisabled()"></button>`,
|
||||
})
|
||||
export class FromContextBindingSignal {
|
||||
isDisabled = signal(true);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
template: function LiteralValueBinding_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelementStart(0, "button");
|
||||
i0.ɵɵpropertyCreate(1, "title", function () { return i0.ɵɵstringifyInterpolation`This is a ${"submit"} button`; });
|
||||
i0.ɵɵelementEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
template: function FromContextBindingStatic_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelementStart(0, "button");
|
||||
i0.ɵɵpropertyCreate(1, "title", function () { return i0.ɵɵstringifyInterpolation`This is a ${ctx.type} button`; });
|
||||
i0.ɵɵelementEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
template: function FromContextBindingSignal_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelementStart(0, "button");
|
||||
i0.ɵɵpropertyCreate(1, "title", function () { return i0.ɵɵstringifyInterpolation`This is a ${ctx.type()} button`; });
|
||||
i0.ɵɵelementEnd();
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import {Component, signal} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button title="This is a {{'submit'}} button"></button>`,
|
||||
})
|
||||
export class LiteralValueBinding {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button title="This is a {{type}} button"></button>`,
|
||||
})
|
||||
export class FromContextBindingStatic {
|
||||
type = 'submit';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button title="This is a {{type()}} button"></button>`,
|
||||
})
|
||||
export class FromContextBindingSignal {
|
||||
type = signal('submit');
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: view_child.js
|
||||
****************************************************************************************************/
|
||||
import { Component, viewChild } from '@angular/core';
|
||||
import * as i0 from "@angular/core";
|
||||
export class SimpleViewChildWithName {
|
||||
constructor() {
|
||||
this.buttonEl = viewChild('button');
|
||||
}
|
||||
}
|
||||
SimpleViewChildWithName.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleViewChildWithName, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
SimpleViewChildWithName.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: SimpleViewChildWithName, isSignal: true, selector: "app", viewQueries: [{ propertyName: "buttonEl", first: true, predicate: ["button"], descendants: true }], ngImport: i0, template: `<button #button></button>`, isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleViewChildWithName, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button #button></button>`,
|
||||
}]
|
||||
}] });
|
||||
export class ButtonComp {
|
||||
}
|
||||
ButtonComp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ButtonComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
ButtonComp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: ButtonComp, isStandalone: true, selector: "button-comp", ngImport: i0, template: '', isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ButtonComp, decorators: [{
|
||||
type: Component,
|
||||
args: [{ selector: 'button-comp', template: '', standalone: true }]
|
||||
}] });
|
||||
export class SimpleViewChildWithType {
|
||||
constructor() {
|
||||
this.buttonEl = viewChild(ButtonComp);
|
||||
}
|
||||
}
|
||||
SimpleViewChildWithType.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleViewChildWithType, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
SimpleViewChildWithType.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: SimpleViewChildWithType, isStandalone: true, isSignal: true, selector: "app", viewQueries: [{ propertyName: "buttonEl", first: true, predicate: ButtonComp, descendants: true }], ngImport: i0, template: `<button-comp></button-comp>`, isInline: true, dependencies: [{ kind: "component", type: ButtonComp, selector: "button-comp" }] });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleViewChildWithType, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
standalone: true,
|
||||
imports: [ButtonComp],
|
||||
template: `<button-comp></button-comp>`,
|
||||
}]
|
||||
}] });
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: view_child.d.ts
|
||||
****************************************************************************************************/
|
||||
import * as i0 from "@angular/core";
|
||||
export declare class SimpleViewChildWithName {
|
||||
buttonEl: import("@angular/core").Signal<unknown>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<SimpleViewChildWithName, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<SimpleViewChildWithName, "app", never, {}, {}, never, never, false, never, true>;
|
||||
}
|
||||
export declare class ButtonComp {
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<ButtonComp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<ButtonComp, "button-comp", never, {}, {}, never, never, true, never>;
|
||||
}
|
||||
export declare class SimpleViewChildWithType {
|
||||
buttonEl: import("@angular/core").Signal<unknown>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<SimpleViewChildWithType, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<SimpleViewChildWithType, "app", never, {}, {}, never, never, true, never, true>;
|
||||
}
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: view_children.js
|
||||
****************************************************************************************************/
|
||||
import { Component, viewChildren } from '@angular/core';
|
||||
import * as i0 from "@angular/core";
|
||||
export class SimpleViewChildrenWithName {
|
||||
constructor() {
|
||||
this.buttonEls = viewChildren('buttons');
|
||||
}
|
||||
}
|
||||
SimpleViewChildrenWithName.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleViewChildrenWithName, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
SimpleViewChildrenWithName.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: SimpleViewChildrenWithName, isSignal: true, selector: "app", viewQueries: [{ propertyName: "buttonEls", predicate: ["buttons"], descendants: true }], ngImport: i0, template: `
|
||||
<button #buttons></button>
|
||||
<button #buttons></button>
|
||||
`, isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleViewChildrenWithName, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `
|
||||
<button #buttons></button>
|
||||
<button #buttons></button>
|
||||
`,
|
||||
}]
|
||||
}] });
|
||||
export class ButtonComp {
|
||||
}
|
||||
ButtonComp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ButtonComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
ButtonComp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: ButtonComp, isStandalone: true, selector: "button-comp", ngImport: i0, template: '', isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ButtonComp, decorators: [{
|
||||
type: Component,
|
||||
args: [{ selector: 'button-comp', template: '', standalone: true }]
|
||||
}] });
|
||||
export class SimpleViewChildrenWithType {
|
||||
constructor() {
|
||||
this.buttonEls = viewChildren(ButtonComp);
|
||||
}
|
||||
}
|
||||
SimpleViewChildrenWithType.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleViewChildrenWithType, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
SimpleViewChildrenWithType.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: SimpleViewChildrenWithType, isStandalone: true, isSignal: true, selector: "app", viewQueries: [{ propertyName: "buttonEls", predicate: ButtonComp, descendants: true }], ngImport: i0, template: `
|
||||
<button-comp></button-comp>
|
||||
<button-comp></button-comp>
|
||||
`, isInline: true, dependencies: [{ kind: "component", type: ButtonComp, selector: "button-comp" }] });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleViewChildrenWithType, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
standalone: true,
|
||||
imports: [ButtonComp],
|
||||
template: `
|
||||
<button-comp></button-comp>
|
||||
<button-comp></button-comp>
|
||||
`,
|
||||
}]
|
||||
}] });
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: view_children.d.ts
|
||||
****************************************************************************************************/
|
||||
import * as i0 from "@angular/core";
|
||||
export declare class SimpleViewChildrenWithName {
|
||||
buttonEls: import("@angular/core").Signal<unknown[]>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<SimpleViewChildrenWithName, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<SimpleViewChildrenWithName, "app", never, {}, {}, never, never, false, never, true>;
|
||||
}
|
||||
export declare class ButtonComp {
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<ButtonComp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<ButtonComp, "button-comp", never, {}, {}, never, never, true, never>;
|
||||
}
|
||||
export declare class SimpleViewChildrenWithType {
|
||||
buttonEls: import("@angular/core").Signal<unknown[]>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<SimpleViewChildrenWithType, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<SimpleViewChildrenWithType, "app", never, {}, {}, never, never, true, never, true>;
|
||||
}
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: content_child.js
|
||||
****************************************************************************************************/
|
||||
import { Component, contentChild } from '@angular/core';
|
||||
import * as i0 from "@angular/core";
|
||||
export class SimpleContentChildWithName {
|
||||
constructor() {
|
||||
this.buttonEl = contentChild('button');
|
||||
}
|
||||
}
|
||||
SimpleContentChildWithName.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleContentChildWithName, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
SimpleContentChildWithName.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: SimpleContentChildWithName, isSignal: true, selector: "app", queries: [{ propertyName: "buttonEl", first: true, predicate: ["button"], descendants: true }], ngImport: i0, template: `{{buttonEl() !== undefined}}`, isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleContentChildWithName, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `{{buttonEl() !== undefined}}`,
|
||||
}]
|
||||
}] });
|
||||
export class ButtonComp {
|
||||
}
|
||||
ButtonComp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ButtonComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
ButtonComp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: ButtonComp, isStandalone: true, selector: "button-comp", ngImport: i0, template: '', isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ButtonComp, decorators: [{
|
||||
type: Component,
|
||||
args: [{ selector: 'button-comp', template: '', standalone: true }]
|
||||
}] });
|
||||
export class SimpleContentChildWithType {
|
||||
constructor() {
|
||||
this.buttonComp = contentChild(ButtonComp);
|
||||
}
|
||||
}
|
||||
SimpleContentChildWithType.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleContentChildWithType, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
SimpleContentChildWithType.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: SimpleContentChildWithType, isSignal: true, selector: "app", queries: [{ propertyName: "buttonComp", first: true, predicate: ButtonComp, descendants: true }], ngImport: i0, template: `{{buttonComp() !== undefined}}`, isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleContentChildWithType, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `{{buttonComp() !== undefined}}`,
|
||||
}]
|
||||
}] });
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: content_child.d.ts
|
||||
****************************************************************************************************/
|
||||
import * as i0 from "@angular/core";
|
||||
export declare class SimpleContentChildWithName {
|
||||
buttonEl: import("@angular/core").Signal<unknown>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<SimpleContentChildWithName, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<SimpleContentChildWithName, "app", never, {}, {}, ["buttonEl"], never, false, never, true>;
|
||||
}
|
||||
export declare class ButtonComp {
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<ButtonComp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<ButtonComp, "button-comp", never, {}, {}, never, never, true, never>;
|
||||
}
|
||||
export declare class SimpleContentChildWithType {
|
||||
buttonComp: import("@angular/core").Signal<unknown>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<SimpleContentChildWithType, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<SimpleContentChildWithType, "app", never, {}, {}, ["buttonComp"], never, false, never, true>;
|
||||
}
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: content_children.js
|
||||
****************************************************************************************************/
|
||||
import { Component, contentChildren } from '@angular/core';
|
||||
import * as i0 from "@angular/core";
|
||||
export class SimpleContentChildrenWithName {
|
||||
constructor() {
|
||||
this.buttonEls = contentChildren('button');
|
||||
}
|
||||
}
|
||||
SimpleContentChildrenWithName.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleContentChildrenWithName, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
SimpleContentChildrenWithName.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: SimpleContentChildrenWithName, isSignal: true, selector: "app", queries: [{ propertyName: "buttonEls", predicate: ["button"] }], ngImport: i0, template: `{{buttonEls().length}}`, isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleContentChildrenWithName, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `{{buttonEls().length}}`,
|
||||
}]
|
||||
}] });
|
||||
export class ButtonComp {
|
||||
}
|
||||
ButtonComp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ButtonComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
ButtonComp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: ButtonComp, isStandalone: true, selector: "button-comp", ngImport: i0, template: '', isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ButtonComp, decorators: [{
|
||||
type: Component,
|
||||
args: [{ selector: 'button-comp', template: '', standalone: true }]
|
||||
}] });
|
||||
export class SimpleContentChildrenWithType {
|
||||
constructor() {
|
||||
this.buttonComps = contentChildren(ButtonComp);
|
||||
}
|
||||
}
|
||||
SimpleContentChildrenWithType.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleContentChildrenWithType, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
SimpleContentChildrenWithType.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: SimpleContentChildrenWithType, isSignal: true, selector: "app", queries: [{ propertyName: "buttonComps", predicate: ButtonComp }], ngImport: i0, template: `{{buttonComps().length}}`, isInline: true });
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SimpleContentChildrenWithType, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `{{buttonComps().length}}`,
|
||||
}]
|
||||
}] });
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: content_children.d.ts
|
||||
****************************************************************************************************/
|
||||
import * as i0 from "@angular/core";
|
||||
export declare class SimpleContentChildrenWithName {
|
||||
buttonEls: import("@angular/core").Signal<unknown[]>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<SimpleContentChildrenWithName, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<SimpleContentChildrenWithName, "app", never, {}, {}, ["buttonEls"], never, false, never, true>;
|
||||
}
|
||||
export declare class ButtonComp {
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<ButtonComp, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<ButtonComp, "button-comp", never, {}, {}, never, never, true, never>;
|
||||
}
|
||||
export declare class SimpleContentChildrenWithType {
|
||||
buttonComps: import("@angular/core").Signal<unknown[]>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<SimpleContentChildrenWithType, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<SimpleContentChildrenWithType, "app", never, {}, {}, ["buttonComps"], never, false, never, true>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"$schema": "../../test_case_schema.json",
|
||||
"cases": [
|
||||
{
|
||||
"onlyForTemplatePipeline": true,
|
||||
"description": "should support querying for a view child",
|
||||
"inputFiles": ["view_child.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"files": [{"generated": "view_child.js", "expected": "view_child.js"}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"onlyForTemplatePipeline": true,
|
||||
"description": "should support querying for multiple view children",
|
||||
"inputFiles": ["view_children.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"files": [{"generated": "view_children.js", "expected": "view_children.js"}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"onlyForTemplatePipeline": true,
|
||||
"description": "should support querying for a content child",
|
||||
"inputFiles": ["content_child.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"files": [{"generated": "content_child.js", "expected": "content_child.js"}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"onlyForTemplatePipeline": true,
|
||||
"description": "should support querying for multiple content children",
|
||||
"inputFiles": ["content_children.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"files": [{"generated": "content_children.js", "expected": "content_children.js"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
const _c0 = ["button"];
|
||||
// ...
|
||||
contentQueries:
|
||||
function SimpleContentChildWithName_ContentQueries(rf, ctx, dirIndex) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵcontentQueryCreate(ctx.buttonEl, dirIndex, _c0, 5);
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
contentQueries: function SimpleContentChildWithType_ContentQueries(rf, ctx, dirIndex) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵcontentQueryCreate(ctx.buttonComp, dirIndex, ButtonComp, 5);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import {Component, contentChild} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `{{buttonEl() !== undefined}}`,
|
||||
})
|
||||
export class SimpleContentChildWithName {
|
||||
buttonEl = contentChild('button');
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'button-comp', template: '', standalone: true})
|
||||
export class ButtonComp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `{{buttonComp() !== undefined}}`,
|
||||
})
|
||||
export class SimpleContentChildWithType {
|
||||
buttonComp = contentChild(ButtonComp);
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
const _c0 = ["button"];
|
||||
// ...
|
||||
contentQueries:
|
||||
function SimpleContentChildrenWithName_ContentQueries(rf, ctx, dirIndex) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵcontentQueryCreate(ctx.buttonEls, dirIndex, _c0, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
contentQueries: function SimpleContentChildrenWithType_ContentQueries(rf, ctx, dirIndex) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵcontentQueryCreate(ctx.buttonComps, dirIndex, ButtonComp, 4);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import {Component, contentChildren} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `{{buttonEls().length}}`,
|
||||
})
|
||||
export class SimpleContentChildrenWithName {
|
||||
buttonEls = contentChildren('button');
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'button-comp', template: '', standalone: true})
|
||||
export class ButtonComp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `{{buttonComps().length}}`,
|
||||
})
|
||||
export class SimpleContentChildrenWithType {
|
||||
buttonComps = contentChildren(ButtonComp);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
const _c0 = ["button"];
|
||||
// ...
|
||||
viewQuery: function SimpleViewChildWithName_Query(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵviewQueryCreate(ctx.buttonEl, _c0, 5);
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
viewQuery: function SimpleViewChildWithType_Query(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵviewQueryCreate(ctx.buttonEl, ButtonComp, 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import {Component, viewChild} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `<button #button></button>`,
|
||||
})
|
||||
export class SimpleViewChildWithName {
|
||||
buttonEl = viewChild('button');
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'button-comp', template: '', standalone: true})
|
||||
export class ButtonComp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
standalone: true,
|
||||
imports: [ButtonComp],
|
||||
template: `<button-comp></button-comp>`,
|
||||
})
|
||||
export class SimpleViewChildWithType {
|
||||
buttonEl = viewChild(ButtonComp);
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
const _c0 = ["buttons"];
|
||||
// ...
|
||||
viewQuery: function SimpleViewChildrenWithName_Query(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵviewQueryCreate(ctx.buttonEls, _c0, 5);
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
viewQuery: function SimpleViewChildrenWithType_Query(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵviewQueryCreate(ctx.buttonEls, ButtonComp, 5);
|
||||
}
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import {Component, viewChildren} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
template: `
|
||||
<button #buttons></button>
|
||||
<button #buttons></button>
|
||||
`,
|
||||
})
|
||||
export class SimpleViewChildrenWithName {
|
||||
buttonEls = viewChildren('buttons');
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'button-comp', template: '', standalone: true})
|
||||
export class ButtonComp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
signals: true,
|
||||
standalone: true,
|
||||
imports: [ButtonComp],
|
||||
template: `
|
||||
<button-comp></button-comp>
|
||||
<button-comp></button-comp>
|
||||
`,
|
||||
})
|
||||
export class SimpleViewChildrenWithType {
|
||||
buttonEls = viewChildren(ButtonComp);
|
||||
}
|
||||
@@ -2040,7 +2040,7 @@ function allTests(os: string) {
|
||||
}
|
||||
`);
|
||||
verifyThrownError(
|
||||
ErrorCode.DECORATOR_ARG_NOT_LITERAL,
|
||||
ErrorCode.QUERY_DEFINITION_ARG_NOT_LITERAL,
|
||||
`@${decorator} options must be an object literal`);
|
||||
});
|
||||
|
||||
@@ -2076,7 +2076,7 @@ function allTests(os: string) {
|
||||
}
|
||||
`);
|
||||
verifyThrownError(
|
||||
ErrorCode.DECORATOR_ARITY_WRONG, `@${decorator} has too many arguments`);
|
||||
ErrorCode.QUERY_DEFINITION_ARITY_WRONG, `@${decorator} has too many arguments`);
|
||||
});
|
||||
|
||||
it(`should throw error if @${decorator} predicate argument has wrong type`, () => {
|
||||
|
||||
@@ -436,6 +436,9 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
|
||||
case o.BinaryOperator.NullishCoalesce:
|
||||
opStr = '??';
|
||||
break;
|
||||
case o.BinaryOperator.Assign:
|
||||
opStr = '=';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown operator ${ast.operator}`);
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ export enum UnaryOperator {
|
||||
}
|
||||
|
||||
export enum BinaryOperator {
|
||||
Assign,
|
||||
Equals,
|
||||
NotEquals,
|
||||
Identical,
|
||||
|
||||
@@ -216,6 +216,7 @@ export class Identifiers {
|
||||
static projectionDef: o.ExternalReference = {name: 'ɵɵprojectionDef', moduleName: CORE};
|
||||
|
||||
static reference: o.ExternalReference = {name: 'ɵɵreference', moduleName: CORE};
|
||||
static shallowReference: o.ExternalReference = {name: 'ɵɵshallowReference', moduleName: CORE};
|
||||
|
||||
static inject: o.ExternalReference = {name: 'ɵɵinject', moduleName: CORE};
|
||||
|
||||
@@ -351,4 +352,16 @@ export class Identifiers {
|
||||
o.ExternalReference = {name: 'ɵɵtrustConstantResourceUrl', moduleName: CORE};
|
||||
static validateIframeAttribute:
|
||||
o.ExternalReference = {name: 'ɵɵvalidateIframeAttribute', moduleName: CORE};
|
||||
|
||||
// ## signal APIs
|
||||
|
||||
// for type-checking
|
||||
static getInputSignalWriteType = {name: 'ɵɵGetInputSignalWriteType', moduleName: CORE};
|
||||
|
||||
static propertyCreate: o.ExternalReference = {name: 'ɵɵpropertyCreate', moduleName: CORE};
|
||||
static stringifyInterpolation:
|
||||
o.ExternalReference = {name: 'ɵɵstringifyInterpolation', moduleName: CORE};
|
||||
|
||||
static viewQueryCreate: o.ExternalReference = {name: 'ɵɵviewQueryCreate', moduleName: CORE};
|
||||
static contentQueryCreate: o.ExternalReference = {name: 'ɵɵcontentQueryCreate', moduleName: CORE};
|
||||
}
|
||||
|
||||
@@ -56,12 +56,14 @@ function baseDirectiveFields(
|
||||
if (meta.queries.length > 0) {
|
||||
// e.g. `contentQueries: (rf, ctx, dirIndex) => { ... }
|
||||
definitionMap.set(
|
||||
'contentQueries', createContentQueriesFunction(meta.queries, constantPool, meta.name));
|
||||
'contentQueries',
|
||||
createContentQueriesFunction(meta.isSignal, meta.queries, constantPool, meta.name));
|
||||
}
|
||||
|
||||
if (meta.viewQueries.length) {
|
||||
definitionMap.set(
|
||||
'viewQuery', createViewQueriesFunction(meta.viewQueries, constantPool, meta.name));
|
||||
'viewQuery',
|
||||
createViewQueriesFunction(meta.isSignal, meta.viewQueries, constantPool, meta.name));
|
||||
}
|
||||
|
||||
// e.g. `hostBindings: (rf, ctx) => { ... }
|
||||
@@ -234,7 +236,8 @@ export function compileComponentFromMetadata(
|
||||
} else {
|
||||
// This path compiles the template using the prototype template pipeline. First the template is
|
||||
// ingested into IR:
|
||||
const tpl = ingest(meta.name, meta.template.nodes, constantPool);
|
||||
console.error('ingest', meta.name, meta.isSignal);
|
||||
const tpl = ingest(meta.name, meta.isSignal, meta.template.nodes, constantPool);
|
||||
|
||||
// Then the IR is transformed to prepare it for cod egeneration.
|
||||
transformTemplate(tpl);
|
||||
@@ -402,12 +405,25 @@ function convertAttributesToExpressions(attributes: {[name: string]: o.Expressio
|
||||
|
||||
// Define and update any content queries
|
||||
function createContentQueriesFunction(
|
||||
queries: R3QueryMetadata[], constantPool: ConstantPool, name?: string): o.Expression {
|
||||
isSignal: boolean, queries: R3QueryMetadata[], constantPool: ConstantPool,
|
||||
name?: string): o.Expression {
|
||||
const createStatements: o.Statement[] = [];
|
||||
const updateStatements: o.Statement[] = [];
|
||||
const tempAllocator = temporaryAllocator(updateStatements, TEMPORARY_NAME);
|
||||
|
||||
for (const query of queries) {
|
||||
if (isSignal) {
|
||||
createStatements.push(
|
||||
o.importExpr(R3.contentQueryCreate)
|
||||
.callFn([
|
||||
o.variable(CONTEXT_NAME).prop(query.propertyName),
|
||||
// TODO(signals): `dirIndex` is not actually used at all. Can remove?
|
||||
o.variable('dirIndex'), ...prepareQueryParams(query, constantPool)
|
||||
])
|
||||
.toStmt());
|
||||
continue;
|
||||
}
|
||||
|
||||
// creation, e.g. r3.contentQuery(dirIndex, somePredicate, true, null);
|
||||
createStatements.push(
|
||||
o.importExpr(R3.contentQuery)
|
||||
@@ -425,16 +441,21 @@ function createContentQueriesFunction(
|
||||
}
|
||||
|
||||
const contentQueriesFnName = name ? `${name}_ContentQueries` : null;
|
||||
const body = [
|
||||
renderFlagCheckIfStmt(core.RenderFlags.Create, createStatements),
|
||||
];
|
||||
|
||||
// Signal directives may not generate update statements.
|
||||
if (updateStatements.length > 0) {
|
||||
body.push(renderFlagCheckIfStmt(core.RenderFlags.Update, updateStatements));
|
||||
}
|
||||
|
||||
return o.fn(
|
||||
[
|
||||
new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, null),
|
||||
new o.FnParam('dirIndex', null)
|
||||
],
|
||||
[
|
||||
renderFlagCheckIfStmt(core.RenderFlags.Create, createStatements),
|
||||
renderFlagCheckIfStmt(core.RenderFlags.Update, updateStatements)
|
||||
],
|
||||
o.INFERRED_TYPE, null, contentQueriesFnName);
|
||||
body, o.INFERRED_TYPE, null, contentQueriesFnName);
|
||||
}
|
||||
|
||||
function stringAsType(str: string): o.Type {
|
||||
@@ -510,12 +531,23 @@ export function createDirectiveType(meta: R3DirectiveMetadata): o.Type {
|
||||
|
||||
// Define and update any view queries
|
||||
function createViewQueriesFunction(
|
||||
viewQueries: R3QueryMetadata[], constantPool: ConstantPool, name?: string): o.Expression {
|
||||
isSignal: boolean, viewQueries: R3QueryMetadata[], constantPool: ConstantPool,
|
||||
name?: string): o.Expression {
|
||||
const createStatements: o.Statement[] = [];
|
||||
const updateStatements: o.Statement[] = [];
|
||||
const tempAllocator = temporaryAllocator(updateStatements, TEMPORARY_NAME);
|
||||
|
||||
viewQueries.forEach((query: R3QueryMetadata) => {
|
||||
if (isSignal) {
|
||||
createStatements.push(o.importExpr(R3.viewQueryCreate)
|
||||
.callFn([
|
||||
o.variable(CONTEXT_NAME).prop(query.propertyName),
|
||||
...prepareQueryParams(query, constantPool)
|
||||
])
|
||||
.toStmt());
|
||||
return;
|
||||
}
|
||||
|
||||
// creation, e.g. r3.viewQuery(somePredicate, true);
|
||||
const queryDefinition =
|
||||
o.importExpr(R3.viewQuery).callFn(prepareQueryParams(query, constantPool));
|
||||
@@ -532,12 +564,17 @@ function createViewQueriesFunction(
|
||||
});
|
||||
|
||||
const viewQueryFnName = name ? `${name}_Query` : null;
|
||||
const body = [
|
||||
renderFlagCheckIfStmt(core.RenderFlags.Create, createStatements),
|
||||
];
|
||||
|
||||
// Signal directives may not generate update statements.
|
||||
if (updateStatements.length > 0) {
|
||||
body.push(renderFlagCheckIfStmt(core.RenderFlags.Update, updateStatements));
|
||||
}
|
||||
|
||||
return o.fn(
|
||||
[new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, null)],
|
||||
[
|
||||
renderFlagCheckIfStmt(core.RenderFlags.Create, createStatements),
|
||||
renderFlagCheckIfStmt(core.RenderFlags.Update, updateStatements)
|
||||
],
|
||||
[new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, null)], body,
|
||||
o.INFERRED_TYPE, null, viewQueryFnName);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,12 @@ export enum OpKind {
|
||||
/**
|
||||
* An operation to interpolate text into an attribute binding.
|
||||
*/
|
||||
|
||||
InterpolateAttribute,
|
||||
/*
|
||||
* TODO
|
||||
*/
|
||||
PropertyCreate,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,6 +163,11 @@ export enum ExpressionKind {
|
||||
*/
|
||||
Reference,
|
||||
|
||||
/**
|
||||
* Runtime operation to retrieve the value of a shallow local reference.
|
||||
*/
|
||||
ShallowReference,
|
||||
|
||||
/**
|
||||
* Runtime operation to snapshot the current view context.
|
||||
*/
|
||||
@@ -226,7 +236,13 @@ export enum ExpressionKind {
|
||||
/**
|
||||
* A reference to a temporary variable.
|
||||
*/
|
||||
|
||||
ReadTemporaryExpr,
|
||||
/**
|
||||
* An interpolation template string expression. Such template string expression is
|
||||
* commonly expected to be part of the `PropertyCreate` operation expression.
|
||||
*/
|
||||
InterpolationTemplateExpr,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,19 +10,19 @@ import * as o from '../../../../output/output_ast';
|
||||
import type {ParseSourceSpan} from '../../../../parse_util';
|
||||
|
||||
import {ExpressionKind, OpKind} from './enums';
|
||||
import {ConsumesVarsTrait, UsesSlotIndex, UsesSlotIndexTrait, UsesVarOffset, UsesVarOffsetTrait} from './traits';
|
||||
|
||||
import type {XrefId} from './operations';
|
||||
import type {CreateOp} from './ops/create';
|
||||
import type {UpdateOp} from './ops/update';
|
||||
import {ConsumesVarsTrait, UsesSlotIndex, UsesSlotIndexTrait, UsesVarOffset, UsesVarOffsetTrait} from './traits';
|
||||
|
||||
/**
|
||||
* An `o.Expression` subtype representing a logical expression in the intermediate representation.
|
||||
*/
|
||||
export type Expression = LexicalReadExpr|ReferenceExpr|ContextExpr|NextContextExpr|
|
||||
GetCurrentViewExpr|RestoreViewExpr|ResetViewExpr|ReadVariableExpr|PureFunctionExpr|
|
||||
PureFunctionParameterExpr|PipeBindingExpr|PipeBindingVariadicExpr|SafePropertyReadExpr|
|
||||
SafeKeyedReadExpr|SafeInvokeFunctionExpr|EmptyExpr|AssignTemporaryExpr|ReadTemporaryExpr;
|
||||
export type Expression = LexicalReadExpr|ReferenceExpr|ShallowReferenceExpr|ContextExpr|
|
||||
NextContextExpr|GetCurrentViewExpr|RestoreViewExpr|ResetViewExpr|ReadVariableExpr|
|
||||
PureFunctionExpr|PureFunctionParameterExpr|PipeBindingExpr|PipeBindingVariadicExpr|
|
||||
SafePropertyReadExpr|SafeKeyedReadExpr|SafeInvokeFunctionExpr|EmptyExpr|AssignTemporaryExpr|
|
||||
ReadTemporaryExpr|InterpolationTemplateExpr;
|
||||
|
||||
/**
|
||||
* Transformer type which converts expressions into general `o.Expression`s (which may be an
|
||||
@@ -115,6 +115,40 @@ export class ReferenceExpr extends ExpressionBase implements UsesSlotIndexTrait
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Runtime operation to retrieve the value of a local reference.
|
||||
*/
|
||||
export class ShallowReferenceExpr extends ExpressionBase implements UsesSlotIndexTrait {
|
||||
override readonly kind = ExpressionKind.ShallowReference;
|
||||
|
||||
readonly[UsesSlotIndex] = true;
|
||||
|
||||
slot: number|null = null;
|
||||
|
||||
constructor(readonly target: XrefId, readonly offset: number) {
|
||||
super();
|
||||
}
|
||||
|
||||
override visitExpression(): void {}
|
||||
|
||||
override isEquivalent(e: o.Expression): boolean {
|
||||
return e instanceof ShallowReferenceExpr && e.target === this.target;
|
||||
}
|
||||
|
||||
override isConstant(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
override transformInternalExpressions(): void {}
|
||||
|
||||
override clone(): ShallowReferenceExpr {
|
||||
const expr = new ShallowReferenceExpr(this.target, this.offset);
|
||||
expr.slot = this.slot;
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference to the current view context (usually the `ctx` variable in a template function).
|
||||
*/
|
||||
@@ -627,6 +661,47 @@ export class SafeTernaryExpr extends ExpressionBase {
|
||||
}
|
||||
}
|
||||
|
||||
export class InterpolationTemplateExpr extends ExpressionBase {
|
||||
override readonly kind = ExpressionKind.InterpolationTemplateExpr;
|
||||
|
||||
constructor(public staticParts: string[], public expressionParts: o.Expression[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
override visitExpression() {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
override isEquivalent(e: o.Expression): boolean {
|
||||
if (!(e instanceof InterpolationTemplateExpr)) {
|
||||
return false;
|
||||
}
|
||||
if (e.staticParts.length !== this.staticParts.length) {
|
||||
return false;
|
||||
}
|
||||
if (e.expressionParts.length !== this.expressionParts.length) {
|
||||
return false;
|
||||
}
|
||||
return e.staticParts.every((p, i) => p === this.staticParts[i]) &&
|
||||
e.expressionParts.every((e, i) => e === this.expressionParts[i]);
|
||||
}
|
||||
|
||||
override isConstant(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
override transformInternalExpressions(transform: ExpressionTransform, flags: VisitorContextFlag):
|
||||
void {
|
||||
this.expressionParts =
|
||||
this.expressionParts.map(p => transformExpressionsInExpression(p, transform, flags));
|
||||
}
|
||||
|
||||
override clone(): InterpolationTemplateExpr {
|
||||
return new InterpolationTemplateExpr(
|
||||
[...this.staticParts], this.expressionParts.map(p => p.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
export class EmptyExpr extends ExpressionBase {
|
||||
override readonly kind = ExpressionKind.EmptyExpr;
|
||||
|
||||
@@ -768,6 +843,9 @@ export function transformExpressionsInOp(
|
||||
transformExpressionsInOp(innerOp, transform, flags | VisitorContextFlag.InChildOperation);
|
||||
}
|
||||
break;
|
||||
case OpKind.PropertyCreate:
|
||||
op.expression = transformExpressionsInExpression(op.expression, transform, flags);
|
||||
break;
|
||||
case OpKind.Element:
|
||||
case OpKind.ElementStart:
|
||||
case OpKind.ElementEnd:
|
||||
|
||||
@@ -279,9 +279,6 @@ export class OpList<OpT extends Op<OpT>> {
|
||||
|
||||
op.debugListId = before.debugListId;
|
||||
|
||||
// Just in case.
|
||||
op.prev = null;
|
||||
|
||||
before.prev!.next = op;
|
||||
op.prev = before.prev;
|
||||
|
||||
@@ -289,6 +286,27 @@ export class OpList<OpT extends Op<OpT>> {
|
||||
before.prev = op;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert `op` after `after`.
|
||||
*/
|
||||
static insertAfter<OpT extends Op<OpT>>(op: OpT, after: OpT): void {
|
||||
OpList.assertIsOwned(after);
|
||||
if (after.next === null) {
|
||||
throw new Error(`AssertionError: illegal operation on list end`);
|
||||
}
|
||||
|
||||
OpList.assertIsNotEnd(op);
|
||||
OpList.assertIsUnowned(op);
|
||||
|
||||
op.debugListId = after.debugListId;
|
||||
|
||||
after.next!.prev = op;
|
||||
op.next = after.next;
|
||||
|
||||
op.prev = after;
|
||||
after.next = op;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `op` does not currently belong to a list.
|
||||
*/
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as o from '../../../../../output/output_ast';
|
||||
import {ElementAttributes} from '../element';
|
||||
import {OpKind} from '../enums';
|
||||
import {Op, OpList, XrefId} from '../operations';
|
||||
import {ConsumesSlotOpTrait, TRAIT_CONSUMES_SLOT, TRAIT_USES_SLOT_INDEX, UsesSlotIndexTrait} from '../traits';
|
||||
import {ConsumesSlotOpTrait, ConsumesVarsTrait, TRAIT_CONSUMES_SLOT, TRAIT_CONSUMES_VARS, TRAIT_USES_SLOT_INDEX, UsesSlotIndexTrait} from '../traits';
|
||||
|
||||
import {ListEndOp, NEW_OP, StatementOp, VariableOp} from './shared';
|
||||
|
||||
@@ -18,9 +19,9 @@ import type {UpdateOp} from './update';
|
||||
/**
|
||||
* An operation usable on the creation side of the IR.
|
||||
*/
|
||||
export type CreateOp =
|
||||
ListEndOp<CreateOp>|StatementOp<CreateOp>|ElementOp|ElementStartOp|ElementEndOp|ContainerOp|
|
||||
ContainerStartOp|ContainerEndOp|TemplateOp|TextOp|ListenerOp|PipeOp|VariableOp<CreateOp>;
|
||||
export type CreateOp = ListEndOp<CreateOp>|StatementOp<CreateOp>|ElementOp|ElementStartOp|
|
||||
ElementEndOp|ContainerOp|ContainerStartOp|ContainerEndOp|TemplateOp|TextOp|ListenerOp|PipeOp|
|
||||
VariableOp<CreateOp>|PropertyCreateOp;
|
||||
|
||||
/**
|
||||
* An operation representing the creation of an element or container.
|
||||
@@ -309,6 +310,45 @@ export function createPipeOp(xref: XrefId, name: string): PipeOp {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
export interface PropertyCreateOp extends Op<CreateOp>, ConsumesSlotOpTrait, ConsumesVarsTrait {
|
||||
kind: OpKind.PropertyCreate;
|
||||
|
||||
/**
|
||||
* Reference to the element on which the property is bound.
|
||||
*/
|
||||
target: XrefId;
|
||||
|
||||
/**
|
||||
* Name of the bound property.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Expression which is bound to the property.
|
||||
*/
|
||||
expression: o.Expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `PropertyCreateOp`.
|
||||
*/
|
||||
export function createPropertyCreateOp(
|
||||
xref: XrefId, target: XrefId, name: string, expression: o.Expression): PropertyCreateOp {
|
||||
return {
|
||||
kind: OpKind.PropertyCreate,
|
||||
xref,
|
||||
target,
|
||||
name,
|
||||
expression,
|
||||
...TRAIT_CONSUMES_SLOT,
|
||||
...TRAIT_CONSUMES_VARS,
|
||||
...NEW_OP,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An index into the `consts` array which is shared across the compilation of all views in a
|
||||
* component.
|
||||
|
||||
@@ -68,18 +68,25 @@ export interface VariableOp<OpT extends Op<OpT>> extends Op<OpT> {
|
||||
* Expression representing the value of the variable.
|
||||
*/
|
||||
initializer: o.Expression;
|
||||
|
||||
/**
|
||||
* Whether the variable created is a constant.
|
||||
*/
|
||||
isConstant: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `VariableOp`.
|
||||
*/
|
||||
export function createVariableOp<OpT extends Op<OpT>>(
|
||||
xref: XrefId, variable: SemanticVariable, initializer: o.Expression): VariableOp<OpT> {
|
||||
xref: XrefId, variable: SemanticVariable, initializer: o.Expression,
|
||||
isConstant: boolean): VariableOp<OpT> {
|
||||
return {
|
||||
kind: OpKind.Variable,
|
||||
xref,
|
||||
variable,
|
||||
initializer,
|
||||
isConstant,
|
||||
...NEW_OP,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ export interface IdentifierVariable extends SemanticVariableBase {
|
||||
* The identifier whose value in the template is tracked in this variable.
|
||||
*/
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* The target in the template associated with this identifier (or `null` if no such target
|
||||
* exists).
|
||||
*/
|
||||
target: XrefId|null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,7 +37,8 @@ export class ComponentCompilation {
|
||||
*/
|
||||
readonly root: ViewCompilation;
|
||||
|
||||
constructor(readonly componentName: string, readonly pool: ConstantPool) {
|
||||
constructor(
|
||||
readonly componentName: string, readonly isSignal: boolean, readonly pool: ConstantPool) {
|
||||
// Allocate the root view.
|
||||
const root = new ViewCompilation(this, this.allocateXrefId(), null);
|
||||
this.views.set(root.xref, root);
|
||||
|
||||
@@ -11,19 +11,20 @@ import {ConstantPool} from '../../../constant_pool';
|
||||
import * as ir from '../ir';
|
||||
|
||||
import type {ComponentCompilation, ViewCompilation} from './compilation';
|
||||
|
||||
import {phaseAlignPipeVariadicVarOffset} from './phases/align_pipe_variadic_var_offset';
|
||||
import {phaseAttributeExtraction} from './phases/attribute_extraction';
|
||||
import {phaseChaining} from './phases/chaining';
|
||||
import {phaseConstCollection} from './phases/const_collection';
|
||||
import {phaseCreationVarColocation} from './phases/creation_var_colocation';
|
||||
import {phaseEmptyElements} from './phases/empty_elements';
|
||||
import {phaseExpandSafeReads} from './phases/expand_safe_reads';
|
||||
import {phaseGenerateAdvance} from './phases/generate_advance';
|
||||
import {phaseNullishCoalescing} from './phases/nullish_coalescing';
|
||||
import {phaseGenerateVariables} from './phases/generate_variables';
|
||||
import {phaseLocalRefs} from './phases/local_refs';
|
||||
import {phaseNaming} from './phases/naming';
|
||||
import {phaseMergeNextContext} from './phases/next_context_merging';
|
||||
import {phaseNgContainer} from './phases/ng_container';
|
||||
import {phaseNullishCoalescing} from './phases/nullish_coalescing';
|
||||
import {phasePipeCreation} from './phases/pipe_creation';
|
||||
import {phasePipeVariadic} from './phases/pipe_variadic';
|
||||
import {phasePureFunctionExtraction} from './phases/pure_function_extraction';
|
||||
@@ -33,10 +34,9 @@ import {phaseResolveContexts} from './phases/resolve_contexts';
|
||||
import {phaseResolveNames} from './phases/resolve_names';
|
||||
import {phaseSaveRestoreView} from './phases/save_restore_view';
|
||||
import {phaseSlotAllocation} from './phases/slot_allocation';
|
||||
import {phaseTemporaryVariables} from './phases/temporary_variables';
|
||||
import {phaseVarCounting} from './phases/var_counting';
|
||||
import {phaseVariableOptimization} from './phases/variable_optimization';
|
||||
import {phaseExpandSafeReads} from './phases/expand_safe_reads';
|
||||
import {phaseTemporaryVariables} from './phases/temporary_variables';
|
||||
|
||||
/**
|
||||
* Run all transformation phases in the correct order against a `ComponentCompilation`. After this
|
||||
@@ -61,6 +61,7 @@ export function transformTemplate(cpl: ComponentCompilation): void {
|
||||
phaseGenerateAdvance(cpl);
|
||||
phaseNaming(cpl);
|
||||
phaseVariableOptimization(cpl, {conservative: true});
|
||||
phaseCreationVarColocation(cpl);
|
||||
phaseMergeNextContext(cpl);
|
||||
phaseNgContainer(cpl);
|
||||
phaseEmptyElements(cpl);
|
||||
|
||||
@@ -20,8 +20,9 @@ import {BINARY_OPERATORS} from './conversion';
|
||||
* representation.
|
||||
*/
|
||||
export function ingest(
|
||||
componentName: string, template: t.Node[], constantPool: ConstantPool): ComponentCompilation {
|
||||
const cpl = new ComponentCompilation(componentName, constantPool);
|
||||
componentName: string, isSignal: boolean, template: t.Node[],
|
||||
constantPool: ConstantPool): ComponentCompilation {
|
||||
const cpl = new ComponentCompilation(componentName, isSignal, constantPool);
|
||||
ingestNodes(cpl.root, template);
|
||||
return cpl;
|
||||
}
|
||||
@@ -218,6 +219,9 @@ function ingestBindings(
|
||||
op.xref, ir.ElementAttributeKind.Attribute, attr.name, o.literal(attr.value)));
|
||||
}
|
||||
|
||||
// Style bindings are ingested before non-style inputs.
|
||||
ingestStylingBindings(view, op.xref, ir.ElementAttributeKind.Binding, element.inputs);
|
||||
|
||||
for (const input of element.inputs) {
|
||||
ingestPropertyBinding(view, op.xref, ir.ElementAttributeKind.Binding, input);
|
||||
}
|
||||
@@ -254,36 +258,105 @@ function ingestBindings(
|
||||
}
|
||||
}
|
||||
|
||||
function ingestStylingBindings(
|
||||
view: ViewCompilation, xref: ir.XrefId,
|
||||
bindingKind: ir.ElementAttributeKind.Binding|ir.ElementAttributeKind.Template,
|
||||
bindings: t.BoundAttribute[]): void {
|
||||
const mapOpsInOrder: ir.UpdateOp[] = [];
|
||||
const singleOpsInOrder: ir.UpdateOp[] = [];
|
||||
|
||||
for (let {value, type, name, unit} of bindings) {
|
||||
if (value instanceof e.ASTWithSource) {
|
||||
value = value.ast;
|
||||
}
|
||||
|
||||
if (value instanceof e.Interpolation) {
|
||||
switch (type) {
|
||||
case e.BindingType.Property:
|
||||
if (name === 'style') {
|
||||
if (bindingKind !== ir.ElementAttributeKind.Binding) {
|
||||
throw Error('Unexpected style binding on ng-template');
|
||||
}
|
||||
mapOpsInOrder.push(ir.createInterpolateStyleMapOp(
|
||||
xref, value.strings, value.expressions.map(expr => convertAst(expr, view.tpl))));
|
||||
}
|
||||
break;
|
||||
case e.BindingType.Style:
|
||||
if (bindingKind !== ir.ElementAttributeKind.Binding) {
|
||||
throw Error('Unexpected style binding on ng-template');
|
||||
}
|
||||
singleOpsInOrder.push(ir.createInterpolateStylePropOp(
|
||||
xref, name, value.strings, value.expressions.map(expr => convertAst(expr, view.tpl)),
|
||||
unit));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (type) {
|
||||
case e.BindingType.Property:
|
||||
if (name === 'style') {
|
||||
if (bindingKind !== ir.ElementAttributeKind.Binding) {
|
||||
throw Error('Unexpected style binding on ng-template');
|
||||
}
|
||||
mapOpsInOrder.push(ir.createStyleMapOp(xref, convertAst(value, view.tpl)));
|
||||
}
|
||||
break;
|
||||
case e.BindingType.Style:
|
||||
if (bindingKind !== ir.ElementAttributeKind.Binding) {
|
||||
throw Error('Unexpected style binding on ng-template');
|
||||
}
|
||||
singleOpsInOrder.push(
|
||||
ir.createStylePropOp(xref, name, convertAst(value, view.tpl), unit));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Order is more nuanced in TDB. Ensure we generate the same output here.
|
||||
// Right now this is the initial foundation to preserve proper styling order.
|
||||
// Can be replaced with: https://github.com/angular/angular/pull/50805.
|
||||
|
||||
// Single bindings (e.g. `[style.X]`) are of higher priority than
|
||||
// map bindings (e.g. `[style]`).
|
||||
mapOpsInOrder.forEach(op => view.update.push(op));
|
||||
singleOpsInOrder.forEach(op => view.update.push(op));
|
||||
}
|
||||
|
||||
function ingestPropertyBinding(
|
||||
view: ViewCompilation, xref: ir.XrefId,
|
||||
bindingKind: ir.ElementAttributeKind.Binding|ir.ElementAttributeKind.Template,
|
||||
{name, value, type, unit}: t.BoundAttribute): void {
|
||||
{name, value, type}: t.BoundAttribute): void {
|
||||
if (value instanceof e.ASTWithSource) {
|
||||
value = value.ast;
|
||||
}
|
||||
|
||||
if (value instanceof e.Interpolation) {
|
||||
switch (type) {
|
||||
case e.BindingType.Style:
|
||||
// `[style.x]` bindings are handled separately in `ingestStylingBindings`.
|
||||
break;
|
||||
case e.BindingType.Property:
|
||||
if (name === 'style') {
|
||||
if (bindingKind !== ir.ElementAttributeKind.Binding) {
|
||||
throw Error('Unexpected style binding on ng-template');
|
||||
}
|
||||
view.update.push(ir.createInterpolateStyleMapOp(
|
||||
xref, value.strings, value.expressions.map(expr => convertAst(expr, view.tpl))));
|
||||
} else {
|
||||
view.update.push(ir.createInterpolatePropertyOp(
|
||||
xref, bindingKind, name, value.strings,
|
||||
value.expressions.map(expr => convertAst(expr, view.tpl))));
|
||||
// `[style]` bindings are handled separately in `ingestStylingBindings`.
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case e.BindingType.Style:
|
||||
if (bindingKind !== ir.ElementAttributeKind.Binding) {
|
||||
throw Error('Unexpected style binding on ng-template');
|
||||
|
||||
if (view.tpl.isSignal) {
|
||||
// Allocating an XRef ID because a property create operation
|
||||
// consumes a slot to store the expression.
|
||||
const propertyXrefId = view.tpl.allocateXrefId();
|
||||
|
||||
view.create.push(ir.createPropertyCreateOp(
|
||||
propertyXrefId, xref, name,
|
||||
new ir.InterpolationTemplateExpr(
|
||||
value.strings, value.expressions.map(e => convertAst(e, view.tpl)))));
|
||||
|
||||
break;
|
||||
}
|
||||
view.update.push(ir.createInterpolateStylePropOp(
|
||||
xref, name, value.strings, value.expressions.map(expr => convertAst(expr, view.tpl)),
|
||||
unit));
|
||||
|
||||
view.update.push(ir.createInterpolatePropertyOp(
|
||||
xref, bindingKind, name, value.strings,
|
||||
value.expressions.map(expr => convertAst(expr, view.tpl))));
|
||||
|
||||
break;
|
||||
case e.BindingType.Attribute:
|
||||
if (bindingKind !== ir.ElementAttributeKind.Binding) {
|
||||
@@ -300,23 +373,27 @@ function ingestPropertyBinding(
|
||||
}
|
||||
} else {
|
||||
switch (type) {
|
||||
case e.BindingType.Property:
|
||||
// Bindings to [style] are mapped to their own special instruction.
|
||||
if (name === 'style') {
|
||||
if (bindingKind !== ir.ElementAttributeKind.Binding) {
|
||||
throw Error('Unexpected style binding on ng-template');
|
||||
}
|
||||
view.update.push(ir.createStyleMapOp(xref, convertAst(value, view.tpl)));
|
||||
} else {
|
||||
view.update.push(
|
||||
ir.createPropertyOp(xref, bindingKind, name, convertAst(value, view.tpl)));
|
||||
}
|
||||
break;
|
||||
case e.BindingType.Style:
|
||||
if (bindingKind !== ir.ElementAttributeKind.Binding) {
|
||||
throw Error('Unexpected style binding on ng-template');
|
||||
// `[style.x]` bindings are handled separately in `ingestStylingBindings`.
|
||||
break;
|
||||
case e.BindingType.Property:
|
||||
if (name === 'style') {
|
||||
// `[style]` bindings are handled separately in `ingestStylingBindings`.
|
||||
break;
|
||||
}
|
||||
view.update.push(ir.createStylePropOp(xref, name, convertAst(value, view.tpl), unit));
|
||||
|
||||
if (view.tpl.isSignal) {
|
||||
console.error('Signal');
|
||||
// TODO: binding kind?
|
||||
// Allocating an XRef ID because a property create operation
|
||||
// consumes a slot to store the expression.
|
||||
const propertyXrefId = view.tpl.allocateXrefId();
|
||||
view.create.push(
|
||||
ir.createPropertyCreateOp(propertyXrefId, xref, name, convertAst(value, view.tpl)));
|
||||
break;
|
||||
}
|
||||
|
||||
view.update.push(ir.createPropertyOp(xref, bindingKind, name, convertAst(value, view.tpl)));
|
||||
break;
|
||||
case e.BindingType.Attribute:
|
||||
if (bindingKind !== ir.ElementAttributeKind.Binding) {
|
||||
|
||||
@@ -102,6 +102,12 @@ export function reference(slot: number): o.Expression {
|
||||
]);
|
||||
}
|
||||
|
||||
export function shallowReference(slot: number): o.Expression {
|
||||
return o.importExpr(Identifiers.shallowReference).callFn([
|
||||
o.literal(slot),
|
||||
]);
|
||||
}
|
||||
|
||||
export function nextContext(steps: number): o.Expression {
|
||||
return o.importExpr(Identifiers.nextContext).callFn(steps === 1 ? [] : [o.literal(steps)]);
|
||||
}
|
||||
@@ -156,6 +162,14 @@ export function styleMap(expression: o.Expression): ir.UpdateOp {
|
||||
return call(Identifiers.styleMap, [expression]);
|
||||
}
|
||||
|
||||
export function propertyCreate(slot: number, name: string, expression: o.Expression): ir.CreateOp {
|
||||
return call(Identifiers.propertyCreate, [
|
||||
o.literal(slot),
|
||||
o.literal(name),
|
||||
o.fn([], [new o.ReturnStatement(expression)]),
|
||||
]);
|
||||
}
|
||||
|
||||
const PIPE_BINDINGS: o.ExternalReference[] = [
|
||||
Identifiers.pipeBind1,
|
||||
Identifiers.pipeBind2,
|
||||
@@ -251,6 +265,13 @@ export function pureFunction(
|
||||
);
|
||||
}
|
||||
|
||||
export function stringifyInterpolation(
|
||||
staticParts: string[], expressions: o.Expression[]): o.Expression {
|
||||
return o.taggedTemplate(
|
||||
o.importExpr(Identifiers.stringifyInterpolation),
|
||||
new o.TemplateLiteral(staticParts.map(p => new o.TemplateLiteralElement(p)), expressions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Collates the string an expression arguments for an interpolation instruction.
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,7 @@ const CHAINABLE = new Set([
|
||||
R3.elementStart,
|
||||
R3.elementEnd,
|
||||
R3.property,
|
||||
R3.propertyCreate,
|
||||
R3.styleProp,
|
||||
R3.attribute,
|
||||
R3.elementContainerStart,
|
||||
@@ -101,7 +102,8 @@ interface Chain {
|
||||
* The expression representing the whole current chained call.
|
||||
*
|
||||
* This should be the same as `op.statement.expression`, but is extracted here for convenience
|
||||
* since the `op` type doesn't capture the fact that `op.statement` is an `o.ExpressionStatement`.
|
||||
* since the `op` type doesn't capture the fact that `op.statement` is an
|
||||
* `o.ExpressionStatement`.
|
||||
*/
|
||||
expression: o.Expression;
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @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 * as o from '../../../../output/output_ast';
|
||||
import * as ir from '../../ir';
|
||||
import type {ComponentCompilation, ViewCompilation} from '../compilation';
|
||||
|
||||
/**
|
||||
* Moves variables defined in creation mode to only be initialized after their creation, splitting
|
||||
* declaration and initialization if necessary to allow forward references.
|
||||
*/
|
||||
export function phaseCreationVarColocation(cpl: ComponentCompilation): void {
|
||||
for (const view of cpl.views.values()) {
|
||||
processView(view);
|
||||
}
|
||||
}
|
||||
|
||||
function processView(view: ViewCompilation): void {
|
||||
const shallowDeclarations = new Map<ir.XrefId, ir.VariableOp<ir.CreateOp>[]>();
|
||||
const seenVariableReads = new Set<ir.XrefId>;
|
||||
let shallow = true;
|
||||
for (const op of view.create) {
|
||||
if (shallow && op.kind === ir.OpKind.Variable &&
|
||||
op.variable.kind === ir.SemanticVariableKind.Identifier && op.variable.target !== null) {
|
||||
// This variable represents the identity of an entity within the current view (shallow) which
|
||||
// may or may not have been created yet.
|
||||
const target = op.variable.target;
|
||||
if (!shallowDeclarations.has(target)) {
|
||||
shallowDeclarations.set(target, []);
|
||||
}
|
||||
shallowDeclarations.get(target)!.push(op);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Scan expressions in this operation for both `NextContext` operations (which affect whether
|
||||
// seen reference declarations are shallow or not) or for `ReferenceExpr`s which may represent
|
||||
// forward references that later need to be accounted for.
|
||||
ir.visitExpressionsInOp(op, (exp, flags) => {
|
||||
if (flags & ir.VisitorContextFlag.InChildOperation) {
|
||||
return;
|
||||
}
|
||||
if (exp instanceof ir.NextContextExpr) {
|
||||
shallow = false;
|
||||
} else if (exp instanceof ir.ReadVariableExpr) {
|
||||
seenVariableReads.add(exp.xref);
|
||||
}
|
||||
});
|
||||
|
||||
if (ir.hasConsumesSlotTrait(op) && shallowDeclarations.has(op.xref)) {
|
||||
// `op` is creating an entity for which at least one shallow declaration has previously been
|
||||
// established. If nothing has referenced those declarations, it can be moved to follow `op`.
|
||||
// Otherwise, we can split the variable into its declaration and assignment.
|
||||
for (const declOp of shallowDeclarations.get(op.xref)!) {
|
||||
// Within the variable initializer, convert the `ReferenceExpr` into a
|
||||
// `ShallowReferenceExpr`. This is necessary since we might be moving the initializer past a
|
||||
// `NextContext` call.
|
||||
declOp.initializer = ir.transformExpressionsInExpression(declOp.initializer, expr => {
|
||||
if (!(expr instanceof ir.ReferenceExpr)) {
|
||||
return expr;
|
||||
}
|
||||
|
||||
const shallowExpr = new ir.ShallowReferenceExpr(expr.target, expr.offset);
|
||||
shallowExpr.slot = expr.slot;
|
||||
return shallowExpr;
|
||||
}, ir.VisitorContextFlag.None);
|
||||
|
||||
if (!seenVariableReads.has(declOp.xref)) {
|
||||
// No references have been recorded to this variable, so move it to follow this
|
||||
// declaration.
|
||||
ir.OpList.remove<ir.CreateOp>(declOp);
|
||||
ir.OpList.insertAfter<ir.CreateOp>(declOp, op);
|
||||
} else {
|
||||
// A forward reference has been observed, so leave the existing declaration in place as an
|
||||
// initializer to `undefined`, and set the variable to its value after its declaration.
|
||||
const initializer = declOp.initializer;
|
||||
declOp.initializer = o.literal(undefined);
|
||||
declOp.isConstant = false;
|
||||
|
||||
const readVar = new ir.ReadVariableExpr(declOp.xref);
|
||||
// TODO: variable naming should run after this and take care of this for us.
|
||||
readVar.name = declOp.variable.name;
|
||||
const assignment =
|
||||
new o.BinaryOperatorExpr(o.BinaryOperator.Assign, readVar, initializer);
|
||||
ir.OpList.insertAfter<ir.CreateOp>(ir.createStatementOp(assignment.toStmt()), op);
|
||||
}
|
||||
}
|
||||
shallowDeclarations.delete(op.xref);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
import * as o from '../../../../output/output_ast';
|
||||
import * as ir from '../../ir';
|
||||
|
||||
import type {ComponentCompilation, ViewCompilation} from '../compilation';
|
||||
|
||||
/**
|
||||
@@ -53,9 +52,9 @@ function recursivelyProcessView(view: ViewCompilation, parentScope: Scope|null):
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend the declarations for all available variables in scope to the `update` block.
|
||||
const preambleOps = generateVariablesInScopeForView(view, scope);
|
||||
view.update.prepend(preambleOps);
|
||||
// Prepend the declarations for all available variables in scope to both blocks.
|
||||
view.create.prepend(generateVariablesInScopeForView(view, scope));
|
||||
view.update.prepend(generateVariablesInScopeForView(view, scope));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,6 +130,7 @@ function getScopeForView(view: ViewCompilation, parent: Scope|null): Scope {
|
||||
kind: ir.SemanticVariableKind.Identifier,
|
||||
name: null,
|
||||
identifier,
|
||||
target: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@ function getScopeForView(view: ViewCompilation, parent: Scope|null): Scope {
|
||||
kind: ir.SemanticVariableKind.Identifier,
|
||||
name: null,
|
||||
identifier: op.localRefs[offset].name,
|
||||
target: op.xref,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -169,34 +170,36 @@ function getScopeForView(view: ViewCompilation, 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(
|
||||
view: ViewCompilation, scope: Scope): ir.VariableOp<ir.UpdateOp>[] {
|
||||
const newOps: ir.VariableOp<ir.UpdateOp>[] = [];
|
||||
function generateVariablesInScopeForView<OpT extends ir.Op<OpT>>(
|
||||
view: ViewCompilation, scope: Scope): 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
|
||||
// view with a `nextContext` expression. This context switching operation itself declares a
|
||||
// variable, because the context of the view may be referenced directly.
|
||||
newOps.push(ir.createVariableOp(
|
||||
view.tpl.allocateXrefId(), scope.viewContextVariable, new ir.NextContextExpr()));
|
||||
view.tpl.allocateXrefId(), scope.viewContextVariable, new ir.NextContextExpr(),
|
||||
/* isConstant */ true));
|
||||
}
|
||||
|
||||
// Add variables for all context variables available in this scope's view.
|
||||
for (const [name, value] of view.tpl.views.get(scope.view)!.contextVariables) {
|
||||
newOps.push(ir.createVariableOp(
|
||||
view.tpl.allocateXrefId(), scope.contextVariables.get(name)!,
|
||||
new o.ReadPropExpr(new ir.ContextExpr(scope.view), value)));
|
||||
new o.ReadPropExpr(new ir.ContextExpr(scope.view), value), /* isConstant */ true));
|
||||
}
|
||||
|
||||
// Add variables for all local references declared for elements in this scope.
|
||||
for (const ref of scope.references) {
|
||||
newOps.push(ir.createVariableOp(
|
||||
view.tpl.allocateXrefId(), ref.variable, new ir.ReferenceExpr(ref.targetId, ref.offset)));
|
||||
view.tpl.allocateXrefId(), ref.variable, new ir.ReferenceExpr(ref.targetId, ref.offset),
|
||||
/* isConstant */ true));
|
||||
}
|
||||
|
||||
if (scope.parent !== null) {
|
||||
// Recursively add variables from the parent scope.
|
||||
newOps.push(...generateVariablesInScopeForView(view, scope.parent));
|
||||
newOps.push(...generateVariablesInScopeForView<OpT>(view, scope.parent));
|
||||
}
|
||||
return newOps;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {sanitizeIdentifier} from '../../../../parse_util';
|
||||
import * as ir from '../../ir';
|
||||
|
||||
import type {ComponentCompilation, ViewCompilation} from '../compilation';
|
||||
|
||||
/**
|
||||
@@ -22,7 +22,7 @@ export function phaseNaming(cpl: ComponentCompilation): void {
|
||||
|
||||
function addNamesToView(view: ViewCompilation, baseName: string, state: {index: number}): void {
|
||||
if (view.fnName === null) {
|
||||
view.fnName = `${baseName}_Template`;
|
||||
view.fnName = sanitizeIdentifier(`${baseName}_Template`);
|
||||
}
|
||||
|
||||
// Keep track of the names we assign to variables in the view. We'll need to propagate these
|
||||
@@ -38,7 +38,8 @@ function addNamesToView(view: ViewCompilation, baseName: string, state: {index:
|
||||
if (op.slot === null) {
|
||||
throw new Error(`Expected a slot to be assigned`);
|
||||
}
|
||||
op.handlerFnName = `${view.fnName}_${op.tag}_${op.name}_${op.slot}_listener`;
|
||||
op.handlerFnName =
|
||||
sanitizeIdentifier(`${view.fnName}_${op.tag}_${op.name}_${op.slot}_listener`);
|
||||
}
|
||||
break;
|
||||
case ir.OpKind.Variable:
|
||||
@@ -49,9 +50,7 @@ function addNamesToView(view: ViewCompilation, baseName: string, state: {index:
|
||||
if (op.slot === null) {
|
||||
throw new Error(`Expected slot to be assigned`);
|
||||
}
|
||||
// TODO: properly escape the tag name.
|
||||
const safeTagName = op.tag.replace('-', '_');
|
||||
addNamesToView(childView, `${baseName}_${safeTagName}_${op.slot}`, state);
|
||||
addNamesToView(childView, `${baseName}_${op.tag}_${op.slot}`, state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
import * as o from '../../../../output/output_ast';
|
||||
import * as ir from '../../ir';
|
||||
|
||||
import type {ComponentCompilation, ViewCompilation} from '../compilation';
|
||||
import * as ng from '../instruction';
|
||||
|
||||
@@ -95,10 +94,20 @@ function reifyCreateOperations(view: ViewCompilation, ops: ir.OpList<ir.CreateOp
|
||||
if (op.variable.name === null) {
|
||||
throw new Error(`AssertionError: unnamed variable ${op.xref}`);
|
||||
}
|
||||
// Optimization for variable co-location. If we know the initializer is undefined,
|
||||
// we don't need to set an explicit one to avoid e.g. a `let b = undefined`.
|
||||
let initializer: o.Expression|undefined = op.initializer;
|
||||
if (initializer instanceof o.LiteralExpr && initializer.value === undefined) {
|
||||
initializer = undefined;
|
||||
}
|
||||
ir.OpList.replace<ir.CreateOp>(
|
||||
op,
|
||||
ir.createStatementOp(new o.DeclareVarStmt(
|
||||
op.variable.name, op.initializer, undefined, o.StmtModifier.Final)));
|
||||
op.variable.name, initializer, undefined,
|
||||
op.isConstant ? o.StmtModifier.Final : undefined)));
|
||||
break;
|
||||
case ir.OpKind.PropertyCreate:
|
||||
ir.OpList.replace(op, ng.propertyCreate(op.slot!, op.name, op.expression));
|
||||
break;
|
||||
case ir.OpKind.Statement:
|
||||
// Pass statement operations directly through.
|
||||
@@ -153,7 +162,8 @@ function reifyUpdateOperations(_view: ViewCompilation, ops: ir.OpList<ir.UpdateO
|
||||
ir.OpList.replace<ir.UpdateOp>(
|
||||
op,
|
||||
ir.createStatementOp(new o.DeclareVarStmt(
|
||||
op.variable.name, op.initializer, undefined, o.StmtModifier.Final)));
|
||||
op.variable.name, op.initializer, undefined,
|
||||
op.isConstant ? o.StmtModifier.Final : undefined)));
|
||||
break;
|
||||
case ir.OpKind.Statement:
|
||||
// Pass statement operations directly through.
|
||||
@@ -175,6 +185,8 @@ function reifyIrExpression(expr: o.Expression): o.Expression {
|
||||
return ng.nextContext(expr.steps);
|
||||
case ir.ExpressionKind.Reference:
|
||||
return ng.reference(expr.slot! + 1 + expr.offset);
|
||||
case ir.ExpressionKind.ShallowReference:
|
||||
return ng.shallowReference(expr.slot! + 1 + expr.offset);
|
||||
case ir.ExpressionKind.LexicalRead:
|
||||
throw new Error(`AssertionError: unresolved LexicalRead of ${expr.name}`);
|
||||
case ir.ExpressionKind.RestoreView:
|
||||
@@ -212,6 +224,8 @@ function reifyIrExpression(expr: o.Expression): o.Expression {
|
||||
return ng.pipeBind(expr.slot!, expr.varOffset!, expr.args);
|
||||
case ir.ExpressionKind.PipeBindingVariadic:
|
||||
return ng.pipeBindV(expr.slot!, expr.varOffset!, expr.args);
|
||||
case ir.ExpressionKind.InterpolationTemplateExpr:
|
||||
return ng.stringifyInterpolation(expr.staticParts, expr.expressionParts);
|
||||
default:
|
||||
throw new Error(`AssertionError: Unsupported reification of ir.Expression kind: ${
|
||||
ir.ExpressionKind[(expr as ir.Expression).kind]}`);
|
||||
|
||||
@@ -35,8 +35,20 @@ function processLexicalScope(view: ViewCompilation, ops: ir.OpList<ir.CreateOp|i
|
||||
case ir.OpKind.Variable:
|
||||
switch (op.variable.kind) {
|
||||
case ir.SemanticVariableKind.Context:
|
||||
if (op.variable.view === view.xref) {
|
||||
// This variable is for the same view as `ctx`. Ideally we'd use `ctx`, but we should
|
||||
// prefer the variable in non-root views because of the risk of closure-capturing
|
||||
// `ctx`.
|
||||
|
||||
if (view === view.tpl.root) {
|
||||
// This is the root view, so it's safe to use `ctx` and we don't need to use this
|
||||
// variable.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// This is a reference to a different context.
|
||||
scope.set(op.variable.view, new ir.ReadVariableExpr(op.xref));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ir.OpKind.Listener:
|
||||
|
||||
@@ -19,7 +19,8 @@ export function phaseSaveRestoreView(cpl: ComponentCompilation): void {
|
||||
name: null,
|
||||
view: view.xref,
|
||||
},
|
||||
new ir.GetCurrentViewExpr()),
|
||||
new ir.GetCurrentViewExpr(), /* isConstant */ true),
|
||||
|
||||
]);
|
||||
|
||||
for (const op of view.create) {
|
||||
@@ -56,7 +57,7 @@ function addSaveRestoreViewOperationToListener(view: ViewCompilation, op: ir.Lis
|
||||
name: null,
|
||||
view: view.xref,
|
||||
},
|
||||
new ir.RestoreViewExpr(view.xref)),
|
||||
new ir.RestoreViewExpr(view.xref), /* isConstant */ true),
|
||||
]);
|
||||
|
||||
// The "restore view" operation in listeners requires a call to `resetView` to reset the
|
||||
|
||||
@@ -64,6 +64,7 @@ function varsUsedByOp(op: (ir.CreateOp|ir.UpdateOp)&ir.ConsumesVarsTrait): numbe
|
||||
case ir.OpKind.Property:
|
||||
case ir.OpKind.StyleProp:
|
||||
case ir.OpKind.StyleMap:
|
||||
case ir.OpKind.PropertyCreate:
|
||||
// Property bindings use 1 variable slot.
|
||||
return 1;
|
||||
case ir.OpKind.Attribute:
|
||||
|
||||
@@ -280,10 +280,15 @@ function fencesForIrExpression(expr: ir.Expression): Fence {
|
||||
function collectOpInfo(op: ir.CreateOp|ir.UpdateOp): OpInfo {
|
||||
let fences = Fence.None;
|
||||
const variablesUsed = new Set<ir.XrefId>();
|
||||
ir.visitExpressionsInOp(op, expr => {
|
||||
ir.visitExpressionsInOp(op, (expr, flags) => {
|
||||
if (!ir.isIrExpression(expr)) {
|
||||
return;
|
||||
}
|
||||
// Fences from child operations are not of interest because they allocate
|
||||
// their own variables.
|
||||
if (flags & ir.VisitorContextFlag.InChildOperation) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (expr.kind) {
|
||||
case ir.ExpressionKind.ReadVariable:
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* @description
|
||||
* Entry point from which you should import all public core APIs.
|
||||
*/
|
||||
|
||||
export * from './metadata';
|
||||
export * from './version';
|
||||
export {TypeDecorator} from './util/decorators';
|
||||
|
||||
@@ -24,4 +24,8 @@ export {
|
||||
EffectRef,
|
||||
EffectCleanupFn,
|
||||
} from './render3/reactivity/effect';
|
||||
// clang-format on
|
||||
export {InputSignal, ɵɵGetInputSignalWriteType, ModelSignal} from './render3/reactivity/input_signal';
|
||||
export {input} from './render3/reactivity/input';
|
||||
export {output} from './render3/reactivity/output';
|
||||
export {contentChild, contentChildren, viewChild, viewChildren} from './render3/reactivity/queries';
|
||||
// clang-format on
|
||||
|
||||
@@ -140,6 +140,7 @@ export {
|
||||
ɵɵprojection,
|
||||
ɵɵprojectionDef,
|
||||
ɵɵproperty,
|
||||
ɵɵpropertyCreate,
|
||||
ɵɵpropertyInterpolate,
|
||||
ɵɵpropertyInterpolate1,
|
||||
ɵɵpropertyInterpolate2,
|
||||
@@ -150,6 +151,7 @@ export {
|
||||
ɵɵpropertyInterpolate7,
|
||||
ɵɵpropertyInterpolate8,
|
||||
ɵɵpropertyInterpolateV,
|
||||
ɵɵstringifyInterpolation,
|
||||
ɵɵProvidersFeature,
|
||||
ɵɵHostDirectivesFeature,
|
||||
ɵɵpureFunction0,
|
||||
@@ -162,6 +164,8 @@ export {
|
||||
ɵɵpureFunction7,
|
||||
ɵɵpureFunction8,
|
||||
ɵɵpureFunctionV,
|
||||
ɵɵcontentQueryCreate,
|
||||
ɵɵviewQueryCreate,
|
||||
ɵɵqueryRefresh,
|
||||
ɵɵreference,
|
||||
ɵɵresetView,
|
||||
@@ -172,6 +176,7 @@ export {
|
||||
|
||||
ɵɵsetComponentScope,
|
||||
ɵɵsetNgModuleScope,
|
||||
ɵɵshallowReference,
|
||||
ɵɵStandaloneFeature,
|
||||
ɵɵstyleMap,
|
||||
ɵɵstyleMapInterpolate1,
|
||||
|
||||
@@ -43,7 +43,8 @@ function symbolIterator<T>(this: QueryList<T>): Iterator<T> {
|
||||
* @publicApi
|
||||
*/
|
||||
export class QueryList<T> implements Iterable<T> {
|
||||
public readonly dirty = true;
|
||||
public readonly dirty: boolean = true;
|
||||
private _onDirty?: () => void;
|
||||
private _results: Array<T> = [];
|
||||
private _changesDetected: boolean = false;
|
||||
private _changes: EventEmitter<QueryList<T>>|null = null;
|
||||
@@ -175,9 +176,19 @@ export class QueryList<T> implements Iterable<T> {
|
||||
this._changes.emit(this);
|
||||
}
|
||||
|
||||
/** internal */
|
||||
onDirty(cb: () => void) {
|
||||
this._onDirty = cb;
|
||||
}
|
||||
|
||||
/** internal */
|
||||
setDirty() {
|
||||
(this as {dirty: boolean}).dirty = true;
|
||||
if (this.dirty === false) {
|
||||
(this as {dirty: boolean}).dirty = true;
|
||||
if (this._onDirty) {
|
||||
this._onDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** internal */
|
||||
|
||||
@@ -345,7 +345,7 @@ export interface Directive {
|
||||
/**
|
||||
* // TODO(signals): Remove internal and add public documentation
|
||||
*
|
||||
* @internal
|
||||
* //TODO-internal
|
||||
*/
|
||||
signals?: boolean;
|
||||
|
||||
@@ -635,7 +635,7 @@ export interface Component extends Directive {
|
||||
|
||||
/**
|
||||
* // TODO(signals): Remove internal and add public documentation.
|
||||
* @internal
|
||||
* TODO- internal
|
||||
*/
|
||||
signals?: boolean;
|
||||
|
||||
|
||||
@@ -205,8 +205,8 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
|
||||
hostRenderer, rootSelectorOrNode, this.componentDef.encapsulation, rootViewInjector) :
|
||||
createElementNode(hostRenderer, elementName, getNamespace(elementName));
|
||||
|
||||
// Signal components use the granular "RefreshView" for change detection
|
||||
const signalFlags = (LViewFlags.SignalView | LViewFlags.IsRoot);
|
||||
// Signal components use the granular "RefreshView" for change detection
|
||||
const signalFlags = (LViewFlags.SignalView | LViewFlags.RefreshView | LViewFlags.IsRoot);
|
||||
// Non-signal components use the traditional "CheckAlways or OnPush/Dirty" change detection
|
||||
const nonSignalFlags = this.componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot :
|
||||
LViewFlags.CheckAlways | LViewFlags.IsRoot;
|
||||
@@ -315,7 +315,7 @@ export class ComponentRef<T> extends AbstractComponentRef<T> {
|
||||
}
|
||||
|
||||
const lView = this._rootLView;
|
||||
setInputsForProperty(lView[TVIEW], lView, dataValue, name, value);
|
||||
setInputsForProperty(lView[TVIEW], lView, dataValue, name, value, /* TODO(signals) */ false);
|
||||
this.previousInputValues.set(name, value);
|
||||
const childComponentLView = getComponentLViewByIndex(this._tNode.index, lView);
|
||||
markViewDirty(childComponentLView);
|
||||
@@ -395,7 +395,7 @@ function createRootComponentView(
|
||||
const viewRenderer = environment.rendererFactory.createRenderer(hostRNode, rootComponentDef);
|
||||
let lViewFlags = LViewFlags.CheckAlways;
|
||||
if (rootComponentDef.signals) {
|
||||
lViewFlags = LViewFlags.SignalView;
|
||||
lViewFlags = LViewFlags.SignalView | LViewFlags.RefreshView;
|
||||
} else if (rootComponentDef.onPush) {
|
||||
lViewFlags = LViewFlags.Dirty;
|
||||
}
|
||||
@@ -431,7 +431,7 @@ function applyRootComponentStyling(
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a root component and sets it up with features and host bindings.Shared by
|
||||
* Creates a root component and sets it up with features and host bindings. Shared by
|
||||
* renderComponent() and ViewContainerRef.createComponent().
|
||||
*/
|
||||
function createRootComponent<T>(
|
||||
|
||||
@@ -297,7 +297,7 @@ export function applyUpdateOpCodes(
|
||||
} else {
|
||||
elementPropertyInternal(
|
||||
tView, tNodeOrTagName, lView, propName, value, lView[RENDERER], sanitizeFn,
|
||||
false);
|
||||
false, /* TODO(signals) */ false);
|
||||
}
|
||||
break;
|
||||
case I18nUpdateOpCode.Text:
|
||||
@@ -348,9 +348,11 @@ function applyIcuUpdateCase(tView: TView, tIcu: TIcu, bindingsStartIndex: number
|
||||
let mask = changeMask;
|
||||
if (activeCaseIndex < 0) {
|
||||
// Clear the flag.
|
||||
// Negative number means that the ICU was freshly created and we need to force the update.
|
||||
// Negative number means that the ICU was freshly created and we need to
|
||||
// force the update.
|
||||
activeCaseIndex = lView[tIcu.currentCaseLViewIndex] = ~activeCaseIndex;
|
||||
// -1 is same as all bits on, which simulates creation since it marks all bits dirty
|
||||
// -1 is same as all bits on, which simulates creation since it marks all
|
||||
// bits dirty
|
||||
mask = -1;
|
||||
}
|
||||
applyUpdateOpCodes(tView, lView, tIcu.update[activeCaseIndex], bindingsStartIndex, mask);
|
||||
|
||||
@@ -77,7 +77,10 @@ export {
|
||||
|
||||
ɵɵprojection,
|
||||
ɵɵprojectionDef,
|
||||
|
||||
ɵɵproperty,
|
||||
ɵɵpropertyCreate,
|
||||
|
||||
ɵɵpropertyInterpolate,
|
||||
ɵɵpropertyInterpolate1,
|
||||
ɵɵpropertyInterpolate2,
|
||||
@@ -88,8 +91,10 @@ export {
|
||||
ɵɵpropertyInterpolate7,
|
||||
ɵɵpropertyInterpolate8,
|
||||
ɵɵpropertyInterpolateV,
|
||||
ɵɵstringifyInterpolation,
|
||||
|
||||
ɵɵreference,
|
||||
ɵɵshallowReference,
|
||||
|
||||
ɵɵstyleMap,
|
||||
ɵɵstyleMapInterpolate1,
|
||||
@@ -168,7 +173,9 @@ export {
|
||||
ɵɵcontentQuery,
|
||||
ɵɵloadQuery,
|
||||
ɵɵqueryRefresh,
|
||||
ɵɵviewQuery} from './query';
|
||||
ɵɵviewQuery
|
||||
} from './query';
|
||||
export {ɵɵcontentQueryCreate, ɵɵviewQueryCreate} from './reactivity/queries';
|
||||
export {
|
||||
ɵɵdisableBindings,
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {assertGreaterThan} from '../../util/assert';
|
||||
import {assertIndexInDeclRange} from '../assert';
|
||||
import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks';
|
||||
import {FLAGS, InitPhaseState, LView, LViewFlags, TView} from '../interfaces/view';
|
||||
import {getLView, getSelectedIndex, getTView, isInCheckNoChangesMode, setSelectedIndex} from '../state';
|
||||
import {getLView, getSelectedIndex, getTView, getVirtualInstructionIndex, incrementVirtualInstructionIndex, isInCheckNoChangesMode, setSelectedIndex} from '../state';
|
||||
|
||||
|
||||
/**
|
||||
@@ -45,6 +45,16 @@ export function selectIndexInternal(
|
||||
tView: TView, lView: LView, index: number, checkNoChangesMode: boolean) {
|
||||
ngDevMode && assertIndexInDeclRange(lView, index);
|
||||
|
||||
// Flush virtual instructions up to this point.
|
||||
const {virtualUpdate} = tView;
|
||||
if (virtualUpdate) {
|
||||
for (let idx = getVirtualInstructionIndex();
|
||||
idx < virtualUpdate.length && virtualUpdate[idx].slot < index;
|
||||
idx = incrementVirtualInstructionIndex()) {
|
||||
virtualUpdate[idx].instruction();
|
||||
}
|
||||
}
|
||||
|
||||
// Flush the initial hooks for elements in the view that have been added up to this point.
|
||||
// PERF WARNING: do NOT extract this to a separate function without running benchmarks
|
||||
if (!checkNoChangesMode) {
|
||||
|
||||
@@ -40,6 +40,7 @@ export * from './namespace';
|
||||
export * from './next_context';
|
||||
export * from './projection';
|
||||
export * from './property';
|
||||
export * from './property_create';
|
||||
export * from './property_interpolation';
|
||||
export * from './advance';
|
||||
export * from './styling';
|
||||
|
||||
@@ -13,11 +13,11 @@ import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} fr
|
||||
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS} from '../interfaces/container';
|
||||
import {ComponentTemplate, RenderFlags} from '../interfaces/definition';
|
||||
import {CONTEXT, DESCENDANT_VIEWS_TO_REFRESH, ENVIRONMENT, FLAGS, InitPhaseState, LView, LViewFlags, PARENT, TVIEW, TView} from '../interfaces/view';
|
||||
import {enterView, isInCheckNoChangesMode, leaveView, setBindingIndex, setIsInCheckNoChangesMode} from '../state';
|
||||
import {enterView, getVirtualInstructionIndex, incrementVirtualInstructionIndex, isInCheckNoChangesMode, leaveView, setBindingIndex, setIsInCheckNoChangesMode} from '../state';
|
||||
import {getFirstLContainer, getNextLContainer} from '../util/view_traversal_utils';
|
||||
import {clearViewRefreshFlag, getComponentLViewByIndex, isCreationMode, markViewForRefresh, resetPreOrderHookFlags, viewAttachedToChangeDetector} from '../util/view_utils';
|
||||
|
||||
import {executeTemplate, executeViewQueryFn, handleError, processHostBindingOpCodes, refreshContentQueries} from './shared';
|
||||
import {executeTemplate, executeViewQueryFn, flushVirtualInstructionsBefore, handleError, processHostBindingOpCodes, refreshContentQueries} from './shared';
|
||||
|
||||
export function detectChangesInternal<T>(
|
||||
tView: TView, lView: LView, context: T, notifyErrorHandler = true) {
|
||||
|
||||
@@ -34,7 +34,9 @@ export function ɵɵhostProperty<T>(
|
||||
if (bindingUpdated(lView, bindingIndex, value)) {
|
||||
const tView = getTView();
|
||||
const tNode = getSelectedTNode();
|
||||
elementPropertyInternal(tView, tNode, lView, propName, value, lView[RENDERER], sanitizer, true);
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, value, lView[RENDERER], sanitizer, true,
|
||||
/* TODO(signals) */ false);
|
||||
ngDevMode && storePropertyBindingMetadata(tView.data, tNode, propName, bindingIndex);
|
||||
}
|
||||
return ɵɵhostProperty;
|
||||
@@ -72,7 +74,8 @@ export function ɵɵsyntheticHostProperty<T>(
|
||||
const tNode = getSelectedTNode();
|
||||
const currentDef = getCurrentDirectiveDef(tView.data);
|
||||
const renderer = loadComponentRenderer(currentDef, tNode, lView);
|
||||
elementPropertyInternal(tView, tNode, lView, propName, value, renderer, sanitizer, true);
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, value, renderer, sanitizer, true, /* TODO(signals) */ false);
|
||||
ngDevMode && storePropertyBindingMetadata(tView.data, tNode, propName, bindingIndex);
|
||||
}
|
||||
return ɵɵsyntheticHostProperty;
|
||||
|
||||
@@ -33,14 +33,16 @@ import {elementPropertyInternal, setInputsForProperty, storePropertyBindingMetad
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵproperty<T>(
|
||||
propName: string, value: T, sanitizer?: SanitizerFn|null): typeof ɵɵproperty {
|
||||
propName: string, value: T, sanitizer?: SanitizerFn|null,
|
||||
opts?: {skipSignal: boolean}): typeof ɵɵproperty {
|
||||
const lView = getLView();
|
||||
const bindingIndex = nextBindingIndex();
|
||||
if (bindingUpdated(lView, bindingIndex, value)) {
|
||||
const tView = getTView();
|
||||
const tNode = getSelectedTNode();
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, value, lView[RENDERER], sanitizer, false);
|
||||
tView, tNode, lView, propName, value, lView[RENDERER], sanitizer, false,
|
||||
!!opts?.skipSignal);
|
||||
ngDevMode && storePropertyBindingMetadata(tView.data, tNode, propName, bindingIndex);
|
||||
}
|
||||
return ɵɵproperty;
|
||||
@@ -55,5 +57,5 @@ export function setDirectiveInputsWhichShadowsStyling(
|
||||
const inputs = tNode.inputs!;
|
||||
const property = isClassBased ? 'class' : 'style';
|
||||
// We support both 'class' and `className` hence the fallback.
|
||||
setInputsForProperty(tView, lView, inputs[property], property, value);
|
||||
setInputsForProperty(tView, lView, inputs[property], property, value, false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @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 {validateAgainstEventProperties} from '../../sanitization/sanitization';
|
||||
import {computed, SIGNAL} from '../../signals';
|
||||
import {assertDefined, assertIndexInRange} from '../../util/assert';
|
||||
import {EMPTY_ARRAY} from '../../util/empty';
|
||||
import {bindingUpdated} from '../bindings';
|
||||
import {DirectiveDef} from '../interfaces/definition';
|
||||
import {PropertyAliasValue, TNode} from '../interfaces/node';
|
||||
import {InternalInputSignal} from '../interfaces/reactivity';
|
||||
import {RComment, RElement} from '../interfaces/renderer_dom';
|
||||
import {SanitizerFn} from '../interfaces/sanitization';
|
||||
import {isComponentHost} from '../interfaces/type_checks';
|
||||
import {HEADER_OFFSET, RENDERER} from '../interfaces/view';
|
||||
import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state';
|
||||
import {renderStringify} from '../util/stringify_utils';
|
||||
import {getNativeByTNode} from '../util/view_utils';
|
||||
|
||||
import {handleUnknownPropertyError, isPropertyValid} from './element_validation';
|
||||
import {mapPropName, markDirtyIfOnPush, setNgReflectProperties, writeToDirectiveInput} from './shared';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵpropertyCreate<T>(
|
||||
slot: number, propName: string, expr: () => T,
|
||||
sanitizer?: SanitizerFn|null): typeof ɵɵpropertyCreate {
|
||||
const lView = getLView();
|
||||
const expressionSlot = HEADER_OFFSET + slot;
|
||||
|
||||
const tView = getTView();
|
||||
const tNode = getCurrentTNode();
|
||||
assertDefined(tNode, `propertyCreate() must follow an actual element`);
|
||||
|
||||
const inputData = tNode.inputs?.[propName] ?? EMPTY_ARRAY;
|
||||
|
||||
let signalInputs: InternalInputSignal[]|null = null;
|
||||
|
||||
// PERF: the fact that we need to iterate over all the inputs here isn't great.
|
||||
// We might consider storing more info on TView
|
||||
let zoneTargets: PropertyAliasValue|null = null;
|
||||
for (let i = 0; i < inputData.length;) {
|
||||
const directiveIndex = inputData[i++] as number;
|
||||
const privateName = inputData[i++] as string;
|
||||
const def = tView.data[directiveIndex] as DirectiveDef<any>;
|
||||
if (!def.signals) {
|
||||
// TODO(pk): refactor - code flow with all those firstCreatePass checks becomes hard to follow
|
||||
if (tView.firstCreatePass) {
|
||||
(zoneTargets ??= []).push(directiveIndex, privateName);
|
||||
}
|
||||
} else {
|
||||
ngDevMode && assertIndexInRange(lView, directiveIndex);
|
||||
// PERF: megamorphic read on [privateName] access
|
||||
const inputSignal = lView[directiveIndex][privateName][SIGNAL] as InternalInputSignal;
|
||||
(signalInputs ??= []).push(inputSignal);
|
||||
}
|
||||
}
|
||||
|
||||
zoneTargets ??= EMPTY_ARRAY;
|
||||
signalInputs ??= EMPTY_ARRAY;
|
||||
|
||||
// PERF(pk): I could avoid wrapping into computed for the case of a single binding to a signal
|
||||
// based component
|
||||
expr = computed(expr);
|
||||
|
||||
lView[expressionSlot] = expr;
|
||||
for (const inputSignal of signalInputs) {
|
||||
inputSignal.bindToComputation(expr);
|
||||
}
|
||||
|
||||
if (tView.firstCreatePass) {
|
||||
if (inputData.length === 0) {
|
||||
// Untargeted input -> DOM binding.
|
||||
(tView.virtualUpdate ??= []).push({
|
||||
slot: expressionSlot,
|
||||
instruction: () =>
|
||||
propertyUpdateDom(tNode.index, propName, expressionSlot, sanitizer ?? null),
|
||||
});
|
||||
} else if (zoneTargets.length) {
|
||||
// Some binding targets were zone-based, so we need an update instruction to process them.
|
||||
(tView.virtualUpdate ??= []).push({
|
||||
slot: expressionSlot,
|
||||
instruction: () => propertyUpdateInput(propName, expressionSlot, zoneTargets!),
|
||||
});
|
||||
} else {
|
||||
// The only target(s) were signal-based, so no update path is needed.
|
||||
}
|
||||
}
|
||||
|
||||
return ɵɵpropertyCreate;
|
||||
}
|
||||
|
||||
export function propertyUpdateDom(
|
||||
nodeSlot: number, propName: string, expressionSlot: number, sanitizer: SanitizerFn|null): void {
|
||||
const lView = getLView();
|
||||
const expr = lView[expressionSlot];
|
||||
|
||||
let value = expr();
|
||||
const bindingIndex = nextBindingIndex();
|
||||
if (!bindingUpdated(lView, bindingIndex, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tView = getTView();
|
||||
const tNode = tView.data[nodeSlot] as TNode;
|
||||
const element = getNativeByTNode(tNode, lView) as RElement | RComment;
|
||||
|
||||
propName = mapPropName(propName);
|
||||
|
||||
if (ngDevMode) {
|
||||
validateAgainstEventProperties(propName);
|
||||
if (!isPropertyValid(element, propName, tNode.value, tView.schemas)) {
|
||||
handleUnknownPropertyError(propName, tNode.value, tNode.type, lView);
|
||||
}
|
||||
ngDevMode.rendererSetProperty++;
|
||||
}
|
||||
|
||||
// It is assumed that the sanitizer is only added when the compiler determines that the
|
||||
// property is risky, so sanitization can be done without further checks.
|
||||
value = sanitizer != null ? (sanitizer(value, tNode.value || '', propName) as any) : value;
|
||||
lView[RENDERER].setProperty(element as RElement, propName, value);
|
||||
}
|
||||
|
||||
export function propertyUpdateInput(
|
||||
propName: string, expressionSlot: number, targets: PropertyAliasValue): void {
|
||||
const lView = getLView();
|
||||
const expr = lView[expressionSlot];
|
||||
const value = expr();
|
||||
|
||||
const tNode = getSelectedTNode();
|
||||
const tView = getTView();
|
||||
|
||||
ngDevMode && assertDefined(tNode.inputs, `Expected tNode to have inputs`);
|
||||
|
||||
const bindingIndex = nextBindingIndex();
|
||||
if (!bindingUpdated(lView, bindingIndex, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < targets.length;) {
|
||||
const index = targets[i++] as number;
|
||||
const privateName = targets[i++] as string;
|
||||
ngDevMode && assertIndexInRange(lView, index);
|
||||
const instance = lView[index];
|
||||
const def = tView.data[index] as DirectiveDef<any>;
|
||||
|
||||
writeToDirectiveInput(def, instance, propName, privateName, value);
|
||||
}
|
||||
|
||||
const element = getNativeByTNode(tNode, lView) as RElement | RComment;
|
||||
if (isComponentHost(tNode)) {
|
||||
markDirtyIfOnPush(lView, tNode.index);
|
||||
}
|
||||
if (ngDevMode) {
|
||||
setNgReflectProperties(lView, element, tNode.type, targets, value);
|
||||
}
|
||||
}
|
||||
|
||||
export function ɵɵstringifyInterpolation(
|
||||
staticStrings: TemplateStringsArray, ...expressionValues: any[]): string {
|
||||
// Build the updated content
|
||||
let content = staticStrings[0];
|
||||
for (let i = 1; i < staticStrings.length; i++) {
|
||||
content += renderStringify(expressionValues[i - 1]) + staticStrings[i];
|
||||
}
|
||||
return content;
|
||||
}
|
||||
@@ -87,7 +87,8 @@ export function ɵɵpropertyInterpolate1(
|
||||
const tView = getTView();
|
||||
const tNode = getSelectedTNode();
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false);
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false,
|
||||
/* TODO(signals) */ false);
|
||||
ngDevMode &&
|
||||
storePropertyBindingMetadata(
|
||||
tView.data, tNode, propName, getBindingIndex() - 1, prefix, suffix);
|
||||
@@ -134,7 +135,8 @@ export function ɵɵpropertyInterpolate2(
|
||||
const tView = getTView();
|
||||
const tNode = getSelectedTNode();
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false);
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false,
|
||||
/* TODO(signals) */ false);
|
||||
ngDevMode &&
|
||||
storePropertyBindingMetadata(
|
||||
tView.data, tNode, propName, getBindingIndex() - 2, prefix, i0, suffix);
|
||||
@@ -184,7 +186,8 @@ export function ɵɵpropertyInterpolate3(
|
||||
const tView = getTView();
|
||||
const tNode = getSelectedTNode();
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false);
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false,
|
||||
/* TODO(signals) */ false);
|
||||
ngDevMode &&
|
||||
storePropertyBindingMetadata(
|
||||
tView.data, tNode, propName, getBindingIndex() - 3, prefix, i0, i1, suffix);
|
||||
@@ -236,7 +239,8 @@ export function ɵɵpropertyInterpolate4(
|
||||
const tView = getTView();
|
||||
const tNode = getSelectedTNode();
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false);
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false,
|
||||
/* TODO(signals) */ false);
|
||||
ngDevMode &&
|
||||
storePropertyBindingMetadata(
|
||||
tView.data, tNode, propName, getBindingIndex() - 4, prefix, i0, i1, i2, suffix);
|
||||
@@ -292,7 +296,8 @@ export function ɵɵpropertyInterpolate5(
|
||||
const tView = getTView();
|
||||
const tNode = getSelectedTNode();
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false);
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false,
|
||||
/* TODO(signals) */ false);
|
||||
ngDevMode &&
|
||||
storePropertyBindingMetadata(
|
||||
tView.data, tNode, propName, getBindingIndex() - 5, prefix, i0, i1, i2, i3, suffix);
|
||||
@@ -350,7 +355,8 @@ export function ɵɵpropertyInterpolate6(
|
||||
const tView = getTView();
|
||||
const tNode = getSelectedTNode();
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false);
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false,
|
||||
/* TODO(signals) */ false);
|
||||
ngDevMode &&
|
||||
storePropertyBindingMetadata(
|
||||
tView.data, tNode, propName, getBindingIndex() - 6, prefix, i0, i1, i2, i3, i4, suffix);
|
||||
@@ -410,7 +416,8 @@ export function ɵɵpropertyInterpolate7(
|
||||
const tView = getTView();
|
||||
const tNode = getSelectedTNode();
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false);
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false,
|
||||
/* TODO(signals) */ false);
|
||||
ngDevMode &&
|
||||
storePropertyBindingMetadata(
|
||||
tView.data, tNode, propName, getBindingIndex() - 7, prefix, i0, i1, i2, i3, i4, i5,
|
||||
@@ -473,7 +480,8 @@ export function ɵɵpropertyInterpolate8(
|
||||
const tView = getTView();
|
||||
const tNode = getSelectedTNode();
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false);
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false,
|
||||
/* TODO(signals) */ false);
|
||||
ngDevMode &&
|
||||
storePropertyBindingMetadata(
|
||||
tView.data, tNode, propName, getBindingIndex() - 8, prefix, i0, i1, i2, i3, i4, i5, i6,
|
||||
@@ -520,7 +528,8 @@ export function ɵɵpropertyInterpolateV(
|
||||
const tView = getTView();
|
||||
const tNode = getSelectedTNode();
|
||||
elementPropertyInternal(
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false);
|
||||
tView, tNode, lView, propName, interpolatedValue, lView[RENDERER], sanitizer, false,
|
||||
/* TODO(signals) */ false);
|
||||
if (ngDevMode) {
|
||||
const interpolationInBetween = [values[0]]; // prefix
|
||||
for (let i = 2; i < values.length; i += 2) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {Injector} from '../../di/injector';
|
||||
import {ErrorHandler} from '../../error_handler';
|
||||
import {RuntimeError, RuntimeErrorCode} from '../../errors';
|
||||
import {DehydratedView} from '../../hydration/interfaces';
|
||||
import {hasInSkipHydrationBlockFlag, hasSkipHydrationAttrOnRElement, SKIP_HYDRATION_ATTR_NAME} from '../../hydration/skip_hydration';
|
||||
import {hasSkipHydrationAttrOnRElement} from '../../hydration/skip_hydration';
|
||||
import {PRESERVE_HOST_CONTENT, PRESERVE_HOST_CONTENT_DEFAULT} from '../../hydration/tokens';
|
||||
import {processTextNodeMarkersBeforeHydration} from '../../hydration/utils';
|
||||
import {DoCheck, OnChanges, OnInit} from '../../interface/lifecycle_hooks';
|
||||
@@ -42,7 +42,7 @@ import {clearElementContents, updateTextNode} from '../node_manipulation';
|
||||
import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher';
|
||||
import {profiler, ProfilerEvent} from '../profiler';
|
||||
import {commitLViewConsumerIfHasProducers, getReactiveLViewConsumer} from '../reactive_lview_consumer';
|
||||
import {getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, isInSkipHydrationBlock, leaveView, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setSelectedIndex} from '../state';
|
||||
import {getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, getVirtualInstructionIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, isInSkipHydrationBlock, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setSelectedIndex} from '../state';
|
||||
import {NO_CHANGE} from '../tokens';
|
||||
import {mergeHostAttrs} from '../util/attrs_utils';
|
||||
import {INTERPOLATION_DELIMITER} from '../util/misc_utils';
|
||||
@@ -263,7 +263,20 @@ export function executeTemplate<T>(
|
||||
isUpdatePhase ? ProfilerEvent.TemplateUpdateStart : ProfilerEvent.TemplateCreateStart;
|
||||
profiler(preHookType, context as unknown as {});
|
||||
if (isUpdatePhase) {
|
||||
consumer.runInContext(templateFn, rf, context);
|
||||
consumer.beforeRun();
|
||||
const prevConsumer = setActiveConsumer(consumer);
|
||||
try {
|
||||
templateFn(rf, context);
|
||||
|
||||
// Flush remaining virtual instructions.
|
||||
if (tView.virtualUpdate) {
|
||||
for (let idx = getVirtualInstructionIndex(); idx < tView.virtualUpdate.length; idx++) {
|
||||
tView.virtualUpdate[idx].instruction();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setActiveConsumer(prevConsumer);
|
||||
}
|
||||
} else {
|
||||
const prevConsumer = setActiveConsumer(null);
|
||||
try {
|
||||
@@ -390,7 +403,7 @@ export function createTView(
|
||||
const initialViewLength = bindingStartIndex + vars;
|
||||
const blueprint = createViewBlueprint(bindingStartIndex, initialViewLength);
|
||||
const consts = typeof constsOrFactory === 'function' ? constsOrFactory() : constsOrFactory;
|
||||
const tView = blueprint[TVIEW as any] = {
|
||||
const tView: TView = blueprint[TVIEW as any] = {
|
||||
type: type,
|
||||
blueprint: blueprint,
|
||||
template: templateFn,
|
||||
@@ -421,6 +434,7 @@ export function createTView(
|
||||
schemas: schemas,
|
||||
consts: consts,
|
||||
incompleteFirstPass: false,
|
||||
virtualUpdate: null,
|
||||
ssrId,
|
||||
};
|
||||
if (ngDevMode) {
|
||||
@@ -740,7 +754,7 @@ function initializeInputAndOutputAliases(
|
||||
* Note: this mapping has to be kept in sync with the equally named mapping in the template
|
||||
* type-checking machinery of ngtsc.
|
||||
*/
|
||||
function mapPropName(name: string): string {
|
||||
export function mapPropName(name: string): string {
|
||||
if (name === 'class') return 'className';
|
||||
if (name === 'for') return 'htmlFor';
|
||||
if (name === 'formaction') return 'formAction';
|
||||
@@ -752,13 +766,16 @@ function mapPropName(name: string): string {
|
||||
|
||||
export function elementPropertyInternal<T>(
|
||||
tView: TView, tNode: TNode, lView: LView, propName: string, value: T, renderer: Renderer,
|
||||
sanitizer: SanitizerFn|null|undefined, nativeOnly: boolean): void {
|
||||
sanitizer: SanitizerFn|null|undefined, nativeOnly: boolean, skipSignal: boolean): void {
|
||||
ngDevMode && assertNotSame(value, NO_CHANGE as any, 'Incoming value should never be NO_CHANGE.');
|
||||
const element = getNativeByTNode(tNode, lView) as RElement | RComment;
|
||||
let inputData = tNode.inputs;
|
||||
let dataValue: PropertyAliasValue|undefined;
|
||||
|
||||
|
||||
// TODO(signals): use prop target here
|
||||
if (!nativeOnly && inputData != null && (dataValue = inputData[propName])) {
|
||||
setInputsForProperty(tView, lView, dataValue, propName, value);
|
||||
setInputsForProperty(tView, lView, dataValue, propName, value, skipSignal);
|
||||
if (isComponentHost(tNode)) markDirtyIfOnPush(lView, tNode.index);
|
||||
if (ngDevMode) {
|
||||
setNgReflectProperties(lView, element, tNode.type, dataValue, value);
|
||||
@@ -1232,7 +1249,7 @@ function addComponentLogic<T>(lView: LView, hostTNode: TElementNode, def: Compon
|
||||
const rendererFactory = lView[ENVIRONMENT].rendererFactory;
|
||||
let lViewFlags = LViewFlags.CheckAlways;
|
||||
if (def.signals) {
|
||||
lViewFlags = LViewFlags.SignalView;
|
||||
lViewFlags = LViewFlags.SignalView | LViewFlags.RefreshView;
|
||||
} else if (def.onPush) {
|
||||
lViewFlags = LViewFlags.Dirty;
|
||||
}
|
||||
@@ -1307,7 +1324,7 @@ function setInputsFromAttrs<T>(
|
||||
}
|
||||
}
|
||||
|
||||
function writeToDirectiveInput<T>(
|
||||
export function writeToDirectiveInput<T>(
|
||||
def: DirectiveDef<T>, instance: T, publicName: string, privateName: string, value: string) {
|
||||
const prevConsumer = setActiveConsumer(null);
|
||||
try {
|
||||
@@ -1568,7 +1585,8 @@ export function handleError(lView: LView, error: any): void {
|
||||
* @param value Value to set.
|
||||
*/
|
||||
export function setInputsForProperty(
|
||||
tView: TView, lView: LView, inputs: PropertyAliasValue, publicName: string, value: any): void {
|
||||
tView: TView, lView: LView, inputs: PropertyAliasValue, publicName: string, value: any,
|
||||
skipSignal: boolean): void {
|
||||
for (let i = 0; i < inputs.length;) {
|
||||
const index = inputs[i++] as number;
|
||||
const privateName = inputs[i++] as string;
|
||||
@@ -1576,6 +1594,11 @@ export function setInputsForProperty(
|
||||
ngDevMode && assertIndexInRange(lView, index);
|
||||
const def = tView.data[index] as DirectiveDef<any>;
|
||||
|
||||
// TODO(signals): not ideal. just for prototyping.
|
||||
if (def.signals && skipSignal) {
|
||||
continue;
|
||||
}
|
||||
|
||||
writeToDirectiveInput(def, instance, publicName, privateName, value);
|
||||
}
|
||||
}
|
||||
@@ -1591,3 +1614,5 @@ export function textBindingInternal(lView: LView, index: number, value: string):
|
||||
ngDevMode && assertDefined(element, 'native element should exist');
|
||||
updateTextNode(lView[RENDERER], element, value);
|
||||
}
|
||||
|
||||
export function flushVirtualInstructionsBefore() {}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @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 {PropertyAliases, PropertyAliasValue, TNode, TNodeType} from '../interfaces/node';
|
||||
|
||||
export enum TargetType {
|
||||
Input,
|
||||
DomBinding,
|
||||
Container,
|
||||
}
|
||||
|
||||
export interface InputTarget {
|
||||
kind: TargetType.Input;
|
||||
data: PropertyAliasValue;
|
||||
}
|
||||
|
||||
export interface DomBindingTarget {
|
||||
kind: TargetType.DomBinding;
|
||||
}
|
||||
|
||||
export interface ContainerTarget {
|
||||
kind: TargetType.Container;
|
||||
}
|
||||
|
||||
export function analyzePropertyForElement(tNode: TNode, propName: string): InputTarget|
|
||||
DomBindingTarget|ContainerTarget|null {
|
||||
const inputs = tNode.inputs;
|
||||
let inputData: PropertyAliasValue|null = null;
|
||||
if (inputs !== null && (inputData = inputs[propName]) !== undefined) {
|
||||
return {kind: TargetType.Input, data: inputData};
|
||||
}
|
||||
if (tNode.type & TNodeType.AnyRNode) {
|
||||
return {kind: TargetType.DomBinding};
|
||||
}
|
||||
if (tNode.type & TNodeType.AnyContainer) {
|
||||
return {kind: TargetType.DomBinding};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {HEADER_OFFSET, LView, TView} from '../interfaces/view';
|
||||
import {getContextLView} from '../state';
|
||||
import {getContextLView, getLView} from '../state';
|
||||
import {load} from '../util/view_utils';
|
||||
|
||||
|
||||
@@ -35,3 +35,14 @@ export function ɵɵreference<T>(index: number) {
|
||||
const contextLView = getContextLView();
|
||||
return load<T>(contextLView, HEADER_OFFSET + index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a local reference from the current `LView`.
|
||||
*
|
||||
* @param index The relative index of the local ref in `LView`.
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵshallowReference<T>(index: number): T {
|
||||
return load<T>(getLView(), HEADER_OFFSET + index);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import {getLView} from '../state';
|
||||
|
||||
import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation';
|
||||
import {ɵɵstyleMap} from './styling';
|
||||
|
||||
|
||||
@@ -227,7 +227,9 @@ export function checkStylingMap(
|
||||
assertEqual(
|
||||
staticPrefix.endsWith(';'), true, 'Expecting static portion to end with \';\'');
|
||||
if (staticPrefix !== null) {
|
||||
// We want to make sure that falsy values of `value` become empty strings.
|
||||
// We want to make sure that falsy values of `value` become empty strings.'
|
||||
|
||||
// THIS MAY NOT BE A STRING...
|
||||
value = concatStringsWithSpace(staticPrefix, value ? value : '');
|
||||
}
|
||||
// Given `<div [style] my-dir>` such that `my-dir` has `@Input('style')`.
|
||||
@@ -771,10 +773,11 @@ function updateStyling(
|
||||
*
|
||||
* @param tData `TData` used for traversing the priority.
|
||||
* @param tNode `TNode` to use for resolving static styling. Also controls search direction.
|
||||
* - `TNode` search next and quit as soon as `isStylingValuePresent(value)` is true.
|
||||
* If no value found consult `tNode.residualStyle`/`tNode.residualClass` for default value.
|
||||
* - `null` search prev and go all the way to end. Return last value where
|
||||
* `isStylingValuePresent(value)` is true.
|
||||
* - `TNode` search next and go all the way to the end. Return the last value where
|
||||
* `isStylingValuePresent(value)` is true. If no value was found, we consult
|
||||
* `tNode.residualStyle`/`tNode.residualClass` for default value.
|
||||
* - `null` search prev and exit as soon as we find a styling value. i.e. the last preceding
|
||||
* value for the property, where `isStylingValuePresent(value)` is true.
|
||||
* @param lView `LView` used for retrieving the actual values.
|
||||
* @param prop Property which we are interested in.
|
||||
* @param index Starting index in the linked list of styling bindings where the search should start.
|
||||
@@ -784,10 +787,15 @@ function findStylingValue(
|
||||
tData: TData, tNode: TNode|null, lView: LView, prop: string, index: number,
|
||||
isClassBased: boolean): any {
|
||||
// `TNode` to use for resolving static styling. Also controls search direction.
|
||||
// - `TNode` search next and quit as soon as `isStylingValuePresent(value)` is true.
|
||||
// If no value found consult `tNode.residualStyle`/`tNode.residualClass` for default value.
|
||||
// - `null` search prev and go all the way to end. Return last value where
|
||||
// `isStylingValuePresent(value)` is true.
|
||||
// - `TNode` search next and go all the way to the end. Return the last value where
|
||||
// `isStylingValuePresent(value)` is true. If no value was found, we consult
|
||||
// `tNode.residualStyle`/`tNode.residualClass` for default value.
|
||||
// - `null` search prev and exit as soon as we find a styling value. i.e. the last preceding
|
||||
// value for the property, where `isStylingValuePresent(value)` is true.
|
||||
// NB: Usually we first look if there is a higher priority value for the property. e.g. resolving
|
||||
// in order. If we then discover there is no better value, and our current instruction **does
|
||||
// not** have a value either- we attempt to resolve in reverse to find lower-priority
|
||||
// bindings with a value as a backup.
|
||||
const isPrevDirection = tNode === null;
|
||||
let value: any = undefined;
|
||||
while (index > 0) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* The private API for signal-based inputs used by the runtime.
|
||||
*/
|
||||
export interface InternalInputSignal {
|
||||
bindToComputation(computation: () => unknown): void;
|
||||
bindToValue(value: unknown): void;
|
||||
initialized(): void;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {ProviderToken} from '../../di/provider_token';
|
||||
import {DehydratedView} from '../../hydration/interfaces';
|
||||
import {SchemaMetadata} from '../../metadata/schema';
|
||||
import {Sanitizer} from '../../sanitization/sanitizer';
|
||||
|
||||
import type {ReactiveLViewConsumer} from '../reactive_lview_consumer';
|
||||
import type {EffectManager} from '../reactivity/effect';
|
||||
|
||||
@@ -832,6 +833,8 @@ export interface TView {
|
||||
* (see `getComponentId` function for details)
|
||||
*/
|
||||
ssrId: string|null;
|
||||
|
||||
virtualUpdate: TVirtualInstruction[]|null;
|
||||
}
|
||||
|
||||
/** Single hook callback function. */
|
||||
@@ -907,6 +910,11 @@ export type DestroyHookData = (HookEntry|HookData)[];
|
||||
export type TData = (TNode|PipeDef<any>|DirectiveDef<any>|ComponentDef<any>|number|TStylingRange|
|
||||
TStylingKey|ProviderToken<any>|TI18n|I18nUpdateOpCodes|TIcu|null|string)[];
|
||||
|
||||
export interface TVirtualInstruction {
|
||||
slot: number;
|
||||
instruction: () => void;
|
||||
}
|
||||
|
||||
// Note: This hack is necessary so we don't erroneously get a circular dependency
|
||||
// failure based on types.
|
||||
export const unusedValueExportToPlacateAjd = 1;
|
||||
|
||||
@@ -89,6 +89,8 @@ export const angularCoreEnv: {[name: string]: Function} =
|
||||
'ɵɵprojectionDef': r3.ɵɵprojectionDef,
|
||||
'ɵɵhostProperty': r3.ɵɵhostProperty,
|
||||
'ɵɵproperty': r3.ɵɵproperty,
|
||||
'ɵɵpropertyCreate': r3.ɵɵpropertyCreate,
|
||||
'ɵɵstringifyInterpolation': r3.ɵɵstringifyInterpolation,
|
||||
'ɵɵpropertyInterpolate': r3.ɵɵpropertyInterpolate,
|
||||
'ɵɵpropertyInterpolate1': r3.ɵɵpropertyInterpolate1,
|
||||
'ɵɵpropertyInterpolate2': r3.ɵɵpropertyInterpolate2,
|
||||
@@ -100,11 +102,14 @@ export const angularCoreEnv: {[name: string]: Function} =
|
||||
'ɵɵpropertyInterpolate8': r3.ɵɵpropertyInterpolate8,
|
||||
'ɵɵpropertyInterpolateV': r3.ɵɵpropertyInterpolateV,
|
||||
'ɵɵpipe': r3.ɵɵpipe,
|
||||
'ɵɵviewQueryCreate': r3.ɵɵviewQueryCreate,
|
||||
'ɵɵcontentQueryCreate': r3.ɵɵcontentQueryCreate,
|
||||
'ɵɵqueryRefresh': r3.ɵɵqueryRefresh,
|
||||
'ɵɵviewQuery': r3.ɵɵviewQuery,
|
||||
'ɵɵloadQuery': r3.ɵɵloadQuery,
|
||||
'ɵɵcontentQuery': r3.ɵɵcontentQuery,
|
||||
'ɵɵreference': r3.ɵɵreference,
|
||||
'ɵɵshallowReference': r3.ɵɵshallowReference,
|
||||
'ɵɵclassMap': r3.ɵɵclassMap,
|
||||
'ɵɵclassMapInterpolate1': r3.ɵɵclassMapInterpolate1,
|
||||
'ɵɵclassMapInterpolate2': r3.ɵɵclassMapInterpolate2,
|
||||
|
||||
@@ -7,11 +7,19 @@
|
||||
*/
|
||||
|
||||
import {assertIndexInRange} from '../util/assert';
|
||||
|
||||
import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, getBinding, updateBinding} from './bindings';
|
||||
import {LView} from './interfaces/view';
|
||||
import {getBindingRoot, getLView} from './state';
|
||||
import {NO_CHANGE} from './tokens';
|
||||
|
||||
let pureFunctionsEnabled = true;
|
||||
|
||||
export function setPureFunctionsEnabled(value: boolean): boolean {
|
||||
const prev = pureFunctionsEnabled;
|
||||
pureFunctionsEnabled = value;
|
||||
return prev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bindings for pure functions are stored after regular bindings.
|
||||
@@ -43,6 +51,8 @@ import {NO_CHANGE} from './tokens';
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵpureFunction0<T>(slotOffset: number, pureFn: () => T, thisArg?: any): T {
|
||||
if (!pureFunctionsEnabled) return thisArg ? pureFn.call(thisArg) : pureFn();
|
||||
|
||||
const bindingIndex = getBindingRoot() + slotOffset;
|
||||
const lView = getLView();
|
||||
return lView[bindingIndex] === NO_CHANGE ?
|
||||
@@ -308,9 +318,11 @@ export function pureFunction1Internal(
|
||||
lView: LView, bindingRoot: number, slotOffset: number, pureFn: (v: any) => any, exp: any,
|
||||
thisArg?: any): any {
|
||||
const bindingIndex = bindingRoot + slotOffset;
|
||||
return bindingUpdated(lView, bindingIndex, exp) ?
|
||||
updateBinding(lView, bindingIndex + 1, thisArg ? pureFn.call(thisArg, exp) : pureFn(exp)) :
|
||||
getPureFunctionReturnValue(lView, bindingIndex + 1);
|
||||
if (pureFunctionsEnabled && !bindingUpdated(lView, bindingIndex, exp)) {
|
||||
return getPureFunctionReturnValue(lView, bindingIndex + 1);
|
||||
}
|
||||
const value = thisArg ? pureFn.call(thisArg, exp) : pureFn(exp);
|
||||
return pureFunctionsEnabled ? updateBinding(lView, bindingIndex + 1, value) : value;
|
||||
}
|
||||
|
||||
|
||||
@@ -331,11 +343,11 @@ export function pureFunction2Internal(
|
||||
lView: LView, bindingRoot: number, slotOffset: number, pureFn: (v1: any, v2: any) => any,
|
||||
exp1: any, exp2: any, thisArg?: any): any {
|
||||
const bindingIndex = bindingRoot + slotOffset;
|
||||
return bindingUpdated2(lView, bindingIndex, exp1, exp2) ?
|
||||
updateBinding(
|
||||
lView, bindingIndex + 2,
|
||||
thisArg ? pureFn.call(thisArg, exp1, exp2) : pureFn(exp1, exp2)) :
|
||||
getPureFunctionReturnValue(lView, bindingIndex + 2);
|
||||
if (pureFunctionsEnabled && !bindingUpdated2(lView, bindingIndex, exp1, exp2)) {
|
||||
return getPureFunctionReturnValue(lView, bindingIndex + 2);
|
||||
}
|
||||
const value = thisArg ? pureFn.call(thisArg, exp1, exp2) : pureFn(exp1, exp2);
|
||||
return pureFunctionsEnabled ? updateBinding(lView, bindingIndex + 2, value) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,11 +369,11 @@ export function pureFunction3Internal(
|
||||
pureFn: (v1: any, v2: any, v3: any) => any, exp1: any, exp2: any, exp3: any,
|
||||
thisArg?: any): any {
|
||||
const bindingIndex = bindingRoot + slotOffset;
|
||||
return bindingUpdated3(lView, bindingIndex, exp1, exp2, exp3) ?
|
||||
updateBinding(
|
||||
lView, bindingIndex + 3,
|
||||
thisArg ? pureFn.call(thisArg, exp1, exp2, exp3) : pureFn(exp1, exp2, exp3)) :
|
||||
getPureFunctionReturnValue(lView, bindingIndex + 3);
|
||||
if (pureFunctionsEnabled && !bindingUpdated3(lView, bindingIndex, exp1, exp2, exp3)) {
|
||||
return getPureFunctionReturnValue(lView, bindingIndex + 3);
|
||||
}
|
||||
const value = thisArg ? pureFn.call(thisArg, exp1, exp2, exp3) : pureFn(exp1, exp2, exp3);
|
||||
return pureFunctionsEnabled ? updateBinding(lView, bindingIndex + 3, value) : value;
|
||||
}
|
||||
|
||||
|
||||
@@ -386,11 +398,12 @@ export function pureFunction4Internal(
|
||||
pureFn: (v1: any, v2: any, v3: any, v4: any) => any, exp1: any, exp2: any, exp3: any, exp4: any,
|
||||
thisArg?: any): any {
|
||||
const bindingIndex = bindingRoot + slotOffset;
|
||||
return bindingUpdated4(lView, bindingIndex, exp1, exp2, exp3, exp4) ?
|
||||
updateBinding(
|
||||
lView, bindingIndex + 4,
|
||||
thisArg ? pureFn.call(thisArg, exp1, exp2, exp3, exp4) : pureFn(exp1, exp2, exp3, exp4)) :
|
||||
getPureFunctionReturnValue(lView, bindingIndex + 4);
|
||||
if (pureFunctionsEnabled && !bindingUpdated4(lView, bindingIndex, exp1, exp2, exp3, exp4)) {
|
||||
return getPureFunctionReturnValue(lView, bindingIndex + 4);
|
||||
}
|
||||
const value =
|
||||
thisArg ? pureFn.call(thisArg, exp1, exp2, exp3, exp4) : pureFn(exp1, exp2, exp3, exp4);
|
||||
return pureFunctionsEnabled ? updateBinding(lView, bindingIndex + 4, value) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -413,9 +426,15 @@ export function pureFunctionVInternal(
|
||||
exps: any[], thisArg?: any): any {
|
||||
let bindingIndex = bindingRoot + slotOffset;
|
||||
let different = false;
|
||||
for (let i = 0; i < exps.length; i++) {
|
||||
bindingUpdated(lView, bindingIndex++, exps[i]) && (different = true);
|
||||
if (pureFunctionsEnabled) {
|
||||
for (let i = 0; i < exps.length; i++) {
|
||||
bindingUpdated(lView, bindingIndex++, exps[i]) && (different = true);
|
||||
}
|
||||
if (!different) {
|
||||
return getPureFunctionReturnValue(lView, bindingIndex);
|
||||
}
|
||||
}
|
||||
return different ? updateBinding(lView, bindingIndex, pureFn.apply(thisArg, exps)) :
|
||||
getPureFunctionReturnValue(lView, bindingIndex);
|
||||
|
||||
const value = pureFn.apply(thisArg, exps);
|
||||
return pureFunctionsEnabled ? updateBinding(lView, bindingIndex, value) : value;
|
||||
}
|
||||
|
||||
@@ -418,12 +418,18 @@ function collectQueryResults<T>(tView: TView, lView: LView, queryIndex: number,
|
||||
*/
|
||||
export function ɵɵqueryRefresh(queryList: QueryList<any>): boolean {
|
||||
const lView = getLView();
|
||||
const tView = getTView();
|
||||
const queryIndex = getCurrentQueryIndex();
|
||||
|
||||
setCurrentQueryIndex(queryIndex + 1);
|
||||
return queryRefreshInternal(lView, queryIndex);
|
||||
}
|
||||
|
||||
export function queryRefreshInternal(lView: LView, queryIndex: number): boolean {
|
||||
const tView = lView[TVIEW];
|
||||
const tQuery = getTQuery(tView, queryIndex);
|
||||
const lQuery = lView[QUERIES]!.queries![queryIndex];
|
||||
const queryList = lQuery.queryList;
|
||||
|
||||
if (queryList.dirty &&
|
||||
(isCreationMode(lView) ===
|
||||
((tQuery.metadata.flags & QueryFlags.isStatic) === QueryFlags.isStatic))) {
|
||||
@@ -453,6 +459,12 @@ export function ɵɵqueryRefresh(queryList: QueryList<any>): boolean {
|
||||
*/
|
||||
export function ɵɵviewQuery<T>(
|
||||
predicate: ProviderToken<unknown>|string[], flags: QueryFlags, read?: any): void {
|
||||
createViewQueryInternal(getLView(), predicate, flags, read);
|
||||
}
|
||||
|
||||
export function createViewQueryInternal<T>(
|
||||
lView: LView, predicate: ProviderToken<unknown>|string[], flags: QueryFlags,
|
||||
read?: any): number {
|
||||
ngDevMode && assertNumber(flags, 'Expecting flags');
|
||||
const tView = getTView();
|
||||
if (tView.firstCreatePass) {
|
||||
@@ -461,7 +473,7 @@ export function ɵɵviewQuery<T>(
|
||||
tView.staticViewQueries = true;
|
||||
}
|
||||
}
|
||||
createLQuery<T>(tView, getLView(), flags);
|
||||
return createLQuery<T>(tView, lView, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -479,6 +491,16 @@ export function ɵɵviewQuery<T>(
|
||||
export function ɵɵcontentQuery<T>(
|
||||
directiveIndex: number, predicate: ProviderToken<unknown>|string[], flags: QueryFlags,
|
||||
read?: any): void {
|
||||
createContentQueryInternal<T>(getLView(), directiveIndex, predicate, flags, read);
|
||||
}
|
||||
|
||||
export function createContentQueryInternal<T>(
|
||||
lView: LView,
|
||||
directiveIndex: number,
|
||||
predicate: ProviderToken<unknown>|string[],
|
||||
flags: QueryFlags,
|
||||
read?: any,
|
||||
) {
|
||||
ngDevMode && assertNumber(flags, 'Expecting flags');
|
||||
const tView = getTView();
|
||||
if (tView.firstCreatePass) {
|
||||
@@ -490,7 +512,7 @@ export function ɵɵcontentQuery<T>(
|
||||
}
|
||||
}
|
||||
|
||||
createLQuery<T>(tView, getLView(), flags);
|
||||
return createLQuery<T>(tView, lView, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -502,20 +524,22 @@ export function ɵɵloadQuery<T>(): QueryList<T> {
|
||||
return loadQueryInternal<T>(getLView(), getCurrentQueryIndex());
|
||||
}
|
||||
|
||||
function loadQueryInternal<T>(lView: LView, queryIndex: number): QueryList<T> {
|
||||
export function loadQueryInternal<T>(lView: LView, queryIndex: number): QueryList<T> {
|
||||
ngDevMode &&
|
||||
assertDefined(lView[QUERIES], 'LQueries should be defined when trying to load a query');
|
||||
ngDevMode && assertIndexInRange(lView[QUERIES]!.queries, queryIndex);
|
||||
return lView[QUERIES]!.queries[queryIndex].queryList;
|
||||
}
|
||||
|
||||
function createLQuery<T>(tView: TView, lView: LView, flags: QueryFlags) {
|
||||
function createLQuery<T>(tView: TView, lView: LView, flags: QueryFlags): number {
|
||||
const queryList = new QueryList<T>(
|
||||
(flags & QueryFlags.emitDistinctChangesOnly) === QueryFlags.emitDistinctChangesOnly);
|
||||
storeCleanupWithContext(tView, lView, queryList, queryList.destroy);
|
||||
|
||||
if (lView[QUERIES] === null) lView[QUERIES] = new LQueries_();
|
||||
lView[QUERIES]!.queries.push(new LQuery_(queryList));
|
||||
|
||||
return lView[QUERIES]!.queries.length - 1;
|
||||
}
|
||||
|
||||
function createTQuery(tView: TView, metadata: TQueryMetadata, nodeIndex: number): void {
|
||||
|
||||
@@ -51,6 +51,10 @@ export class ReactiveLViewConsumer extends ReactiveNode {
|
||||
}
|
||||
}
|
||||
|
||||
beforeRun(): void {
|
||||
this.trackingVersion++;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
// Incrementing the version means that every producer which tries to update this consumer will
|
||||
// consider its record stale, and not notify.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @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 {createSignalFromFunction} from '../../signals';
|
||||
|
||||
import {InputSignal, InputSignalImpl} from './input_signal';
|
||||
|
||||
export interface PrimaryInputOptions<ReadT, WriteT> {
|
||||
alias?: string;
|
||||
transform?: (value: WriteT) => ReadT;
|
||||
}
|
||||
|
||||
export interface InputOptions<ReadT, WriteT> extends PrimaryInputOptions<ReadT, WriteT> {
|
||||
initialValue?: WriteT;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export function input(): InputSignal<undefined, undefined>;
|
||||
export function input<T>(): InputSignal<T|undefined, T>;
|
||||
export function input<T>(
|
||||
initialValue: T&(string | number | boolean),
|
||||
opts?: PrimaryInputOptions<T, T>&{transform?: undefined}): InputSignal<T, T>;
|
||||
export function input<ReadT, WriteT = ReadT>(
|
||||
initialValue: WriteT&(string | number | boolean),
|
||||
opts: PrimaryInputOptions<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
|
||||
export function input<T>(opts: InputOptions<T, T>&
|
||||
{required: true, transform?: undefined}): InputSignal<T, T>;
|
||||
export function input<ReadT, WriteT = ReadT>(opts: InputOptions<ReadT, WriteT>&
|
||||
{required: true}): InputSignal<ReadT, WriteT>;
|
||||
export function input<T>(opts: InputOptions<T, T>&
|
||||
{initialValue: T, transform?: undefined}): InputSignal<T, T>;
|
||||
export function input<ReadT, WriteT = ReadT>(opts: InputOptions<ReadT, WriteT>&
|
||||
{initialValue: ReadT}): InputSignal<ReadT, WriteT>;
|
||||
export function input<ReadT, WriteT = ReadT>(opts: InputOptions<ReadT, WriteT>):
|
||||
InputSignal<ReadT|undefined, WriteT>;
|
||||
export function input<ReadT, WriteT>(opts?: InputOptions<ReadT, WriteT>):
|
||||
InputSignal<ReadT, WriteT> {
|
||||
const node = new InputSignalImpl<ReadT, WriteT>(opts?.initialValue, opts?.transform ?? null);
|
||||
return createSignalFromFunction(node, node.signal.bind(node)) as InputSignal<ReadT, WriteT>;
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* @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 {ComputedImpl, createSignalFromFunction, defaultEquals, ERRORED, setActiveConsumer, Signal, UNSET, WritableSignal} from '../../signals';
|
||||
import {setPureFunctionsEnabled} from '../pure_function';
|
||||
|
||||
export const BRAND_WRITE_TYPE = Symbol();
|
||||
|
||||
/**
|
||||
* A `Signal` representing a component or directive input.
|
||||
*
|
||||
* This is equivalent to a `Signal`, except it also carries type information about the
|
||||
*/
|
||||
export type InputSignal<ReadT, WriteT> = Signal<ReadT>&{
|
||||
[BRAND_WRITE_TYPE]: WriteT;
|
||||
};
|
||||
|
||||
|
||||
// TODO(signals)
|
||||
// This will be replaced, depending on how we implement `InputSignal`
|
||||
export type ɵɵGetInputSignalWriteType<T> = T extends InputSignal<any, infer X>? X : never;
|
||||
|
||||
/**
|
||||
* A `Signal` representing a component or directive model input.
|
||||
*
|
||||
* Model inputs also have the `WritableSignal` interface for their WriteTer side.
|
||||
*/
|
||||
export type ModelSignal<ReadT, WriteT> =
|
||||
InputSignal<ReadT, WriteT>&Pick<WritableSignal<WriteT>, 'set'|'update'|'mutate'>;
|
||||
|
||||
/**
|
||||
* Internal implementation of input signals, derived from `Computed`.
|
||||
*/
|
||||
export class InputSignalImpl<ReadT, WriteT> extends ComputedImpl<ReadT> {
|
||||
/**
|
||||
* The computation to which the `InputSignal` is currently bound, if bound to a computation.
|
||||
*/
|
||||
protected boundComputation: (() => WriteT)|null = null;
|
||||
|
||||
/**
|
||||
* If the `InputSignal` is not bound to a computation, then it's bound to a value.
|
||||
*/
|
||||
protected boundValue: WriteT|undefined;
|
||||
|
||||
// TODO: this should be `false` to start with, and the runtime should call `initialized` after all
|
||||
// inputs have been set for the first time. However this is not currently implemented, so we cheat
|
||||
// and treat all input signals as initialized for now.
|
||||
protected isInitialized = true;
|
||||
protected transform: (value: WriteT) => ReadT;
|
||||
|
||||
/**
|
||||
* Whether to skip polling our dependencies for change.
|
||||
*
|
||||
* Input signals can have their computation/value updated directly (the change doesn't come from
|
||||
* producers, but rather is initiated at this node).
|
||||
*/
|
||||
protected assumeDependenciesHaveChanged = false;
|
||||
|
||||
constructor(defaultValue: WriteT|undefined, transform: ((value: WriteT) => ReadT)|null) {
|
||||
super(null!, defaultEquals);
|
||||
|
||||
this.boundValue = defaultValue;
|
||||
this.computation = this.switchedComputation.bind(this);
|
||||
|
||||
if (transform !== null) {
|
||||
this.transform = (value: WriteT) => {
|
||||
const prev = setActiveConsumer(null);
|
||||
try {
|
||||
return transform(value);
|
||||
} finally {
|
||||
setActiveConsumer(prev);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
this.transform = noopTransform as typeof this.transform;
|
||||
}
|
||||
}
|
||||
|
||||
bindToComputation(computation: () => WriteT): void {
|
||||
this.boundComputation = computation;
|
||||
this.boundValue = undefined;
|
||||
this.stale = true;
|
||||
this.assumeDependenciesHaveChanged = true;
|
||||
this.producerMayHaveChanged();
|
||||
}
|
||||
|
||||
bindToValue(value: WriteT): void {
|
||||
this.boundComputation = null;
|
||||
this.boundValue = value;
|
||||
this.stale = true;
|
||||
this.assumeDependenciesHaveChanged = true;
|
||||
this.producerMayHaveChanged();
|
||||
}
|
||||
|
||||
initialized(): void {
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
protected switchedComputation(): ReadT {
|
||||
if (!this.isInitialized) {
|
||||
// TODO(alxhub): Make this a proper RuntimeError
|
||||
throw new Error(`InputSignal not yet initialized`);
|
||||
}
|
||||
|
||||
if (this.boundComputation !== null) {
|
||||
// Disable pure function memoization when running computations of input signals.
|
||||
//
|
||||
// Bound computations are generated with instructions in place to memoize allocations like
|
||||
// object literals, or for pipe transformations. Such operations do not need to be memoized in
|
||||
// input computations as the `InputSignal` naturally memoizes the whole expression.
|
||||
const prevPureFunctionsEnabled = setPureFunctionsEnabled(false);
|
||||
try {
|
||||
return this.transform(this.boundComputation());
|
||||
} finally {
|
||||
setPureFunctionsEnabled(prevPureFunctionsEnabled);
|
||||
}
|
||||
} else {
|
||||
// `boundValue` is only `undefined` when `boundComputation` is not (unless `undefined` is
|
||||
// actually the current value).
|
||||
return this.transform(this.boundValue!);
|
||||
}
|
||||
}
|
||||
|
||||
protected override recomputeValue(): void {
|
||||
this.assumeDependenciesHaveChanged = false;
|
||||
super.recomputeValue();
|
||||
}
|
||||
|
||||
protected override consumerPollProducersForChange(): boolean {
|
||||
if (this.assumeDependenciesHaveChanged) {
|
||||
return true;
|
||||
}
|
||||
return super.consumerPollProducersForChange();
|
||||
}
|
||||
}
|
||||
|
||||
export class ModelSignalImpl<ReadT, WriteT> extends InputSignalImpl<ReadT, WriteT> {
|
||||
setTemporarily(setValue: WriteT): void {
|
||||
// Check whether the current and new values are equal.
|
||||
this.onProducerUpdateValueVersion();
|
||||
const newValue = this.transform(setValue);
|
||||
if (this.value !== UNSET && this.value !== ERRORED && this.equal(this.value, newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.value = newValue;
|
||||
this.error = null;
|
||||
this.valueVersion++;
|
||||
this.producerMayHaveChanged();
|
||||
}
|
||||
|
||||
updateTemporarily(update: (value: ReadT) => WriteT): void {
|
||||
this.onProducerUpdateValueVersion();
|
||||
if (this.value === UNSET || this.value === ERRORED) {
|
||||
return;
|
||||
}
|
||||
this.setTemporarily(update(this.value));
|
||||
}
|
||||
|
||||
mutateTemporarily(mutator: (value: ReadT) => void): void {
|
||||
this.onProducerUpdateValueVersion();
|
||||
if (this.value === ERRORED) {
|
||||
throw this.error;
|
||||
}
|
||||
mutator(this.value);
|
||||
this.valueVersion++;
|
||||
this.producerMayHaveChanged();
|
||||
}
|
||||
}
|
||||
|
||||
function noopTransform<T>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @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 {EventEmitter} from '../../event_emitter';
|
||||
|
||||
export function output<T>(opts?: {alias?: string, isAsync?: boolean}): EventEmitter<T> {
|
||||
return new EventEmitter(opts?.isAsync);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @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 {ProviderToken} from '../../di/provider_token';
|
||||
import {QueryList} from '../../linker';
|
||||
import {createSignalFromFunction, ReactiveNode, SIGNAL, Signal} from '../../signals';
|
||||
import {QueryFlags} from '../interfaces/query';
|
||||
import {LView} from '../interfaces/view';
|
||||
import {createContentQueryInternal, createViewQueryInternal, loadQueryInternal, queryRefreshInternal} from '../query';
|
||||
import {getLView} from '../state';
|
||||
|
||||
export interface InternalQuerySignal {
|
||||
bindToQuery(queryIndex: number): void;
|
||||
}
|
||||
|
||||
abstract class QuerySignal<T> extends ReactiveNode {
|
||||
private _lView?: LView;
|
||||
private _queryIndex?: number;
|
||||
protected queryList?: QueryList<T>;
|
||||
|
||||
protected override consumerAllowSignalWrites = false;
|
||||
|
||||
protected override onConsumerDependencyMayHaveChanged(): void {
|
||||
// This never happens for query signals as they're not consumers.
|
||||
}
|
||||
protected override onProducerUpdateValueVersion(): void {
|
||||
if (this.queryList === undefined || !this.queryList?.dirty) {
|
||||
// The current value and its version are already up to date.
|
||||
return;
|
||||
}
|
||||
|
||||
// The current value is stale. Check whether we need to produce a new one.
|
||||
// TODO: assert: I've got both the lView and queryIndex stored
|
||||
if (queryRefreshInternal(this._lView!, this._queryIndex!)) {
|
||||
this.valueVersion++;
|
||||
}
|
||||
}
|
||||
|
||||
bindToQuery(queryIndex: number) {
|
||||
// TODO: assert: should bind only once, make sure it is not re-assigned again
|
||||
this._lView = getLView();
|
||||
this._queryIndex = queryIndex;
|
||||
this.queryList = loadQueryInternal(this._lView, queryIndex);
|
||||
|
||||
this.queryList.onDirty(() => {
|
||||
// Notify any consumers about the potential change. Note that the onDirty callback will fire
|
||||
// only on the initial dirty marking (that is, subsequent dirty notifications are not fired -
|
||||
// until the QueryList becomes clean again).
|
||||
this.producerMayHaveChanged();
|
||||
});
|
||||
}
|
||||
|
||||
protected signalInternal(): void {
|
||||
// Check if the value needs updating before returning it.
|
||||
this.onProducerUpdateValueVersion();
|
||||
|
||||
// Record that someone looked at this signal.
|
||||
this.producerAccessed();
|
||||
}
|
||||
}
|
||||
|
||||
export class ChildQuerySignalImpl<T> extends QuerySignal<T> implements InternalQuerySignal {
|
||||
signal(): T|undefined {
|
||||
this.signalInternal();
|
||||
return this.queryList?.first;
|
||||
}
|
||||
}
|
||||
|
||||
export class ChildrenQuerySignalImpl<T> extends QuerySignal<T> implements InternalQuerySignal {
|
||||
signal(): T[] {
|
||||
this.signalInternal();
|
||||
// TODO: perf - I should not be obliged to create a new array every time we call signal()
|
||||
return this.queryList?.toArray() ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
// THINK: code duplication for predicate, flags etc.? Or would it be extracted by the compiler?
|
||||
export function ɵɵviewQueryCreate<T>(
|
||||
target: Signal<T|undefined>, predicate: ProviderToken<T>|string[], flags: QueryFlags,
|
||||
read?: any) {
|
||||
const lView = getLView();
|
||||
const reactiveQueryNode = target[SIGNAL] as InternalQuerySignal;
|
||||
reactiveQueryNode.bindToQuery(createViewQueryInternal<T>(lView, predicate, flags, read));
|
||||
}
|
||||
|
||||
// Q: assuming that the return type must be similar to InputSignal, with the write ability? (this is
|
||||
// needed only from the generated code so maybe not?)
|
||||
export function viewChild<T>(
|
||||
selector: ProviderToken<T>|string, opts?: {read?: any, static?: boolean}): Signal<T|undefined> {
|
||||
const node = new ChildQuerySignalImpl();
|
||||
return createSignalFromFunction<T|undefined>(node, node.signal.bind(node) as Signal<T|undefined>);
|
||||
}
|
||||
|
||||
export function viewChildren<T>(
|
||||
selector: ProviderToken<T>|string,
|
||||
opts?: {read?: any, emitDistinctChangesOnly?: boolean}): Signal<T[]> {
|
||||
// Q: by returning a signal we are effectively "dropping" QueryList from the public API. Is there
|
||||
// anything valuable there that we would be losing?
|
||||
const node = new ChildrenQuerySignalImpl();
|
||||
return createSignalFromFunction<T[]>(node, node.signal.bind(node) as Signal<T[]>);
|
||||
}
|
||||
|
||||
export function ɵɵcontentQueryCreate<T>(
|
||||
target: Signal<T|undefined>, dirIndex: number, predicate: ProviderToken<T>|string[],
|
||||
flags: QueryFlags, read?: any) {
|
||||
const lView = getLView();
|
||||
const reactiveQueryNode = target[SIGNAL] as InternalQuerySignal;
|
||||
// Q: why do we need the directive index?
|
||||
reactiveQueryNode.bindToQuery(
|
||||
createContentQueryInternal<T>(lView, dirIndex, predicate, flags, read));
|
||||
}
|
||||
|
||||
export function contentChild<T>(
|
||||
selector: ProviderToken<T>|string,
|
||||
opts?: {descendants?: boolean, read?: any, static?: boolean}): Signal<T|undefined> {
|
||||
const node = new ChildQuerySignalImpl();
|
||||
return createSignalFromFunction<T|undefined>(node, node.signal.bind(node) as Signal<T|undefined>);
|
||||
}
|
||||
|
||||
export function contentChildren<T>(
|
||||
selector: ProviderToken<T>|string,
|
||||
opts?: {descendants?: boolean, read?: any, emitDistinctChangesOnly?: boolean}): Signal<T[]> {
|
||||
// Q: by returning a signal we are effectively "dropping" QueryList from the public API. Is there
|
||||
// anything valuable there that we would be loosing?
|
||||
const node = new ChildrenQuerySignalImpl();
|
||||
return createSignalFromFunction<T[]>(node, node.signal.bind(node) as Signal<T[]>);
|
||||
}
|
||||
@@ -119,6 +119,8 @@ interface LFrame {
|
||||
*/
|
||||
currentDirectiveIndex: number;
|
||||
|
||||
currentVirtualInstruction: number;
|
||||
|
||||
/**
|
||||
* Are we currently in i18n block as denoted by `ɵɵelementStart` and `ɵɵelementEnd`.
|
||||
*
|
||||
@@ -421,6 +423,14 @@ export function nextBindingIndex(): number {
|
||||
return instructionState.lFrame.bindingIndex++;
|
||||
}
|
||||
|
||||
export function getVirtualInstructionIndex(): number {
|
||||
return instructionState.lFrame.currentVirtualInstruction;
|
||||
}
|
||||
|
||||
export function incrementVirtualInstructionIndex(): number {
|
||||
return ++instructionState.lFrame.currentVirtualInstruction;
|
||||
}
|
||||
|
||||
export function incrementBindingIndex(count: number): number {
|
||||
const lFrame = instructionState.lFrame;
|
||||
const index = lFrame.bindingIndex;
|
||||
@@ -637,6 +647,7 @@ function createLFrame(parent: LFrame|null): LFrame {
|
||||
currentDirectiveIndex: -1,
|
||||
bindingRootIndex: -1,
|
||||
bindingIndex: -1,
|
||||
currentVirtualInstruction: 0,
|
||||
currentQueryIndex: 0,
|
||||
parent: parent!,
|
||||
child: null,
|
||||
@@ -687,6 +698,7 @@ export function leaveView() {
|
||||
oldLFrame.contextLView = null;
|
||||
oldLFrame.elementDepthCount = 0;
|
||||
oldLFrame.currentDirectiveIndex = -1;
|
||||
oldLFrame.currentVirtualInstruction = 0;
|
||||
oldLFrame.currentNamespace = null;
|
||||
oldLFrame.bindingRootIndex = -1;
|
||||
oldLFrame.bindingIndex = -1;
|
||||
|
||||
@@ -26,8 +26,10 @@ export function computeStaticStyling(
|
||||
tNode: TNode, attrs: TAttributes|null, writeToHost: boolean): void {
|
||||
ngDevMode &&
|
||||
assertFirstCreatePass(getTView(), 'Expecting to be called in first template pass only');
|
||||
let styles: string|null = writeToHost ? tNode.styles : null;
|
||||
let classes: string|null = writeToHost ? tNode.classes : null;
|
||||
// Was `toWriteHost` here useful? sounds like `tNode.styles/classes` would always be empty anyway.
|
||||
// This is not called multiple times and actually initializes `tNode.classes/styles`.
|
||||
let styles: string|null = null;
|
||||
let classes: string|null = null;
|
||||
let mode: AttributeMarker|0 = 0;
|
||||
if (attrs !== null) {
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export {createSignalFromFunction, defaultEquals, isSignal, Signal, ValueEqualityFn} from './src/api';
|
||||
export {computed, CreateComputedOptions} from './src/computed';
|
||||
export {createSignalFromFunction, defaultEquals, isSignal, Signal, SIGNAL, ValueEqualityFn} from './src/api';
|
||||
export {computed, ComputedImpl, COMPUTING, CreateComputedOptions, ERRORED, UNSET} from './src/computed';
|
||||
export {setThrowInvalidWriteToSignalError} from './src/errors';
|
||||
export {ReactiveNode, setActiveConsumer} from './src/graph';
|
||||
export {CreateSignalOptions, setPostSignalSetFn, signal, WritableSignal} from './src/signal';
|
||||
|
||||
@@ -13,7 +13,7 @@ import {ReactiveNode} from './graph';
|
||||
*
|
||||
* This can be used to auto-unwrap signals in various cases, or to auto-wrap non-signal values.
|
||||
*/
|
||||
const SIGNAL = Symbol('SIGNAL');
|
||||
export const SIGNAL = Symbol('SIGNAL');
|
||||
|
||||
/**
|
||||
* A reactive value which notifies consumers of any changes.
|
||||
|
||||
@@ -39,29 +39,30 @@ export function computed<T>(computation: () => T, options?: CreateComputedOption
|
||||
* A dedicated symbol used before a computed value has been calculated for the first time.
|
||||
* Explicitly typed as `any` so we can use it as signal's value.
|
||||
*/
|
||||
const UNSET: any = Symbol('UNSET');
|
||||
export const UNSET: any = Symbol('UNSET');
|
||||
|
||||
/**
|
||||
* A dedicated symbol used in place of a computed signal value to indicate that a given computation
|
||||
* is in progress. Used to detect cycles in computation chains.
|
||||
* Explicitly typed as `any` so we can use it as signal's value.
|
||||
*/
|
||||
const COMPUTING: any = Symbol('COMPUTING');
|
||||
export const COMPUTING: any = Symbol('COMPUTING');
|
||||
|
||||
/**
|
||||
* A dedicated symbol used in place of a computed signal value to indicate that a given computation
|
||||
* failed. The thrown error is cached until the computation gets dirty again.
|
||||
* Explicitly typed as `any` so we can use it as signal's value.
|
||||
*/
|
||||
const ERRORED: any = Symbol('ERRORED');
|
||||
export const ERRORED: any = Symbol('ERRORED');
|
||||
|
||||
/**
|
||||
* A computation, which derives a value from a declarative reactive expression.
|
||||
*
|
||||
* `Computed`s are both producers and consumers of reactivity.
|
||||
*/
|
||||
class ComputedImpl<T> extends ReactiveNode {
|
||||
constructor(private computation: () => T, private equal: (oldValue: T, newValue: T) => boolean) {
|
||||
export class ComputedImpl<T> extends ReactiveNode {
|
||||
constructor(
|
||||
protected computation: () => T, protected equal: (oldValue: T, newValue: T) => boolean) {
|
||||
super();
|
||||
}
|
||||
/**
|
||||
@@ -69,13 +70,13 @@ class ComputedImpl<T> extends ReactiveNode {
|
||||
*
|
||||
* This can also be one of the special values `UNSET`, `COMPUTING`, or `ERRORED`.
|
||||
*/
|
||||
private value: T = UNSET;
|
||||
protected value: T = UNSET;
|
||||
|
||||
/**
|
||||
* If `value` is `ERRORED`, the error caught from the last computation attempt which will
|
||||
* be re-thrown.
|
||||
*/
|
||||
private error: unknown = null;
|
||||
protected error: unknown = null;
|
||||
|
||||
/**
|
||||
* Flag indicating that the computation is currently stale, meaning that one of the
|
||||
@@ -84,7 +85,7 @@ class ComputedImpl<T> extends ReactiveNode {
|
||||
* It's possible that no dependency has _actually_ changed, in which case the `stale`
|
||||
* state can be resolved without recomputing the value.
|
||||
*/
|
||||
private stale = true;
|
||||
protected stale = true;
|
||||
|
||||
protected override readonly consumerAllowSignalWrites = false;
|
||||
|
||||
@@ -123,7 +124,7 @@ class ComputedImpl<T> extends ReactiveNode {
|
||||
this.recomputeValue();
|
||||
}
|
||||
|
||||
private recomputeValue(): void {
|
||||
protected recomputeValue(): void {
|
||||
if (this.value === COMPUTING) {
|
||||
// Our computation somehow led to a cyclic read of itself.
|
||||
throw new Error('Detected cycle in computations.');
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
load("//tools:defaults.bzl", "jasmine_node_test", "karma_web_test_suite", "ts_library")
|
||||
load(":tests.bzl", "TEST_FILES", "assertTestsInSync")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
exports_files(
|
||||
TEST_FILES,
|
||||
visibility = ["//packages/core/test/acceptance/signal-components/aot:__pkg__"],
|
||||
)
|
||||
|
||||
assertTestsInSync(globbed_files = glob(["**/*.ts"]))
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = TEST_FILES,
|
||||
deps = [
|
||||
"//packages/compiler/src/template/pipeline/switch",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "test",
|
||||
bootstrap = ["//tools/testing:node"],
|
||||
tags = ["only-template-pipeline"],
|
||||
deps = [
|
||||
":test_lib",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test_web",
|
||||
tags = ["only-template-pipeline"],
|
||||
deps = [
|
||||
":test_lib",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
load("//packages/core/test/acceptance/signal-components:tests.bzl", "TEST_FILES")
|
||||
load("//tools:defaults.bzl", "jasmine_node_test", "karma_web_test_suite", "ng_module")
|
||||
|
||||
# Copy files over to avoid conflicts. We cannot define a `ng_module` and `ts_library`
|
||||
# in the same directory. So we copy test files over to be able to compile them with AOT.
|
||||
genrule(
|
||||
name = "test_sources",
|
||||
srcs = ["//packages/core/test/acceptance/signal-components:%s" % f for f in TEST_FILES],
|
||||
outs = [f for f in TEST_FILES],
|
||||
cmd = """cp $(SRCS) $(RULEDIR)""",
|
||||
)
|
||||
|
||||
ng_module(
|
||||
name = "test_lib_aot",
|
||||
testonly = True,
|
||||
srcs = [":test_sources"],
|
||||
strict_templates = True,
|
||||
tags = [
|
||||
"only-template-pipeline",
|
||||
],
|
||||
deps = [
|
||||
"//packages/compiler/src/template/pipeline/switch",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "test",
|
||||
bootstrap = ["//tools/testing:node"],
|
||||
tags = ["only-template-pipeline"],
|
||||
deps = [
|
||||
":test_lib_aot",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test_web",
|
||||
tags = ["only-template-pipeline"],
|
||||
deps = [
|
||||
":test_lib_aot",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @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 {USE_TEMPLATE_PIPELINE} from '@angular/compiler/src/template/pipeline/switch';
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
if (!USE_TEMPLATE_PIPELINE) {
|
||||
console.error(
|
||||
'ERROR: Cannot run this test target without: --//packages/compiler:use_template_pipeline');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
describe('dom property bindings in signal based components', () => {
|
||||
it('should bind to mapped property names', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<div [tabindex]="tIdx()"></div>`,
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
tIdx = signal(5);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const div = fixture.nativeElement.firstChild;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(div.tabIndex).toBe(5);
|
||||
});
|
||||
|
||||
it('should not update DOM binding if a binding is not reactive', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<div [tabindex]="nonReactive()">{{unrelated()}}</div>`,
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
idx = 0;
|
||||
nonReactive() {
|
||||
return this.idx++;
|
||||
}
|
||||
unrelated = signal('foo');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const cmpInstance = fixture.componentInstance;
|
||||
const div = fixture.nativeElement.firstChild;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(div.tabIndex).toBe(0);
|
||||
|
||||
cmpInstance.unrelated.set('bar');
|
||||
fixture.detectChanges();
|
||||
expect(div.tabIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @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 {USE_TEMPLATE_PIPELINE} from '@angular/compiler/src/template/pipeline/switch/index';
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
if (!USE_TEMPLATE_PIPELINE) {
|
||||
console.error(
|
||||
'ERROR: Cannot run this test target without: --//packages/compiler:use_template_pipeline');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
describe('Signal component DOM property interpolations', () => {
|
||||
it('should bind interpolated values', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<div title="Hello, {{name()}}!"></div>`,
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
name = signal('Angular');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const div = fixture.nativeElement.firstChild;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(div.title).toBe('Hello, Angular!');
|
||||
});
|
||||
|
||||
it('should support updating values', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<div title="Hello, {{name()}}!"></div>`,
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
name = signal('Angular');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const div = fixture.nativeElement.firstChild;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(div.title).toBe('Hello, Angular!');
|
||||
|
||||
fixture.componentInstance.name.set('New Name');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(div.title).toBe('Hello, New Name!');
|
||||
});
|
||||
|
||||
it('should render null and undefined as empty strings', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<div title="a{{null}}b{{undefined}}c">a{{null}}b{{undefined}}c</div>`,
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const div = fixture.nativeElement.firstChild;
|
||||
fixture.detectChanges();
|
||||
expect(div.title).toBe('abc');
|
||||
// verify that text bindings and interpolated binding rendering is consistent
|
||||
expect(div.title).toBe(div.textContent);
|
||||
});
|
||||
|
||||
it('should render NaN as-is', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<div title="a{{notANumber}}b">a{{notANumber}}b</div>`,
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
notANumber = NaN;
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const div = fixture.nativeElement.firstChild;
|
||||
fixture.detectChanges();
|
||||
expect(div.title).toBe('aNaNb');
|
||||
// verify that text bindings and interpolated binding rendering is consistent
|
||||
expect(div.title).toBe(div.textContent);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* @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 {USE_TEMPLATE_PIPELINE} from '@angular/compiler/src/template/pipeline/switch/index';
|
||||
import {Component, Directive, input, Input, signal} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
if (!USE_TEMPLATE_PIPELINE) {
|
||||
console.error(
|
||||
'ERROR: Cannot run this test target without: --//packages/compiler:use_template_pipeline');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
describe('Signal component inputs', () => {
|
||||
describe('input bindings from signal-based components', () => {
|
||||
describe('to signal-based targets', () => {
|
||||
@Component({
|
||||
selector: 'print',
|
||||
signals: true,
|
||||
template: `{{num()}}`,
|
||||
standalone: true,
|
||||
})
|
||||
class Print {
|
||||
@Input() num = input(0);
|
||||
}
|
||||
|
||||
it('should bind literal values', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<print [num]="3">`,
|
||||
imports: [Print],
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('3');
|
||||
});
|
||||
|
||||
|
||||
it('should bind signal values', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<print [num]="num()">`,
|
||||
imports: [Print],
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
num = signal(3);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('3');
|
||||
|
||||
fixture.componentInstance.num.set(4);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('4');
|
||||
});
|
||||
|
||||
it('should bind to multiple directives matching on the same node', () => {
|
||||
@Directive({
|
||||
selector: '[dir1]',
|
||||
standalone: true,
|
||||
signals: true,
|
||||
})
|
||||
class Dir1 {
|
||||
@Input() testInput = input('');
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[dir2]',
|
||||
standalone: true,
|
||||
signals: true,
|
||||
})
|
||||
class Dir2 {
|
||||
@Input() testInput = input('');
|
||||
}
|
||||
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<div dir1 dir2 [testInput]="name()"></div>`,
|
||||
imports: [Dir1, Dir2],
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
name = signal('Angular');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
const dir1Instance = fixture.debugElement.children[0].injector.get(Dir1);
|
||||
const dir2Instance = fixture.debugElement.children[0].injector.get(Dir2);
|
||||
|
||||
expect(dir1Instance.testInput()).toBe('Angular');
|
||||
expect(dir2Instance.testInput()).toBe('Angular');
|
||||
|
||||
fixture.componentInstance.name.set('Reactive Angular');
|
||||
expect(dir1Instance.testInput()).toBe('Reactive Angular');
|
||||
expect(dir2Instance.testInput()).toBe('Reactive Angular');
|
||||
});
|
||||
|
||||
it('should bind inputs when a target shows up multiple times in a template', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<print [num]="num()"></print>:<print [num]="num() * 2"></print>
|
||||
`,
|
||||
imports: [Print],
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
num = signal(3);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('3:6');
|
||||
|
||||
fixture.componentInstance.num.set(4);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('4:8');
|
||||
});
|
||||
|
||||
it('should support usage of the same input in different views', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
selector: 'uses-print',
|
||||
template: `<print [num]="num()"></print>`,
|
||||
imports: [Print],
|
||||
standalone: true,
|
||||
})
|
||||
class UsesPrint {
|
||||
num = signal(3);
|
||||
}
|
||||
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<uses-print/>:<uses-print/>`,
|
||||
imports: [UsesPrint],
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('3:3');
|
||||
});
|
||||
|
||||
it('should evaluate expression only once when binding to multiple inputs with the same name',
|
||||
() => {
|
||||
@Directive({
|
||||
selector: '[dir1]',
|
||||
standalone: true,
|
||||
signals: true,
|
||||
})
|
||||
class Dir1 {
|
||||
@Input() testInputArr = input<string[]>({initialValue: []});
|
||||
@Input()
|
||||
testInputObj =
|
||||
input<{foo: string, bar: string}>({initialValue: {foo: 'before', bar: 'before'}});
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[dir2]',
|
||||
standalone: true,
|
||||
signals: true,
|
||||
})
|
||||
class Dir2 {
|
||||
@Input() testInputArr = input<string[]>({initialValue: []});
|
||||
@Input()
|
||||
testInputObj =
|
||||
input<{foo: string, bar: string}>({initialValue: {foo: 'before', bar: 'before'}});
|
||||
}
|
||||
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `
|
||||
<div dir1 dir2
|
||||
[testInputArr]="['foo', 'bar']"
|
||||
[testInputObj]="{foo: 'foo', bar: 'bar'}"
|
||||
></div>`,
|
||||
imports: [Dir1, Dir2],
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
const dir1Instance = fixture.debugElement.children[0].injector.get(Dir1);
|
||||
const dir2Instance = fixture.debugElement.children[0].injector.get(Dir2);
|
||||
|
||||
expect(dir1Instance.testInputArr()).toEqual(['foo', 'bar']);
|
||||
expect(dir2Instance.testInputArr()).toEqual(['foo', 'bar']);
|
||||
expect(dir1Instance.testInputArr()).toBe(dir2Instance.testInputArr());
|
||||
|
||||
expect(dir1Instance.testInputObj()).toEqual({foo: 'foo', bar: 'bar'});
|
||||
expect(dir2Instance.testInputObj()).toEqual({foo: 'foo', bar: 'bar'});
|
||||
expect(dir1Instance.testInputObj()).toBe(dir2Instance.testInputObj());
|
||||
});
|
||||
|
||||
it('should be able to bind a variable', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<print [num]="varFromCtx" />|<print [num]="varFromCtx2()" />
|
||||
`,
|
||||
signals: true,
|
||||
standalone: true,
|
||||
imports: [Print],
|
||||
})
|
||||
class App {
|
||||
varFromCtx = 1;
|
||||
varFromCtx2 = signal(3);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('1|3');
|
||||
|
||||
fixture.componentInstance.varFromCtx2.set(4);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('1|4');
|
||||
});
|
||||
|
||||
it('should not evaluate expression if no signal is consumed', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<print [num]="varFromCtx" />|<print [num]="varFromCtx2()" />
|
||||
`,
|
||||
signals: true,
|
||||
standalone: true,
|
||||
imports: [Print],
|
||||
})
|
||||
class App {
|
||||
varFromCtx = 1;
|
||||
varFromCtx2 = signal(3);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('1|3');
|
||||
|
||||
// Even though we change the value here, the `Print#num`
|
||||
// binding is not refreshed because no signal was consumed.
|
||||
fixture.componentInstance.varFromCtx = 2;
|
||||
fixture.componentInstance.varFromCtx2.set(4);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('1|4');
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to pass local refs as inputs in expressions', () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
signals: true,
|
||||
template: '{{testInput() != null}}',
|
||||
selector: 'test-cmp',
|
||||
})
|
||||
class TestComponent {
|
||||
@Input() testInput = input<HTMLElement>({required: true});
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div #d></div>
|
||||
<test-cmp [testInput]="d" />
|
||||
`,
|
||||
signals: true,
|
||||
standalone: true,
|
||||
imports: [TestComponent],
|
||||
})
|
||||
class App {
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('true');
|
||||
});
|
||||
|
||||
it('should be able to pass forward local refs as inputs in expressions', () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
signals: true,
|
||||
template: '{{testInput() != null}}',
|
||||
selector: 'test-cmp',
|
||||
})
|
||||
class TestComponent {
|
||||
@Input() testInput = input<HTMLElement>({required: true});
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<test-cmp [testInput]="d" />
|
||||
<div #d></div>
|
||||
`,
|
||||
signals: true,
|
||||
standalone: true,
|
||||
imports: [TestComponent],
|
||||
})
|
||||
class App {
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('to zone-based targets', () => {
|
||||
it('works', () => {});
|
||||
});
|
||||
|
||||
describe('to a mix of zone-based and signal-based targets', () => {
|
||||
it('should evaluate expression only once when binding to multiple inputs with the same name',
|
||||
() => {
|
||||
@Directive({
|
||||
selector: '[dirZone]',
|
||||
standalone: true,
|
||||
signals: false,
|
||||
})
|
||||
class DirZone {
|
||||
@Input() testInputArr: string[] = [];
|
||||
@Input() testInputObj = {foo: 'before', bar: 'before'};
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[dirSignal]',
|
||||
standalone: true,
|
||||
signals: true,
|
||||
})
|
||||
class DirSignal {
|
||||
@Input() testInputArr = input<string[]>({initialValue: []});
|
||||
@Input()
|
||||
testInputObj =
|
||||
input<{foo: string, bar: string}>({initialValue: {foo: 'before', bar: 'before'}});
|
||||
}
|
||||
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `
|
||||
<div dirZone dirSignal
|
||||
[testInputArr]="['foo', 'bar']"
|
||||
[testInputObj]="{foo: 'foo', bar: 'bar'}"
|
||||
></div>`,
|
||||
imports: [DirZone, DirSignal],
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
const dirZoneInstance = fixture.debugElement.children[0].injector.get(DirZone);
|
||||
const dirSignalInstance = fixture.debugElement.children[0].injector.get(DirSignal);
|
||||
|
||||
expect(dirZoneInstance.testInputArr).toEqual(['foo', 'bar']);
|
||||
expect(dirSignalInstance.testInputArr()).toEqual(['foo', 'bar']);
|
||||
expect(dirZoneInstance.testInputArr).toBe(dirSignalInstance.testInputArr());
|
||||
|
||||
expect(dirZoneInstance.testInputObj).toEqual({foo: 'foo', bar: 'bar'});
|
||||
expect(dirSignalInstance.testInputObj()).toEqual({foo: 'foo', bar: 'bar'});
|
||||
expect(dirZoneInstance.testInputObj).toBe(dirSignalInstance.testInputObj());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @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 {USE_TEMPLATE_PIPELINE} from '@angular/compiler/src/template/pipeline/switch/index';
|
||||
import {Component, input, Input, signal} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
if (!USE_TEMPLATE_PIPELINE) {
|
||||
console.error(
|
||||
'ERROR: Cannot run this test target without: --//packages/compiler:use_template_pipeline');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
describe('Signal component input interpolations', () => {
|
||||
@Component({
|
||||
selector: 'print',
|
||||
signals: true,
|
||||
template: `{{text()}}`,
|
||||
standalone: true,
|
||||
})
|
||||
class Print {
|
||||
@Input() text = input('');
|
||||
}
|
||||
|
||||
it('should bind interpolated values', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<print text="Hello, {{name()}}!">`,
|
||||
imports: [Print],
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
name = signal('Angular');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('Hello, Angular!');
|
||||
});
|
||||
|
||||
it('should allow updating interpolated values', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
template: `<print text="Hello, {{name()}}!">`,
|
||||
imports: [Print],
|
||||
standalone: true,
|
||||
})
|
||||
class App {
|
||||
name = signal('Angular');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('Hello, Angular!');
|
||||
|
||||
fixture.componentInstance.name.set('New Name');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toBe('Hello, New Name!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @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 {Component, signal} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
describe('listeners', () => {
|
||||
it('should invoke listeners that modify context data', () => {
|
||||
@Component({
|
||||
signals: true,
|
||||
standalone: true,
|
||||
template: `<button (click)="counter.set(1)">{{counter()}}</button>`,
|
||||
})
|
||||
class App {
|
||||
counter = signal(0);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('0');
|
||||
|
||||
fixture.nativeElement.firstChild.click();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
// todo: listener restore view was missing in template pipeline
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user