Compare commits

...

79 Commits

Author SHA1 Message Date
Paul Gschwendtner 836530fdfd test: update symbol golden for defer
The golden JSON file is from  and doesn't support the virtual instruction
logic we've added in the signals branch. Surprisingly, this shows that something
is wrong with this test.
2023-10-13 10:37:48 +00:00
Paul Gschwendtner ca81661c95 fix(core): fix property_create instruction breaking binding to Zone targets
The property create instruction is currently failing to bind to Zone
targets if there is no advance call before the virtual instruction
that selects the intended element.
2023-10-12 17:29:21 +00:00
Paul Gschwendtner 5bf5cac714 refactor(core): rework queries to work with new signal graph API 2023-10-12 17:29:21 +00:00
Paul Gschwendtner 70861909a7 refactor(core): add more signal todos for input signal 2023-10-12 17:28:05 +00:00
Paul Gschwendtner f54cd057ad ci: enable github action tests for signals branch 2023-10-12 17:28:05 +00:00
Paul Gschwendtner 4ae88dc9be test: update compliance signal tests after template pipeline fixes/changes
The template pipeline now properly extracts the attribute constants,
so the older compliance tests from the signal branch need to be updated
to account for this new fix.
2023-10-12 17:28:05 +00:00
Paul Gschwendtner c8ac2b6d1e test: setup signal acceptance tests to also run with AOT compilation
This allows us to test the template pipeline TS emit (where we already
caught a bug using this), and also allows us to test the proper
recognition of e.g. `input`, `output`, `viewChild` etc- while also
testing type-checking.
2023-10-12 17:28:05 +00:00
Paul Gschwendtner 8151dd3cb6 fix(compiler): do not use constants for colocated shallow references
Currently the TS generated output (not JIT), breaks at runtime because
we emit all variables using the `Final` modifier. i.e. constants.

This breaks shallow references when we run our acceptance signal
tests using AOT.
2023-10-12 17:28:03 +00:00
Paul Gschwendtner 51ce1d6228 feat(compiler): support public aliases for signal output API 2023-10-12 17:27:39 +00:00
Paul Gschwendtner f79b106dfc feat(compiler): support signal-based output API
* Detect and recognize outputs for type-checking and definition
  generation
* Add compliance tests & fake core infrastructure
* Add playground example
2023-10-12 17:27:39 +00:00
Paul Gschwendtner 7504f24fe2 test: rework and enable outputs test to verify functionality 2023-10-12 17:27:39 +00:00
Pawel Kozlowski 63084b6502 feat(core): add runtime support for outputs in signal components
Runtime implementation is very simple - it adds the output function that
acts as an alias to the existing EventEmitter class constructor.
2023-10-12 17:27:39 +00:00
Paul Gschwendtner 1200ae9c6e test: update partial output golden for content queries 2023-10-12 17:27:39 +00:00
Paul Gschwendtner 0dbccabdf9 feat(compiler): support signal based content queries
Compiler portion for supporting content queries in signal components.
This is based on the preparation work, for detecting such queries. See
previous commits.
2023-10-12 17:27:39 +00:00
Pawel Kozlowski 1d2f7545c5 feat(core): reactive content queries
Runtime part of the reactive content queries.
2023-10-12 17:27:39 +00:00
Pawel Kozlowski bf16d7e414 test(core): add test for QueryList.onDirty callback
Add a test verifying that the onDirty callback:
- does not fire initially;
- fires once when a query is marked dirty (even if there are
multiple setDirty notifications).
2023-10-12 17:27:39 +00:00
Pawel Kozlowski 6106fcf4c7 fix(core): adjust reactive query types
Improvements to the types signature in viewChild / viewChildren.
2023-10-12 17:27:39 +00:00
Paul Gschwendtner 3e01740e43 feat(compiler): support generating queries for signal components 2023-10-12 17:27:39 +00:00
Pawel Kozlowski 968700c5f9 refactor(core): rename queryCreate to viewQueryCreate
In the end we need to distinguish between view and content query
creation and need separate names for those instructions.
2023-10-12 17:27:39 +00:00
Paul Gschwendtner f108800804 test: update api golden for core for new query APIs 2023-10-12 17:27:39 +00:00
Pawel Kozlowski c77976d9aa feat(core): reactive queries
Initial runtime implementation of the reactive view queries
(in preparation for the compiler work).
2023-10-12 17:27:39 +00:00
Pawel Kozlowski 2c754081e1 fix(core): fix stringify logic for interpolated values
Make sure that null, undefined and NaN interpolated values
are rendered consistently with the current ivy approach:
- null and undefined are rendered as empty strings;
- NaN is rendered as-is.
2023-10-12 17:27:38 +00:00
Paul Gschwendtner 1a2a6659a0 feat: support property interpolation in signal components 2023-10-12 17:27:38 +00:00
Pawel Kozlowski 1067f8a439 test: fix signal tests after property binding changes
Property bindings are wrapped into computed now.
2023-10-12 17:27:38 +00:00
Pawel Kozlowski e94b474b96 refactor(core): fix lint issues
Fixing lint issues that I've just introduced...
2023-10-12 17:27:38 +00:00
Pawel Kozlowski 34f002db7e feat(core): introduce property interpolation and make DOM property bindings reactive
DOM bindings are wrapped into computed now so their values
are memoized. As the result expression in DOM bindings are
re-evaluated when a component is change-detected.
2023-10-12 17:27:38 +00:00
Paul Gschwendtner a0af416ca1 fix: variable optimization preserving unused nextContext calls due to listeners
With the recent changes for local refs, we now also generate all
variables in the create block.

This works overall, but we end up with incorrect variable optimization
because calls of `nextContext` in listener functions end up tricking
the optimization logic into thinking that the outer `nextContext` calls
in the create block are _relevant_ for the listener function body.

e.g.

```
if (create) {
  const _bla = nextContext();
  i0.listener(() => {
    restoreView();
    const _ctx1 = nextContext();
    /* do somth with _ctx1 */
  });
}
```

`_bla` can safely be removed here, even though a `ContextRead/Write` has
occurred inside the listener operation. We process listeners separately.
2023-10-12 17:27:38 +00:00
Paul Gschwendtner 6dc61179ca fix: do not generate pure functions for listeners
We recently changed the pure function code to look for
all operations in a view. This ended up causing pure functions
to be generated for expressions in listeners. This might be fine
but diverges from the existing TDB and breaks compliance tests.

Culprit commit: https://github.com/angular/angular/commit/a61e41c88049eb12a0b23b1e7fa49a16b0eb91d7
2023-10-12 17:27:38 +00:00
Paul Gschwendtner c09cace82b WIP: Implement type-checking for signal inputs
New implementation of type-checking for signal inputs. Much simpler, and
without causing signficant type check block changes that would require
more changes to the language service completion logic / or rewrites of
more than 50+ handwritten code output tests for type check blocks.

See previous commits for other explored solutions.
2023-10-12 17:27:38 +00:00
Paul Gschwendtner 6840f93f37 WIP: Revert initial type-checking approach and completion adjustments
We will attempt a new solution that is not as invasive- and integrates
better with the constraints of the language service.
2023-10-12 17:27:38 +00:00
Paul Gschwendtner 3f5e3001f4 WIP: fixing tests and completion with signal based type checking
NOTE: This is very experimental and manual type check block code is not
updated. All of this complexity arises from the fact of attempting to
support bindings where they go to the same directive with multiple
inputs of the same name.

Potentially we can deprecate this behavior for input signal based
components and avoid this complexity. Alternative solution is to simply
repeat expression diagnostics if a binding goes to two same fields.
2023-10-12 17:27:38 +00:00
Pawel Kozlowski e5e405ee6e refactor(compiler): add required file header
Add required file header.
2023-10-12 17:27:37 +00:00
Pawel Kozlowski b263c00d36 test: add basic test for event listeners
Adding a passing test for a listener invocation
that changes signal value.
2023-10-12 17:27:20 +00:00
Pawel Kozlowski 3bd2fcd044 Revert "feat(core): propertyInterpolationCreate"
This reverts commit 2e3fe0747990647c5cfbba5429e90ed621b18e6e.

Based on the recent design discussion we are going to express property
interpolation as a computed function and reuse the propertyCreate
infrastructure.
2023-10-12 17:27:20 +00:00
Alex Rickabaugh 7feda88abb wip: support local references in signal property bindings
This commit adds support for local refs in signal property bindings, by
generating variables in the creation block. To implement this support:

* a new instruction shallowReference() is introduced, which is not tied to
  the current view context.
* variables are now generated for creation mode as well as update mode
* a new template pipeline pass (`phaseCreationVarColocation`) handles
  ordering of reference variable declaration and assignment in the creation
  block (guaranteeing references are only read after declarations).
2023-10-12 17:27:20 +00:00
Alex Rickabaugh f7265d34ac refactor(compiler): support o.BinaryExpression for assignments
Previously the compiler output AST did not support using
`o.BinaryExpression` for assignments (as TypeScript does). Instead
`o.WriteVarExpr` is defined, which only allows writing to string named
variables.
2023-10-12 17:26:52 +00:00
Paul Gschwendtner 5264c18b53 test: disable input signal initialization test
Still needs to be implemented and discussed.
2023-10-12 17:26:52 +00:00
Paul Gschwendtner 5ab3598746 test: update core API golden to add input API 2023-10-12 17:26:52 +00:00
Paul Gschwendtner f877b544a7 WIP: fix regressions in type checking after signal changes
There was an interesting issue that surfaced in the type checking
tests. Previously an input was type-checked using an expression like:

```ts
myDir.input = myrDir.input2 = userExpr
```

Note how the user expression was only specified once. With signals for
type-checking we cannot do this because both inputs cannot use the same
expression unless we extract it into a separate variable. This is what
this commit implemements.

```ts
const x1 = userExpr;
toWriteableSignal(myDir.input).set(x1);
toWriteableSignal(myDir.input2).set(x2);
```

Note: Even if no input is bound, due to e.g. the `inputs` decorator
field being used and no statically analyzable class member being
discoverable- the user expression should be *evaluated*. This was
something we broke with the initial signal input prototyping.
2023-10-12 17:26:52 +00:00
Pawel Kozlowski f5edc3e9a8 feat(core): propertyInterpolationCreate
Skeleton of the propertyInterpolationCreate instruction.
2023-10-12 17:26:52 +00:00
Paul Gschwendtner 9bf377a424 build: fix lint errors 2023-10-12 17:26:52 +00:00
Pawel Kozlowski 0eced157a9 test: add failing tests for property interpolation
Adding failing tests for the property interpolation so we
keep those instructions on our radar.
2023-10-12 17:26:52 +00:00
Pawel Kozlowski df4196e63f refactor: fix lint issue
Minor refactor to fix the linting job.
2023-10-12 17:26:52 +00:00
Pawel Kozlowski 5dea0312d6 test: binding to a mix of zone and signal inputs
This commit adds a test for a case of a binding targetting
multiple inputs with the same name. Test passes for both
signal based components as well as a mix of zone and signal
based components.
2023-10-12 17:26:52 +00:00
Pawel Kozlowski 7b15e261ad test: enable passing tests after recent code changes
Enabling newly passing tests.
2023-10-12 17:26:51 +00:00
Alex Rickabaugh a59dcb52bd wip: one calculation for many input targets
Wrap bound expressions in `computed` where necessary to ensure all consumers
of a binding see the same value.
2023-10-12 17:26:51 +00:00
Paul Gschwendtner 6cd5dfa966 test: support signal library in compliance test fake_core package
Compliance tests do not depend on the real typings for
`@angular/core`, but rather a fake core package is used.

We want to be able to write compliance tests using signals, so we should
add some rough types to the fake core package. This will also be useful
in `fake_core` for type checking tests.
2023-10-12 17:26:51 +00:00
Paul Gschwendtner 809a96e65d test: generate partial golden for signal compliance test 2023-10-12 17:26:51 +00:00
Paul Gschwendtner 2c6154fe14 test: ensure signal compliance test only runs with template pipeline 2023-10-12 17:26:51 +00:00
Paul Gschwendtner bdbb76f344 refactor: remove debug print for type check block 2023-10-12 17:26:51 +00:00
Paul Gschwendtner 24d689730d test: add test to ensure variable bindings work 2023-10-12 17:26:51 +00:00
Paul Gschwendtner 6310ccc221 WIP: prepare tests for failing scenarios discovered during collab
* Local refs needs more testing
* Basic variable read failed
* We do not have any tests for listeners? there was some restore view
  issue: https://github.com/angular/angular/commit/3acfdf4861d5eeed5b2ad42978547da929cc3567
2023-10-12 17:26:51 +00:00
Pawel Kozlowski faf5b21275 fix(core): properly bind to the same input from multiple views
Before this fix bindings to the same input from multiple LViews didn't
work properly as the binding expression was assigned to LView only on
the first create pass (instead of assigning on each durective usage pass).
2023-10-12 17:26:51 +00:00
Alex Rickabaugh 317ddb0d00 wip: allocate vars for PropertyCreateOp
This commit adds the `ConsumesVarsTrait` to `PropertyCreateOp` which causes
vars to be counted for use in the virtual instructions it might add. This
allows the DOM test to pass.
2023-10-12 17:26:51 +00:00
Pawel Kozlowski b7f33034e8 test: failing test case for non-primitive literals in a template 2023-10-12 17:26:51 +00:00
Pawel Kozlowski a79e202f33 test: add test veryfing bindings to multiple directives 2023-10-12 17:26:51 +00:00
Pawel Kozlowski 92f4d0127f test: add failing test for dom property bindings
The test is skipped as we are not propagating number of vars
based on the bindings found in a template.
2023-10-12 17:26:51 +00:00
Pawel Kozlowski 43cf37aa3e test: re-organize tests 2023-10-12 17:26:51 +00:00
JoostK 41db51bf91 test: add test to bind a signal to a signal input 2023-10-12 17:26:51 +00:00
JoostK 1ae67024cb test: allow signal component test to pass 2023-10-12 17:26:51 +00:00
Alex Rickabaugh b04e75a175 refactor(compiler): only reference a view via a variable if it's not ctx
Previously the template pipeline would prefer to use a local variable
definition in favor of ctx. This commit improves the logic to prefer `ctx`
for the root view even if a variable for the context is available.
2023-10-12 17:26:50 +00:00
Alex Rickabaugh d56cbc2fdf wip: bypass pure functions in input computations 2023-10-12 17:26:50 +00:00
Alex Rickabaugh 49cbecaeab wip: fix signals test
(required and initial values for inputs are mutually incompatible)
2023-10-12 17:26:50 +00:00
Alex Rickabaugh a6702e92aa wip: switch to the real input() implementation 2023-10-12 17:26:50 +00:00
Paul Gschwendtner dadd751a48 refactor: fix lint warnings 2023-10-12 17:26:50 +00:00
Paul Gschwendtner 48054443da refactor: wire up propertyCreate instruction for JIT generation 2023-10-12 17:26:50 +00:00
Paul Gschwendtner 84e0908b29 WIP: setup template pipeline acceptance tests on CI and base setup 2023-10-12 17:26:50 +00:00
Alex Rickabaugh 0f228885bf wip: working signal input and all 3 binding types 2023-10-12 17:26:50 +00:00
Alex Rickabaugh af93b126dd wip: specialize virtual instructions
* splits propertyCreate virtual instructions into DOM vs zone inputs
* integrates with `InputSignal` setting (untested)
* deletes old interfaces/virtual_property impl in favor of Paul's
2023-10-12 17:26:50 +00:00
Alex Rickabaugh f7408579a4 input signals 2023-10-12 17:26:50 +00:00
Paul Gschwendtner c00ab4bcea Fix that signals: true views are never running update mode 2023-10-12 17:26:49 +00:00
Paul Gschwendtner f0a92e2097 WIP: generate propertyCreate instructions in template pipeline 2023-10-12 17:26:49 +00:00
Paul Gschwendtner e35ac097be WIP: create new instruction "propertyCreate" 2023-10-12 17:26:49 +00:00
Paul Gschwendtner 647f57407b WIP: setup http server for test app to test runtime 2023-10-12 17:18:38 +00:00
Paul Gschwendtner 2bb73d1d09 WIP: type checking for signal inputs 2023-10-12 17:18:38 +00:00
Paul Gschwendtner ea48506f67 WIP: Discover input class members in directive shared code. No type-chechking yet. 2023-10-12 17:18:37 +00:00
Paul Gschwendtner d49aec75df WIP: implementation of input function signature 2023-10-12 17:18:37 +00:00
Paul Gschwendtner f050c5b19a WIP: initial input function and test app 2023-10-12 17:18:37 +00:00
Alex Rickabaugh 565032c038 wip: basic proof of concept for virtual instructions 2023-10-12 17:18:37 +00:00
134 changed files with 4171 additions and 220 deletions
+1
View File
@@ -4,6 +4,7 @@ on:
push:
branches:
- main
- signals
- '[0-9]+.[0-9]+.x'
pull_request:
types: [opened, synchronize, reopened]
@@ -77,6 +77,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,
+83 -1
View File
@@ -228,6 +228,7 @@ export interface Component extends Directive {
moduleId?: string;
preserveWhitespaces?: boolean;
schemas?: SchemaMetadata[];
signals?: boolean;
standalone?: boolean;
styles?: string | string[];
styleUrl?: string;
@@ -320,6 +321,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?: {
@@ -341,6 +349,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?: {
@@ -523,6 +538,7 @@ export interface Directive {
[key: string]: any;
};
selector?: string;
signals?: boolean;
standalone?: boolean;
}
@@ -862,6 +878,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;
@@ -869,6 +924,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;
@@ -988,6 +1048,9 @@ export enum MissingTranslationStrategy {
Warning = 1
}
// @public
export type ModelSignal<ReadT, WriteT> = InputSignal<ReadT, WriteT> & Pick<WritableSignal<WriteT>, 'set' | 'update'>;
// @public @deprecated
export class ModuleWithComponentFactories<T> {
constructor(ngModuleFactory: NgModuleFactory<T>, componentFactories: ComponentFactory<any>[]);
@@ -1131,6 +1194,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;
@@ -1232,7 +1301,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[];
@@ -1247,6 +1316,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;
@@ -1549,6 +1619,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?: {
@@ -1568,6 +1644,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?: {
@@ -4,6 +4,43 @@
```ts
// @public
export const COMPUTED_NODE: {
value: any;
dirty: boolean;
error: null;
equal: typeof defaultEquals;
producerMustRecompute(node: ComputedNode<unknown>): boolean;
producerRecomputeValue(node: ComputedNode<unknown>): void;
version: number & {
__brand: "Version";
};
producerNode: ReactiveNode[] | undefined;
producerLastReadVersion: (number & {
__brand: "Version";
})[] | undefined;
producerIndexOfThis: number[] | undefined;
nextProducerIndex: number;
liveConsumerNode: ReactiveNode[] | undefined;
liveConsumerIndexOfThis: number[] | undefined;
consumerAllowSignalWrites: boolean;
consumerIsAlwaysLive: boolean;
consumerMarkedDirty(node: unknown): void;
consumerOnSignalRead(node: unknown): void;
};
// @public
export interface ComputedNode<T> extends ReactiveNode {
computation: () => T;
// (undocumented)
equal: ValueEqualityFn<T>;
error: unknown;
value: T;
}
// @public
export const COMPUTING: any;
// @public
export function consumerAfterComputation(node: ReactiveNode | null, prevConsumer: ReactiveNode | null): void;
@@ -13,6 +50,9 @@ export function consumerBeforeComputation(node: ReactiveNode | null): ReactiveNo
// @public
export function consumerDestroy(node: ReactiveNode): void;
// @public (undocumented)
export function consumerMarkDirty(node: ReactiveNode): void;
// @public
export function createComputed<T>(computation: () => T): ComputedGetter<T>;
@@ -25,6 +65,9 @@ export function createWatch(fn: (onCleanup: WatchCleanupRegisterFn) => void, sch
// @public
export function defaultEquals<T>(a: T, b: T): boolean;
// @public
export const ERRORED: any;
// @public (undocumented)
export function getActiveConsumer(): ReactiveNode | null;
@@ -116,6 +159,9 @@ export function signalSetFn<T>(node: SignalNode<T>, newValue: T): void;
// @public (undocumented)
export function signalUpdateFn<T>(node: SignalNode<T>, updater: (value: T) => T): void;
// @public
export const UNSET: any;
// @public
export type ValueEqualityFn<T> = (a: T, b: T) => boolean;
+1 -1
View File
@@ -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);
}
@@ -77,17 +77,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,
@@ -96,7 +112,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(
@@ -105,7 +122,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 =
@@ -242,13 +260,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];
@@ -266,23 +284,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')) {
@@ -294,7 +313,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;
}
@@ -305,7 +325,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;
}
@@ -314,7 +334,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;
}
@@ -322,7 +342,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 {
@@ -415,6 +435,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): {
@@ -451,7 +494,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 {
@@ -558,7 +602,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);
});
}
@@ -771,8 +816,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];
@@ -904,6 +949,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`.
@@ -404,6 +404,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!;
}
@@ -262,7 +262,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, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstElement, TmplAstForLoopBlock, TmplAstHoverDeferredTrigger, TmplAstIcu, TmplAstIfBlock, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, TmplAstViewportDeferredTrigger, 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, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstElement, TmplAstForLoopBlock, TmplAstHoverDeferredTrigger, TmplAstIcu, TmplAstIfBlock, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, TmplAstViewportDeferredTrigger, TransplantedType} from '@angular/compiler';
import ts from 'typescript';
import {Reference} from '../../imports';
@@ -754,6 +754,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)) {
@@ -771,14 +773,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",
],
@@ -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>;
}
@@ -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"}]
}
]
}
]
}
@@ -0,0 +1,6 @@
SensorComp.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({
// ...
outputs: {
pressed: "pressed",
_internalName: "touched"
},
@@ -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'});
}
@@ -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>;
}
@@ -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"}]
}
]
}
]
}
@@ -0,0 +1,27 @@
template: function LiteralValueBinding_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelementStart(0, "button", 0);
i0.ɵɵpropertyCreate(1, "disabled", function () { return true; });
i0.ɵɵelementEnd();
}
}
// ...
template: function FromContextBindingStatic_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelementStart(0, "button", 0);
i0.ɵɵpropertyCreate(1, "disabled", function () { return ctx.isDisabled; });
i0.ɵɵelementEnd();
}
}
// ...
template: function FromContextBindingSignal_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelementStart(0, "button", 0);
i0.ɵɵpropertyCreate(1, "disabled", function () { return ctx.isDisabled(); });
i0.ɵɵelementEnd();
}
}
@@ -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);
}
@@ -0,0 +1,27 @@
template: function LiteralValueBinding_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelementStart(0, "button", 0);
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", 0);
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", 0);
i0.ɵɵpropertyCreate(1, "title", function () { return i0.ɵɵstringifyInterpolation`This is a ${ctx.type()} button`; });
i0.ɵɵelementEnd();
}
}
@@ -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');
}
@@ -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"}]
}
]
}
]
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
@@ -2045,7 +2045,7 @@ function allTests(os: string) {
}
`);
verifyThrownError(
ErrorCode.DECORATOR_ARG_NOT_LITERAL,
ErrorCode.QUERY_DEFINITION_ARG_NOT_LITERAL,
`@${decorator} options must be an object literal`);
});
@@ -2081,7 +2081,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`, () => {
@@ -451,6 +451,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,
@@ -249,6 +249,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};
@@ -389,4 +390,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};
}
+55 -18
View File
@@ -57,12 +57,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) => { ... }
@@ -70,7 +72,7 @@ function baseDirectiveFields(
'hostBindings',
createHostBindingsFunction(
meta.host, meta.typeSourceSpan, bindingParser, constantPool, meta.selector || '',
meta.name, definitionMap));
meta.name, meta.isSignal, definitionMap));
// e.g 'inputs: {a: 'a'}`
definitionMap.set('inputs', conditionallyCreateDirectiveBindingLiteral(meta.inputs, true));
@@ -235,7 +237,7 @@ export function compileComponentFromMetadata(
// This path compiles the template using the prototype template pipeline. First the template is
// ingested into IR:
const tpl = ingestComponent(
meta.name, meta.template.nodes, constantPool, meta.relativeContextFilePath,
meta.name, meta.isSignal, meta.template.nodes, constantPool, meta.relativeContextFilePath,
meta.i18nUseExternalIds);
// Then the IR is transformed to prepare it for cod egeneration.
@@ -432,12 +434,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)
@@ -455,16 +470,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 {
@@ -540,12 +560,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));
@@ -562,12 +593,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);
}
@@ -575,7 +611,7 @@ function createViewQueriesFunction(
function createHostBindingsFunction(
hostBindingsMetadata: R3HostMetadata, typeSourceSpan: ParseSourceSpan,
bindingParser: BindingParser, constantPool: ConstantPool, selector: string, name: string,
definitionMap: DefinitionMap): o.Expression|null {
isSignal: boolean, definitionMap: DefinitionMap): o.Expression|null {
const bindings =
bindingParser.createBoundHostProperties(hostBindingsMetadata.properties, typeSourceSpan);
@@ -599,12 +635,13 @@ function createHostBindingsFunction(
const hostJob = ingestHostBinding(
{
isSignal,
componentName: name,
properties: bindings,
events: eventBindings,
attributes: hostBindingsMetadata.attributes,
},
bindingParser, constantPool);
constantPool);
transform(hostJob, CompilationJobKind.Host);
definitionMap.set('hostAttrs', hostJob.root.attributes);
@@ -7,6 +7,7 @@
*/
export * from './src/enums';
export * from './src/interpolation';
export * from './src/expression';
export * from './src/operations';
export * from './src/ops/create';
@@ -100,6 +100,12 @@ export enum OpKind {
*/
Binding,
/**
* A placeholder operation reserving space for `Binding` operations that end up being
* moved into the creation block phase for signal components.
*/
BindingSignalPlaceholder,
/**
* An operation to bind an expression to a property of an element.
*/
@@ -209,6 +215,11 @@ export enum OpKind {
* An instruction that applies a set of i18n expressions.
*/
I18nApply,
/*
* TODO
*/
PropertyCreate,
}
/**
@@ -240,6 +251,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.
*/
@@ -308,6 +324,7 @@ export enum ExpressionKind {
/**
* A reference to a temporary variable.
*/
ReadTemporaryExpr,
/**
@@ -324,6 +341,12 @@ export enum ExpressionKind {
* A test expression for a conditional op.
*/
ConditionalCase,
/**
* An interpolation template string expression. Such template string expression is
* commonly expected to be part of the `PropertyCreate` operation expression.
*/
InterpolationTemplateExpr,
}
/**
@@ -8,23 +8,23 @@
import * as o from '../../../../output/output_ast';
import type {ParseSourceSpan} from '../../../../parse_util';
import * as t from '../../../../render3/r3_ast';
import {ExpressionKind, OpKind, SanitizerFn} from './enums';
import {ConsumesVarsTrait, UsesSlotIndex, UsesSlotIndexTrait, UsesVarOffset, UsesVarOffsetTrait} from './traits';
import {ExpressionKind, OpKind, SanitizerFn} from './enums';
import {Interpolation} from './interpolation';
import type {XrefId} from './operations';
import type {CreateOp} from './ops/create';
import {Interpolation, type UpdateOp} from './ops/update';
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|SanitizerExpr|SlotLiteralExpr|ConditionalCaseExpr;
export type Expression = LexicalReadExpr|ReferenceExpr|ShallowReferenceExpr|ContextExpr|
NextContextExpr|GetCurrentViewExpr|RestoreViewExpr|ResetViewExpr|ReadVariableExpr|
PureFunctionExpr|PureFunctionParameterExpr|PipeBindingExpr|PipeBindingVariadicExpr|
SafePropertyReadExpr|SafeKeyedReadExpr|SafeInvokeFunctionExpr|EmptyExpr|AssignTemporaryExpr|
ReadTemporaryExpr|SanitizerExpr|SlotLiteralExpr|ConditionalCaseExpr|InterpolationTemplateExpr;
/**
* Transformer type which converts expressions into general `o.Expression`s (which may be an
@@ -117,6 +117,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;
targetSlot: 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.targetSlot = this.targetSlot;
return expr;
}
}
/**
* A reference to the current view context (usually the `ctx` variable in a template function).
*/
@@ -630,6 +664,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;
@@ -852,6 +927,7 @@ export function transformExpressionsInOp(
}
break;
case OpKind.Property:
case OpKind.PropertyCreate:
case OpKind.Attribute:
if (op.expression instanceof Interpolation) {
transformExpressionsInInterpolation(op.expression, transform, flags);
@@ -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 * as o from '../../../../output/output_ast';
export class Interpolation {
constructor(readonly strings: string[], readonly expressions: o.Expression[]) {}
}
@@ -293,9 +293,6 @@ export class OpList<OpT extends Op<OpT>> {
op.debugListId = target.debugListId;
// Just in case.
op.prev = null;
target.prev!.next = op;
op.prev = target.prev;
@@ -6,25 +6,26 @@
* found in the LICENSE file at https://angular.io/license
*/
import {SecurityContext} from '../../../../../core';
import * as i18n from '../../../../../i18n/i18n_ast';
import * as o from '../../../../../output/output_ast';
import {ParseSourceSpan} from '../../../../../parse_util';
import {BindingKind, DeferSecondaryKind, OpKind} from '../enums';
import {Interpolation} from '../interpolation';
import {Op, OpList, XrefId} from '../operations';
import {ConsumesSlotOpTrait, HasConstTrait, TRAIT_CONSUMES_SLOT, TRAIT_HAS_CONST, TRAIT_USES_SLOT_INDEX, UsesSlotIndexTrait} from '../traits';
import {ConsumesSlotOpTrait, ConsumesVarsTrait, HasConstTrait, TRAIT_CONSUMES_SLOT, TRAIT_CONSUMES_VARS, TRAIT_HAS_CONST, TRAIT_USES_SLOT_INDEX, UsesSlotIndexTrait} from '../traits';
import {ListEndOp, NEW_OP, StatementOp, VariableOp} from './shared';
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|EnableBindingsOp|DisableBindingsOp|TextOp|ListenerOp|
PipeOp|VariableOp<CreateOp>|NamespaceOp|ProjectionDefOp|ProjectionOp|ExtractedAttributeOp|
DeferOp|DeferSecondaryBlockOp|DeferOnOp|ExtractedMessageOp|I18nOp|I18nStartOp|I18nEndOp;
export type CreateOp = ListEndOp<CreateOp>|StatementOp<CreateOp>|ElementOp|ElementStartOp|
ElementEndOp|ContainerOp|ContainerStartOp|ContainerEndOp|TemplateOp|EnableBindingsOp|
DisableBindingsOp|TextOp|ListenerOp|PipeOp|VariableOp<CreateOp>|NamespaceOp|ProjectionDefOp|
ProjectionOp|ExtractedAttributeOp|DeferOp|DeferSecondaryBlockOp|DeferOnOp|ExtractedMessageOp|
I18nOp|I18nStartOp|I18nEndOp|BindingSignalPlaceholder|PropertyCreateOp;
/**
* An operation representing the creation of an element or container.
@@ -775,6 +776,103 @@ export function createI18nEndOp(xref: XrefId): I18nEndOp {
};
}
/*
* A placeholder operation reserving space for `Binding` operations that end up being
* moved into the creation block phase for signal components.
*
* e.g. `Property` is moved into `PropertyCreate` from the update block to the
* creation block.
*/
export interface BindingSignalPlaceholder extends Op<CreateOp> {
kind: OpKind.BindingSignalPlaceholder;
/** Xref ID for the binding. */
bindingXref: XrefId;
}
/**
* Create a `BindingSignalPlaceholder`.
*/
export function createBindingSignalPlaceholderOp(bindingXref: XrefId): BindingSignalPlaceholder {
return {
kind: OpKind.BindingSignalPlaceholder,
bindingXref,
...NEW_OP,
};
}
export interface PropertyCreateOp extends Op<CreateOp>, ConsumesSlotOpTrait, ConsumesVarsTrait {
kind: OpKind.PropertyCreate;
/** Xref ID of the binding. Needed as we consume a slot for the expression to be stored. */
xref: XrefId;
/**
* 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.
*
* Note: Unlike `PropertyOp`, this does not accept `ir.Interpolation` as we
* expect interpolations to be transformed into interpolation template expressions.
*/
expression: o.Expression;
/**
* Whether this property is an animation trigger.
*/
isAnimationTrigger: boolean;
/**
* The security context of the binding.
*/
securityContext: SecurityContext;
/**
* The sanitizer for this property.
*/
sanitizer: o.Expression|null;
/**
* Whether this binding is on a template.
*/
isTemplate: boolean;
/** Source span describing the property binding. */
sourceSpan: ParseSourceSpan;
}
/**
* Create a `PropertyCreateOp`.
*/
export function createPropertyCreateOp(
bindingXref: XrefId, target: XrefId, name: string, expression: o.Expression,
isAnimationTrigger: boolean, securityContext: SecurityContext, isTemplate: boolean,
sourceSpan: ParseSourceSpan): PropertyCreateOp {
return {
kind: OpKind.PropertyCreate,
xref: bindingXref,
target,
name,
expression,
isAnimationTrigger,
securityContext,
sanitizer: null,
isTemplate,
sourceSpan,
...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.
@@ -9,12 +9,12 @@
import * as o from '../../../../../../src/output/output_ast';
import {ParseSourceSpan} from '../../../../../../src/parse_util';
import {OpKind} from '../enums';
import {Interpolation} from '../interpolation';
import {Op} from '../operations';
import {ConsumesVarsTrait, TRAIT_CONSUMES_VARS} from '../traits';
import {NEW_OP} from './shared';
import type {Interpolation, UpdateOp} from './update';
import type {UpdateOp} from './update';
/**
@@ -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,
};
}
@@ -12,6 +12,7 @@ import * as o from '../../../../../output/output_ast';
import {ParseSourceSpan} from '../../../../../parse_util';
import {BindingKind, OpKind} from '../enums';
import type {ConditionalCaseExpr} from '../expression';
import {Interpolation} from '../interpolation';
import {Op, XrefId} from '../operations';
import {ConsumesVarsTrait, DependsOnSlotContextOpTrait, TRAIT_CONSUMES_VARS, TRAIT_DEPENDS_ON_SLOT_CONTEXT, TRAIT_USES_SLOT_INDEX, UsesSlotIndexTrait} from '../traits';
@@ -72,10 +73,6 @@ export function createInterpolateTextOp(
};
}
export class Interpolation {
constructor(readonly strings: string[], readonly expressions: o.Expression[]) {}
}
/**
* An intermediate binding op, that has not yet been processed into an individual property,
* attribute, style, etc.
@@ -83,6 +80,9 @@ export class Interpolation {
export interface BindingOp extends Op<UpdateOp> {
kind: OpKind.Binding;
/** Xref ID for the binding. */
bindingXref: XrefId;
/**
* Reference to the element on which the property is bound.
*/
@@ -132,11 +132,12 @@ export interface BindingOp extends Op<UpdateOp> {
* Create a `BindingOp`, not yet transformed into a particular type of binding.
*/
export function createBindingOp(
target: XrefId, kind: BindingKind, name: string, expression: o.Expression|Interpolation,
unit: string|null, securityContext: SecurityContext, isTextAttribute: boolean,
isTemplate: boolean, sourceSpan: ParseSourceSpan): BindingOp {
bindingXref: XrefId, target: XrefId, kind: BindingKind, name: string,
expression: o.Expression|Interpolation, unit: string|null, securityContext: SecurityContext,
isTextAttribute: boolean, isTemplate: boolean, sourceSpan: ParseSourceSpan): BindingOp {
return {
kind: OpKind.Binding,
bindingXref,
bindingKind: kind,
target,
name,
@@ -156,6 +157,9 @@ export function createBindingOp(
export interface PropertyOp extends Op<UpdateOp>, ConsumesVarsTrait, DependsOnSlotContextOpTrait {
kind: OpKind.Property;
/** Xref ID for the binding. Used to reference the `BindingSignalPlaceholder`. */
bindingXref: XrefId;
/**
* Reference to the element on which the property is bound.
*/
@@ -198,12 +202,13 @@ export interface PropertyOp extends Op<UpdateOp>, ConsumesVarsTrait, DependsOnSl
* Create a `PropertyOp`.
*/
export function createPropertyOp(
target: XrefId, name: string, expression: o.Expression|Interpolation,
bindingXref: XrefId, target: XrefId, name: string, expression: o.Expression|Interpolation,
isAnimationTrigger: boolean, securityContext: SecurityContext, isTemplate: boolean,
sourceSpan: ParseSourceSpan): PropertyOp {
return {
kind: OpKind.Property,
bindingXref,
target,
name,
expression,
@@ -51,6 +51,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;
}
/**
@@ -22,7 +22,7 @@ export enum CompilationJobKind {
*/
export abstract class CompilationJob {
constructor(
readonly componentName: string, readonly pool: ConstantPool,
readonly componentName: string, readonly isSignal: boolean, readonly pool: ConstantPool,
readonly compatibility: ir.CompatibilityMode) {}
kind: CompilationJobKind = CompilationJobKind.Both;
@@ -63,9 +63,10 @@ export abstract class CompilationJob {
*/
export class ComponentCompilationJob extends CompilationJob {
constructor(
componentName: string, pool: ConstantPool, compatibility: ir.CompatibilityMode,
readonly relativeContextFilePath: string, readonly i18nUseExternalIds: boolean) {
super(componentName, pool, compatibility);
componentName: string, isSignal: boolean, pool: ConstantPool,
compatibility: ir.CompatibilityMode, readonly relativeContextFilePath: string,
readonly i18nUseExternalIds: boolean) {
super(componentName, isSignal, pool, compatibility);
this.root = new ViewCompilationUnit(this, this.allocateXrefId(), null);
this.views.set(this.root.xref, this.root);
}
@@ -213,8 +214,10 @@ export class ViewCompilationUnit extends CompilationUnit {
* Compilation-in-progress of a host binding, which contains a single unit for that host binding.
*/
export class HostBindingCompilationJob extends CompilationJob {
constructor(componentName: string, pool: ConstantPool, compatibility: ir.CompatibilityMode) {
super(componentName, pool, compatibility);
constructor(
componentName: string, isSignal: boolean, pool: ConstantPool,
compatibility: ir.CompatibilityMode) {
super(componentName, isSignal, pool, compatibility);
this.root = new HostBindingCompilationUnit(this);
}
@@ -11,7 +11,6 @@ import {ConstantPool} from '../../../constant_pool';
import * as ir from '../ir';
import {CompilationJob, CompilationJobKind as Kind, type ComponentCompilationJob, type HostBindingCompilationJob, type ViewCompilationUnit} from './compilation';
import {phaseAlignPipeVariadicVarOffset} from './phases/align_pipe_variadic_var_offset';
import {phaseFindAnyCasts} from './phases/any_cast';
import {phaseApplyI18nExpressions} from './phases/apply_i18n_expressions';
@@ -21,6 +20,7 @@ import {phaseBindingSpecialization} from './phases/binding_specialization';
import {phaseChaining} from './phases/chaining';
import {phaseConditionals} from './phases/conditionals';
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';
@@ -55,6 +55,7 @@ import {phaseResolveI18nPlaceholders} from './phases/resolve_i18n_placeholders';
import {phaseResolveNames} from './phases/resolve_names';
import {phaseResolveSanitizers} from './phases/resolve_sanitizers';
import {phaseSaveRestoreView} from './phases/save_restore_view';
import {phaseSignalBindings} from './phases/signal_bindings';
import {phaseSlotAllocation} from './phases/slot_allocation';
import {phaseStyleBindingSpecialization} from './phases/style_binding_specialization';
import {phaseTemporaryVariables} from './phases/temporary_variables';
@@ -78,10 +79,11 @@ const phases: Phase[] = [
{kind: Kind.Tmpl, fn: phaseNamespace},
{kind: Kind.Both, fn: phaseStyleBindingSpecialization},
{kind: Kind.Both, fn: phaseBindingSpecialization},
{kind: Kind.Tmpl, fn: phasePropagateI18nBlocks},
{kind: Kind.Both, fn: phaseSignalBindings},
{kind: Kind.Both, fn: phaseAttributeExtraction},
{kind: Kind.Both, fn: phaseParseExtractedStyles},
{kind: Kind.Tmpl, fn: phaseRemoveEmptyBindings},
{kind: Kind.Tmpl, fn: phasePropagateI18nBlocks},
{kind: Kind.Tmpl, fn: phaseConditionals},
{kind: Kind.Tmpl, fn: phasePipeCreation},
{kind: Kind.Tmpl, fn: phaseI18nTextExtraction},
@@ -111,6 +113,7 @@ const phases: Phase[] = [
{kind: Kind.Both, fn: phaseVarCounting},
{kind: Kind.Tmpl, fn: phaseGenerateAdvance},
{kind: Kind.Both, fn: phaseVariableOptimization},
{kind: Kind.Both, fn: phaseCreationVarColocation},
{kind: Kind.Both, fn: phaseNaming},
{kind: Kind.Tmpl, fn: phaseMergeNextContext},
{kind: Kind.Tmpl, fn: phaseNgContainer},
@@ -14,10 +14,9 @@ import {splitNsName} from '../../../ml_parser/tags';
import * as o from '../../../output/output_ast';
import {ParseSourceSpan} from '../../../parse_util';
import * as t from '../../../render3/r3_ast';
import {BindingParser} from '../../../template_parser/binding_parser';
import * as ir from '../ir';
import {ComponentCompilationJob, HostBindingCompilationJob, type CompilationJob, type ViewCompilationUnit} from './compilation';
import {type CompilationJob, ComponentCompilationJob, HostBindingCompilationJob, type ViewCompilationUnit} from './compilation';
import {BINARY_OPERATORS, namespaceForKey} from './conversion';
const compatibilityMode = ir.CompatibilityMode.TemplateDefinitionBuilder;
@@ -28,15 +27,17 @@ const compatibilityMode = ir.CompatibilityMode.TemplateDefinitionBuilder;
* TODO: Refactor more of the ingestion code into phases.
*/
export function ingestComponent(
componentName: string, template: t.Node[], constantPool: ConstantPool,
componentName: string, isSignal: boolean, template: t.Node[], constantPool: ConstantPool,
relativeContextFilePath: string, i18nUseExternalIds: boolean): ComponentCompilationJob {
const cpl = new ComponentCompilationJob(
componentName, constantPool, compatibilityMode, relativeContextFilePath, i18nUseExternalIds);
componentName, isSignal, constantPool, compatibilityMode, relativeContextFilePath,
i18nUseExternalIds);
ingestNodes(cpl.root, template);
return cpl;
}
export interface HostBindingInput {
isSignal: boolean;
componentName: string;
properties: e.ParsedProperty[]|null;
attributes: {[key: string]: o.Expression};
@@ -48,9 +49,9 @@ export interface HostBindingInput {
* representation.
*/
export function ingestHostBinding(
input: HostBindingInput, bindingParser: BindingParser,
constantPool: ConstantPool): HostBindingCompilationJob {
const job = new HostBindingCompilationJob(input.componentName, constantPool, compatibilityMode);
input: HostBindingInput, constantPool: ConstantPool): HostBindingCompilationJob {
const job = new HostBindingCompilationJob(
input.componentName, input.isSignal, constantPool, compatibilityMode);
for (const property of input.properties ?? []) {
ingestHostProperty(job, property, false);
}
@@ -84,8 +85,11 @@ export function ingestHostProperty(
if (property.isAnimation) {
bindingKind = ir.BindingKind.Animation;
}
const bindingXref = job.allocateXrefId();
job.root.create.push(ir.createBindingSignalPlaceholderOp(bindingXref));
job.root.update.push(ir.createBindingOp(
job.root.xref, bindingKind, property.name, expression, null,
bindingXref, job.root.xref, bindingKind, property.name, expression, null,
SecurityContext
.NONE /* TODO: what should we pass as security context? Passing NONE for now. */,
isTextAttribute, false, property.sourceSpan));
@@ -93,8 +97,10 @@ export function ingestHostProperty(
export function ingestHostAttribute(
job: HostBindingCompilationJob, name: string, value: o.Expression): void {
const bindingXref = job.allocateXrefId();
const attrBinding = ir.createBindingOp(
job.root.xref, ir.BindingKind.Attribute, name, value, null, SecurityContext.NONE, true, false,
bindingXref, job.root.xref, ir.BindingKind.Attribute, name, value, null, SecurityContext.NONE,
true, false,
/* TODO: host attribute source spans */ null!);
job.root.update.push(attrBinding);
}
@@ -611,7 +617,7 @@ enum BindingFlags {
}
function ingestBinding(
view: ViewCompilationUnit, xref: ir.XrefId, name: string, value: e.AST|o.Expression,
view: ViewCompilationUnit, targetXref: ir.XrefId, name: string, value: e.AST|o.Expression,
type: e.BindingType, unit: string|null, securityContext: SecurityContext,
sourceSpan: ParseSourceSpan, flags: BindingFlags): void {
if (value instanceof e.ASTWithSource) {
@@ -622,7 +628,8 @@ function ingestBinding(
type === e.BindingType.Property) {
// This binding only exists for later const extraction, and is not an actual binding to be
// created.
view.create.push(ir.createExtractedAttributeOp(xref, ir.BindingKind.Property, name, null));
view.create.push(
ir.createExtractedAttributeOp(targetXref, ir.BindingKind.Property, name, null));
return;
}
@@ -639,9 +646,13 @@ function ingestBinding(
}
const kind: ir.BindingKind = BINDING_KINDS.get(type)!;
const bindingXref = view.job.allocateXrefId();
view.create.push(ir.createBindingSignalPlaceholderOp(bindingXref));
view.update.push(ir.createBindingOp(
xref, kind, name, expression, unit, securityContext, !!(flags & BindingFlags.TextValue),
!!(flags & BindingFlags.IsStructuralTemplateAttribute), sourceSpan));
bindingXref, targetXref, kind, name, expression, unit, securityContext,
!!(flags & BindingFlags.TextValue), !!(flags & BindingFlags.IsStructuralTemplateAttribute),
sourceSpan));
}
/**
@@ -151,6 +151,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)]);
}
@@ -287,6 +293,22 @@ export function classMap(expression: o.Expression, sourceSpan: ParseSourceSpan):
return call(Identifiers.classMap, [expression], sourceSpan);
}
export function propertyCreate(
slot: number, name: string, expression: o.Expression, sanitizer: o.Expression|null,
sourceSpan: ParseSourceSpan): ir.CreateOp {
const args: o.Expression[] = [
o.literal(slot),
o.literal(name),
o.fn([], [new o.ReturnStatement(expression)]),
];
if (sanitizer !== null) {
args.push(sanitizer);
}
return call(Identifiers.propertyCreate, args, sourceSpan);
}
const PIPE_BINDINGS: o.ExternalReference[] = [
Identifiers.pipeBind1,
Identifiers.pipeBind2,
@@ -424,6 +446,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.
*/
@@ -24,6 +24,7 @@ export function phaseAttributeExtraction(job: CompilationJob): void {
extractAttributeOp(unit, op, elements);
break;
case ir.OpKind.Property:
case ir.OpKind.PropertyCreate:
if (!op.isAnimationTrigger) {
ir.OpList.insertBefore<ir.CreateOp>(
ir.createExtractedAttributeOp(
@@ -64,8 +64,9 @@ export function phaseBindingSpecialization(job: CompilationJob): void {
ir.OpList.replace<ir.UpdateOp>(
op,
ir.createPropertyOp(
op.target, op.name, op.expression, op.bindingKind === ir.BindingKind.Animation,
op.securityContext, op.isTemplate, op.sourceSpan));
op.bindingXref, op.target, op.name, op.expression,
op.bindingKind === ir.BindingKind.Animation, op.securityContext, op.isTemplate,
op.sourceSpan));
}
break;
@@ -25,6 +25,7 @@ const CHAINABLE = new Set([
R3.listener,
R3.listener,
R3.property,
R3.propertyCreate,
R3.styleProp,
R3.stylePropInterpolate1,
R3.stylePropInterpolate2,
@@ -119,7 +120,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 {CompilationJob, CompilationUnit} 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: CompilationJob): void {
for (const unit of cpl.units) {
processUnit(unit);
}
}
function processUnit(unit: CompilationUnit): void {
const shallowDeclarations = new Map<ir.XrefId, ir.VariableOp<ir.CreateOp>[]>();
const seenVariableReads = new Set<ir.XrefId>;
let shallow = true;
for (const op of unit.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.targetSlot = expr.targetSlot;
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 {ComponentCompilationJob, ViewCompilationUnit} from '../compilation';
/**
@@ -53,9 +52,9 @@ function recursivelyProcessView(view: ViewCompilationUnit, parentScope: Scope|nu
}
}
// 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: ViewCompilationUnit, parent: Scope|null): Scope {
kind: ir.SemanticVariableKind.Identifier,
name: null,
identifier,
target: null,
});
}
@@ -153,6 +153,7 @@ function getScopeForView(view: ViewCompilationUnit, parent: Scope|null): Scope {
kind: ir.SemanticVariableKind.Identifier,
name: null,
identifier: op.localRefs[offset].name,
target: op.xref,
},
});
}
@@ -169,16 +170,17 @@ function getScopeForView(view: ViewCompilationUnit, parent: Scope|null): Scope {
* This is a recursive process, as views inherit variables available from their parent view, which
* itself may have inherited variables, etc.
*/
function generateVariablesInScopeForView(
view: ViewCompilationUnit, scope: Scope): ir.VariableOp<ir.UpdateOp>[] {
const newOps: ir.VariableOp<ir.UpdateOp>[] = [];
function generateVariablesInScopeForView<OpT extends ir.Op<OpT>>(
view: ViewCompilationUnit, 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.job.allocateXrefId(), scope.viewContextVariable, new ir.NextContextExpr()));
view.job.allocateXrefId(), scope.viewContextVariable, new ir.NextContextExpr(),
/* isConstant */ true));
}
// Add variables for all context variables available in this scope's view.
@@ -188,18 +190,20 @@ function generateVariablesInScopeForView(
const variable = value === ir.CTX_REF ? context : new o.ReadPropExpr(context, value);
// Add the variable declaration.
newOps.push(ir.createVariableOp(
view.job.allocateXrefId(), scope.contextVariables.get(name)!, variable));
view.job.allocateXrefId(), scope.contextVariables.get(name)!, variable,
/* 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.job.allocateXrefId(), ref.variable, new ir.ReferenceExpr(ref.targetId, ref.offset)));
view.job.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;
}
@@ -9,7 +9,7 @@
import {sanitizeIdentifier} from '../../../../parse_util';
import {hyphenate} from '../../../../render3/view/style_parser';
import * as ir from '../../ir';
import {ViewCompilationUnit, type CompilationJob, type CompilationUnit, HostBindingCompilationUnit} from '../compilation';
import {type CompilationJob, type CompilationUnit, HostBindingCompilationUnit, ViewCompilationUnit} from '../compilation';
import {prefixWithNamespace} from '../conversion';
/**
@@ -37,6 +37,7 @@ function addNamesToView(
for (const op of unit.ops()) {
switch (op.kind) {
case ir.OpKind.Property:
case ir.OpKind.PropertyCreate:
case ir.OpKind.HostProperty:
if (op.isAnimationTrigger) {
op.name = '@' + op.name;
@@ -26,6 +26,9 @@ interface Rule<T extends ir.CreateOp|ir.UpdateOp> {
const CREATE_ORDERING: Array<Rule<ir.CreateOp>> = [
{test: op => op.kind === ir.OpKind.Listener && op.hostListener && op.isAnimationListener},
{test: op => op.kind === ir.OpKind.Listener && !(op.hostListener && op.isAnimationListener)},
// TODO(signals): Think about ordering for instructions in the create block!
// Right now, not relevant as we only have a single property create instruction.
];
/**
@@ -47,8 +50,14 @@ const UPDATE_ORDERING: Array<Rule<ir.UpdateOp>> = [
* The set of all op kinds we handle in the reordering phase.
*/
const handledOpKinds = new Set([
ir.OpKind.Listener, ir.OpKind.StyleMap, ir.OpKind.ClassMap, ir.OpKind.StyleProp,
ir.OpKind.ClassProp, ir.OpKind.Property, ir.OpKind.HostProperty, ir.OpKind.Attribute
ir.OpKind.Listener,
ir.OpKind.StyleMap,
ir.OpKind.ClassMap,
ir.OpKind.StyleProp,
ir.OpKind.ClassProp,
ir.OpKind.Property,
ir.OpKind.HostProperty,
ir.OpKind.Attribute,
]);
export function phaseOrdering(job: CompilationJob) {
@@ -9,7 +9,7 @@
import * as o from '../../../../output/output_ast';
import {Identifiers} from '../../../../render3/r3_identifiers';
import * as ir from '../../ir';
import {ViewCompilationUnit, type CompilationJob, type CompilationUnit} from '../compilation';
import {type CompilationJob, type CompilationUnit, ViewCompilationUnit} from '../compilation';
import * as ng from '../instruction';
/**
@@ -122,10 +122,17 @@ function reifyCreateOperations(unit: CompilationUnit, 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.Namespace:
switch (op.active) {
@@ -164,6 +171,14 @@ function reifyCreateOperations(unit: CompilationUnit, ops: ir.OpList<ir.CreateOp
ir.OpList.replace<ir.CreateOp>(
op, ng.projection(op.slot, op.projectionSlotIndex, op.attributes));
break;
case ir.OpKind.PropertyCreate:
if (op.expression instanceof ir.Interpolation) {
// TODO: handle interpolation
} else {
ir.OpList.replace(
op, ng.propertyCreate(op.slot!, op.name, op.expression, op.sanitizer, op.sourceSpan));
}
break;
case ir.OpKind.Statement:
// Pass statement operations directly through.
break;
@@ -268,7 +283,8 @@ function reifyUpdateOperations(_unit: CompilationUnit, 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.Conditional:
if (op.processed === null) {
@@ -300,6 +316,8 @@ function reifyIrExpression(expr: o.Expression): o.Expression {
return ng.nextContext(expr.steps);
case ir.ExpressionKind.Reference:
return ng.reference(expr.targetSlot! + 1 + expr.offset);
case ir.ExpressionKind.ShallowReference:
return ng.shallowReference(expr.targetSlot! + 1 + expr.offset);
case ir.ExpressionKind.LexicalRead:
throw new Error(`AssertionError: unresolved LexicalRead of ${expr.name}`);
case ir.ExpressionKind.RestoreView:
@@ -341,6 +359,8 @@ function reifyIrExpression(expr: o.Expression): o.Expression {
return o.importExpr(sanitizerIdentifierMap.get(expr.fn)!);
case ir.ExpressionKind.SlotLiteralExpr:
return o.literal(expr.targetSlot);
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]}`);
@@ -6,23 +6,23 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as o from '../../../../output/output_ast';
import * as ir from '../../ir';
import type {CompilationJob} from '../compilation';
export function phaseRemoveEmptyBindings(job: CompilationJob): void {
for (const unit of job.units) {
for (const op of unit.update) {
for (const op of unit.ops()) {
switch (op.kind) {
case ir.OpKind.Attribute:
case ir.OpKind.Binding:
case ir.OpKind.ClassProp:
case ir.OpKind.ClassMap:
case ir.OpKind.Property:
case ir.OpKind.PropertyCreate:
case ir.OpKind.StyleProp:
case ir.OpKind.StyleMap:
if (op.expression instanceof ir.EmptyExpr) {
ir.OpList.remove<ir.UpdateOp>(op);
ir.OpList.remove<ir.CreateOp|ir.UpdateOp>(op);
}
break;
}
@@ -35,8 +35,20 @@ function processLexicalScope(view: CompilationUnit, 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.job.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:
@@ -27,32 +27,38 @@ const sanitizers = new Map<SecurityContext, ir.SanitizerFn|null>([
export function phaseResolveSanitizers(job: ComponentCompilationJob): void {
for (const unit of job.units) {
const elements = getElementsByXrefId(unit);
let sanitizerFn: ir.SanitizerFn|null;
for (const op of unit.update) {
for (const op of unit.ops()) {
switch (op.kind) {
case ir.OpKind.Property:
case ir.OpKind.PropertyCreate:
case ir.OpKind.Attribute:
sanitizerFn = sanitizers.get(op.securityContext) || null;
op.sanitizer = sanitizerFn ? new ir.SanitizerExpr(sanitizerFn) : null;
// If there was no sanitization function found based on the security context of an
// attribute/property, check whether this attribute/property is one of the
// security-sensitive <iframe> attributes (and that the current element is actually an
// <iframe>).
if (op.sanitizer === null) {
const ownerOp = elements.get(op.target);
if (ownerOp === undefined) {
throw Error('Property should have an element-like owner');
}
if (isIframeElement(ownerOp) && isIframeSecuritySensitiveAttr(op.name)) {
op.sanitizer = new ir.SanitizerExpr(ir.SanitizerFn.IframeAttribute);
}
}
resolveSanitizer(op, elements);
break;
}
}
}
}
function resolveSanitizer(
op: ir.PropertyOp|ir.PropertyCreateOp|ir.AttributeOp,
elements: Map<ir.XrefId, ir.ElementOrContainerOps>): void {
const sanitizerFn = sanitizers.get(op.securityContext) || null;
op.sanitizer = sanitizerFn ? new ir.SanitizerExpr(sanitizerFn) : null;
// If there was no sanitization function found based on the security context of an
// attribute/property, check whether this attribute/property is one of the
// security-sensitive <iframe> attributes (and that the current element is actually an
// <iframe>).
if (op.sanitizer === null) {
const ownerOp = elements.get(op.target);
if (ownerOp === undefined) {
throw Error('Property should have an element-like owner');
}
if (isIframeElement(ownerOp) && isIframeSecuritySensitiveAttr(op.name)) {
op.sanitizer = new ir.SanitizerExpr(ir.SanitizerFn.IframeAttribute);
}
}
}
/**
* Checks whether the given op represents an iframe element.
*/
@@ -19,7 +19,8 @@ export function phaseSaveRestoreView(job: ComponentCompilationJob): 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(unit: ViewCompilationUnit, op: ir
name: null,
view: unit.xref,
},
new ir.RestoreViewExpr(unit.xref)),
new ir.RestoreViewExpr(unit.xref), /* isConstant */ true),
]);
// The "restore view" operation in listeners requires a call to `resetView` to reset the
@@ -0,0 +1,71 @@
/**
* @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 ir from '../../ir';
import {CompilationJob, CompilationUnit} from '../compilation';
/**
* Phase that moves specialized `Property` operations to the creation
* block if the compilation job targets is signal based. Also removes
* all superfluous binding signal placeholders.
*
* This phase exists as we want to avoid having to the repeat binding specialization
* logic for property bindings, but rather re-use the already-specialized information.
*/
export function phaseSignalBindings(cpl: CompilationJob): void {
for (const unit of cpl.units) {
processUnit(unit);
}
}
function processUnit(unit: CompilationUnit) {
const placeholders = new Map<ir.XrefId, ir.BindingSignalPlaceholder>();
for (const op of unit.create) {
if (op.kind === ir.OpKind.BindingSignalPlaceholder) {
placeholders.set(op.bindingXref, op);
}
}
// For signal jobs, we move:
// - property operations into the create block.
if (unit.job.isSignal) {
for (const op of unit.update) {
if (op.kind === ir.OpKind.Property) {
const placeholderOp = placeholders.get(op.bindingXref);
if (placeholderOp === undefined) {
throw new Error(
'Expected binding placeholder operation to exist for property operation.');
}
// Signal property bindings, do not maintain a separate instruction for property
// interpolations as the expression is wrapped is `computed` anyway. Instead, we
// generate an interpolation template literal expression.
const expression = op.expression instanceof ir.Interpolation ?
new ir.InterpolationTemplateExpr(op.expression.strings, op.expression.expressions) :
op.expression;
const createOp = ir.createPropertyCreateOp(
op.bindingXref, op.target, op.name, expression, op.isAnimationTrigger,
op.securityContext, op.isTemplate, op.sourceSpan);
// Replace the placeholder with the new instruction.
ir.OpList.replace<ir.CreateOp>(placeholderOp, createOp);
placeholders.delete(op.bindingXref);
// Remove the property update operation, as we have the create operation now.
ir.OpList.remove<ir.UpdateOp>(op);
}
}
}
// Remove all remaining placeholders. Not all binding operations
// end up in the create block, even for signal jobs. For non-signal
// jobs we remove all placeholder operations.
for (const op of placeholders.values()) {
ir.OpList.remove<ir.CreateOp>(op);
}
}
@@ -72,6 +72,7 @@ function varsUsedByOp(op: (ir.CreateOp|ir.UpdateOp)&ir.ConsumesVarsTrait): numbe
let slots: number;
switch (op.kind) {
case ir.OpKind.Property:
case ir.OpKind.PropertyCreate:
case ir.OpKind.HostProperty:
case ir.OpKind.Attribute:
// All of these bindings use 1 variable slot, plus 1 slot for every interpolated expression,
@@ -276,10 +276,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:
+2 -2
View File
@@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
export {createComputed} from './src/computed';
export {COMPUTED_NODE, ComputedNode, COMPUTING, createComputed, ERRORED, UNSET} from './src/computed';
export {defaultEquals, ValueEqualityFn} from './src/equality';
export {setThrowInvalidWriteToSignalError} from './src/errors';
export {consumerAfterComputation, consumerBeforeComputation, consumerDestroy, getActiveConsumer, isInNotificationPhase, isReactive, producerAccessed, producerNotifyConsumers, producerUpdatesAllowed, producerUpdateValueVersion, Reactive, REACTIVE_NODE, ReactiveNode, setActiveConsumer, SIGNAL} from './src/graph';
export {consumerAfterComputation, consumerBeforeComputation, consumerDestroy, consumerMarkDirty, getActiveConsumer, isInNotificationPhase, isReactive, producerAccessed, producerNotifyConsumers, producerUpdatesAllowed, producerUpdateValueVersion, Reactive, REACTIVE_NODE, ReactiveNode, setActiveConsumer, SIGNAL} from './src/graph';
export {createSignal, setPostSignalSetFn, SignalGetter, signalMutateFn, SignalNode, signalSetFn, signalUpdateFn} from './src/signal';
export {createWatch, Watch, WatchCleanupFn, WatchCleanupRegisterFn} from './src/watch';
export {setAlternateWeakRefImpl} from './src/weak_ref';
@@ -68,26 +68,26 @@ export function createComputed<T>(computation: () => T): ComputedGetter<T> {
* 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 = /* @__PURE__ */ Symbol('UNSET');
export const UNSET: any = /* @__PURE__ */ 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 = /* @__PURE__ */ Symbol('COMPUTING');
export const COMPUTING: any = /* @__PURE__ */ 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 = /* @__PURE__ */ Symbol('ERRORED');
export const ERRORED: any = /* @__PURE__ */ Symbol('ERRORED');
// Note: Using an IIFE here to ensure that the spread assignment is not considered
// a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
// TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
const COMPUTED_NODE = /* @__PURE__ */ (() => {
export const COMPUTED_NODE = /* @__PURE__ */ (() => {
return {
...REACTIVE_NODE,
value: UNSET,
+1
View File
@@ -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';
@@ -36,4 +36,9 @@ export {
export {
assertNotInReactiveContext,
} from './render3/reactivity/asserts';
export {input} from './render3/reactivity/input';
export {InputSignal, ɵɵGetInputSignalWriteType} from './render3/reactivity/input_signal';
export {ModelSignal} from './render3/reactivity/model_signal';
export {contentChild, contentChildren, viewChild, viewChildren} from './render3/reactivity/queries';
export {output} from './render3/reactivity/output';
// clang-format on
@@ -147,6 +147,7 @@ export {
ɵɵprojection,
ɵɵprojectionDef,
ɵɵproperty,
ɵɵpropertyCreate,
ɵɵpropertyInterpolate,
ɵɵpropertyInterpolate1,
ɵɵpropertyInterpolate2,
@@ -157,6 +158,7 @@ export {
ɵɵpropertyInterpolate7,
ɵɵpropertyInterpolate8,
ɵɵpropertyInterpolateV,
ɵɵstringifyInterpolation,
ɵɵProvidersFeature,
ɵɵHostDirectivesFeature,
ɵɵpureFunction0,
@@ -169,6 +171,8 @@ export {
ɵɵpureFunction7,
ɵɵpureFunction8,
ɵɵpureFunctionV,
ɵɵcontentQueryCreate,
ɵɵviewQueryCreate,
ɵɵqueryRefresh,
ɵɵreference,
ɵɵresetView,
@@ -186,6 +190,7 @@ export {
ɵɵsetNgModuleScope,
ɵɵgetComponentDepsFactory,
ɵɵStandaloneFeature,
ɵɵshallowReference,
ɵɵstyleMap,
ɵɵstyleMapInterpolate1,
ɵɵstyleMapInterpolate2,
+13 -2
View File
@@ -44,7 +44,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;
@@ -173,9 +174,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 */
+2 -2
View File
@@ -345,7 +345,7 @@ export interface Directive {
/**
* // TODO(signals): Remove internal and add public documentation
*
* @internal
* //TODO-internal
*/
signals?: boolean;
@@ -640,7 +640,7 @@ export interface Component extends Directive {
/**
* // TODO(signals): Remove internal and add public documentation.
* @internal
* TODO- internal
*/
signals?: boolean;
@@ -6,12 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertNotInReactiveContext} from '../core_reactivity_export_internal';
import {assertInInjectionContext, Injector, ɵɵdefineInjectable} from '../di';
import {inject} from '../di/injector_compatibility';
import {ErrorHandler} from '../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {DestroyRef} from '../linker/destroy_ref';
import {assertNotInReactiveContext} from '../render3/reactivity/asserts';
import {assertGreaterThan} from '../util/assert';
import {NgZone} from '../zone';
+5 -5
View File
@@ -220,8 +220,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;
@@ -336,7 +336,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);
@@ -409,7 +409,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;
}
@@ -445,7 +445,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>(
+5 -3
View File
@@ -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);
+8 -1
View File
@@ -80,7 +80,10 @@ export {
ɵɵprojection,
ɵɵprojectionDef,
ɵɵproperty,
ɵɵpropertyCreate,
ɵɵpropertyInterpolate,
ɵɵpropertyInterpolate1,
ɵɵpropertyInterpolate2,
@@ -91,8 +94,10 @@ export {
ɵɵpropertyInterpolate7,
ɵɵpropertyInterpolate8,
ɵɵpropertyInterpolateV,
ɵɵstringifyInterpolation,
ɵɵreference,
ɵɵshallowReference,
ɵɵrepeater,
ɵɵrepeaterCreate,
@@ -203,7 +208,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, 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[TVIEW], 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) {
@@ -46,6 +46,7 @@ export * from './namespace';
export * from './next_context';
export * from './projection';
export * from './property';
export * from './property_create';
export * from './property_interpolation';
export * from './storage';
export * from './style_map_interpolation';
@@ -13,11 +13,11 @@ import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} fr
import {CONTAINER_HEADER_OFFSET, HAS_CHILD_VIEWS_TO_REFRESH, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS} from '../interfaces/container';
import {ComponentTemplate, RenderFlags} from '../interfaces/definition';
import {CONTEXT, ENVIRONMENT, FLAGS, InitPhaseState, LView, LViewFlags, PARENT, TVIEW, TView} from '../interfaces/view';
import {enterView, isInCheckNoChangesMode, leaveView, setBindingIndex, setIsInCheckNoChangesMode} from '../state';
import {enterView, getVirtualInstructionIndex, isInCheckNoChangesMode, leaveView, setBindingIndex, setIsInCheckNoChangesMode} from '../state';
import {getFirstLContainer, getNextLContainer} from '../util/view_traversal_utils';
import {getComponentLViewByIndex, isCreationMode, markAncestorsForTraversal, 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,184 @@
/**
* @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 {SIGNAL} from '@angular/core/primitives/signals';
import {validateAgainstEventProperties} from '../../sanitization/sanitization';
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 {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 {computed} from '../reactivity/computed';
import {InputSignalNode} from '../reactivity/input_signal';
import {getCurrentTNode, getLView, 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: InputSignalNode<unknown, unknown>[]|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 InputSignalNode<unknown, unknown>;
(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) {
// TODO: Improve this by not allocating an object literal here. This exists just for testing.
inputSignal.bind(inputSignal, {computation: expr});
// TODO: figure out where to set `isInitialized`.
inputSignal.isInitialized = true;
}
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(tNode.index, 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(
nodeIndex: number, propName: string, expressionSlot: number,
targets: PropertyAliasValue): void {
const lView = getLView();
const expr = lView[expressionSlot];
const value = expr();
const tView = getTView();
const tNode = tView.data[nodeIndex] as TNode;
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) {
@@ -44,7 +44,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, 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';
@@ -277,6 +277,13 @@ export function executeTemplate<T>(
effectiveConsumer.dirty = false;
}
templateFn(rf, context);
// Flush remaining virtual instructions.
if (isUpdatePhase && tView.virtualUpdate) {
for (let idx = getVirtualInstructionIndex(); idx < tView.virtualUpdate.length; idx++) {
tView.virtualUpdate[idx].instruction();
}
}
} finally {
consumerAfterComputation(effectiveConsumer, prevConsumer);
}
@@ -398,7 +405,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,
@@ -429,6 +436,7 @@ export function createTView(
schemas: schemas,
consts: consts,
incompleteFirstPass: false,
virtualUpdate: null,
ssrId,
};
if (ngDevMode) {
@@ -748,7 +756,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';
@@ -760,13 +768,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);
@@ -1241,7 +1252,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;
}
@@ -1316,7 +1327,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 {
@@ -1577,7 +1588,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;
@@ -1585,6 +1597,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);
}
}
@@ -1600,3 +1617,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);
}
@@ -0,0 +1,7 @@
/**
* @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
*/
@@ -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 {FlushableEffectRunner} from '../reactivity/effect';
import type {AfterRenderEventManager} from '../after_render_hooks';
@@ -841,6 +842,8 @@ export interface TView {
* (see `getComponentId` function for details)
*/
ssrId: string|null;
virtualUpdate: TVirtualInstruction[]|null;
}
/** Single hook callback function. */
@@ -917,6 +920,11 @@ export type TData =
(TNode|PipeDef<any>|DirectiveDef<any>|ComponentDef<any>|number|TStylingRange|TStylingKey|
ProviderToken<any>|TI18n|I18nUpdateOpCodes|TIcu|null|string|TDeferBlockDetails)[];
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,
+41 -22
View File
@@ -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;
}
+29 -5
View File
@@ -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 {
@@ -0,0 +1,65 @@
/**
* @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 {ERRORED, producerAccessed, producerUpdateValueVersion, SIGNAL} from '@angular/core/primitives/signals';
import {INPUT_SIGNAL_NODE, InputSignal, InputSignalNode} 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: InputSignalNode<ReadT, WriteT> = Object.create(INPUT_SIGNAL_NODE);
opts?.transform && (node.transform = opts.transform);
opts?.initialValue && (node.value = node.transform(opts.initialValue));
function inputValueFn() {
// Check if the value needs updating before returning it.
producerUpdateValueVersion(node);
// Record that someone looked at this signal.
producerAccessed(node);
if (node.value === ERRORED) {
throw node.error;
}
return node.value;
}
(inputValueFn as any)[SIGNAL] = node;
return inputValueFn as InputSignal<ReadT, WriteT>;
}
@@ -0,0 +1,107 @@
/**
* @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 {COMPUTED_NODE, ComputedNode, UNSET} from '@angular/core/primitives/signals';
import {setPureFunctionsEnabled} from '../pure_function';
import {Signal} from './api';
export const BRAND_WRITE_TYPE = /* @__PURE__ */ 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)
export type ɵɵGetInputSignalWriteType<T> = T extends InputSignal<any, infer X>? X : never;
export interface InputSignalNode<ReadT, WriteT> extends ComputedNode<ReadT> {
/**
* Whether the input signal is initialized. If not, accessing the node results in an error.
*
* We support this to enable required inputs which may be set later, but should not be accessed
* before as there is no meaningful value and this usually indicates incorrect user code.
*/
isInitialized: boolean;
/**
* The computation to which the input is currently bound. An input signal is commonly
* bound to a computation if a parent signal component binds to the input.
*/
_boundComputation?: (() => WriteT);
/**
* An input signal can hold a directly bound value. This is used when e.g. Zone components
* change input signal values.
*/
_boundValue?: WriteT;
bind(node: this, value: {value?: WriteT, computation?: () => WriteT}): void;
/** Transform function that is run whenever the node value is retrieved. */
// TODO: do we need to memoize
transform: (value: WriteT) => ReadT;
}
// Note: Using an IIFE here to ensure that the spread assignment is not considered
// a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
// TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
export const INPUT_SIGNAL_NODE: InputSignalNode<unknown, unknown> = /* @__PURE__ */ (() => {
return {
...COMPUTED_NODE,
// Input-signal specific defaults.
isInitialized: false,
transform: value => value,
// Overrides from the computed node.
// TODO: Should we change `ComputedNode` somehow to not rely on context (seems rather brittle).
computation: function(this: InputSignalNode<unknown, unknown>) {
if (!this.isInitialized) {
// TODO: Make this a proper RuntimeError
throw new Error(`InputSignal not yet initialized`);
}
if (this._boundComputation !== undefined) {
// TODO(signals): Do we need this?
// 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);
}
}
// Alternatively, if there no bound computation, we use the bound value.
return this.transform(this._boundValue);
},
bind: function<ReadT, WriteT>(
node: InputSignalNode<ReadT, WriteT>, value: {value?: WriteT, computation?: () => WriteT}) {
// TODO(signals): perf]
node._boundComputation = value.computation;
node._boundValue = value.value;
// TODO: Do we need to switch between bound values/computation.
node.dirty = true;
node.value = UNSET;
},
};
})();
@@ -0,0 +1,20 @@
/**
* @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 {InputSignal, InputSignalNode} from './input_signal';
import {WritableSignal} from './signal';
/**
* 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'>;
export interface ModelSignalNode<ReadT, WriteT> extends InputSignalNode<ReadT, WriteT> {}
@@ -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,150 @@
/**
* @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 {consumerMarkDirty, producerAccessed, producerUpdateValueVersion, REACTIVE_NODE, ReactiveNode, SIGNAL} from '@angular/core/primitives/signals';
import {ProviderToken} from '../../di/provider_token';
import {QueryList} from '../../linker';
import {QueryFlags} from '../interfaces/query';
import {LView} from '../interfaces/view';
import {createContentQueryInternal, createViewQueryInternal, loadQueryInternal, queryRefreshInternal} from '../query';
import {getLView} from '../state';
import {Signal} from './api';
export interface QuerySignalNode<T> extends ReactiveNode {
_lView?: LView;
_queryIndex?: number;
_queryList?: QueryList<T>;
bindToQuery(node: this, queryIndex: number): void;
}
// Note: Using an IIFE here to ensure that the spread assignment is not considered
// a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
// TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
export const QUERY_SIGNAL_NODE: QuerySignalNode<unknown> = /* @__PURE__ */ (() => {
return {
...REACTIVE_NODE,
// Base reactive node.overrides
producerMustRecompute: (node: QuerySignalNode<unknown>) => {
return !!node._queryList?.dirty;
},
producerRecomputeValue: (node: QuerySignalNode<unknown>) => {
// 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(node._lView!, node._queryIndex!)) {
node.version++;
}
},
// Query-specific implementations.
bindToQuery: (node: QuerySignalNode<unknown>, queryIndex: number) => {
// TODO: assert: should bind only once, make sure it is not re-assigned again
node._lView = getLView();
node._queryIndex = queryIndex;
node._queryList = loadQueryInternal(node._lView, queryIndex);
node._queryList.onDirty(() => {
console.error('Dirty');
// Mark this producer as dirty and notify live consumer 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).
consumerMarkDirty(node);
});
},
// TODO(signals): Unsubscribe - destroy?
};
})();
function querySignalFnFirst<T>(): Signal<T|undefined> {
const node: QuerySignalNode<T> = Object.create(QUERY_SIGNAL_NODE);
function signalFn() {
// Check if the value needs updating before returning it.
producerUpdateValueVersion(node);
// Mark this producer as accessed.
producerAccessed(node);
return node._queryList?.first;
}
(signalFn as any)[SIGNAL] = node;
return signalFn as Signal<T|undefined>;
}
function querySignalFnAll<T>(): Signal<T[]> {
const node: QuerySignalNode<T> = Object.create(QUERY_SIGNAL_NODE);
function signalFn() {
// Check if the value needs updating before returning it.
producerUpdateValueVersion(node);
// Mark this producer as accessed.
producerAccessed(node);
return node._queryList?.toArray() ?? [];
}
(signalFn as any)[SIGNAL] = node;
return signalFn as Signal<T[]>;
}
// 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 QuerySignalNode<unknown>;
reactiveQueryNode.bindToQuery(
reactiveQueryNode, 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> {
return querySignalFnFirst();
}
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?
return querySignalFnAll();
}
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 QuerySignalNode<unknown>;
// Q: why do we need the directive index?
reactiveQueryNode.bindToQuery(
reactiveQueryNode, 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> {
return querySignalFnFirst();
}
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?
return querySignalFnAll();
}

Some files were not shown because too many files have changed in this diff Show More