Compare commits

...

88 Commits

Author SHA1 Message Date
Paul Gschwendtner c4b6d474f8 WIP STYLING: temp change to fix styling template pipeline emit 2023-07-14 15:52:38 +00:00
Paul Gschwendtner 78efc406ba WIP STYLING: improve comment correctness styling 2023-07-14 15:30:48 +00:00
Paul Gschwendtner c277e88b22 WIP STYLING: styling findings/reversing 2023-07-14 15:30:45 +00:00
Paul Gschwendtner 574b1c276d 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-07-14 13:12:41 +00:00
Paul Gschwendtner 40eb5e3207 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-07-14 13:12:41 +00:00
Paul Gschwendtner 8ef145fa1b feat(compiler): support public aliases for signal output API 2023-07-14 13:12:41 +00:00
Paul Gschwendtner 94f2daaa1c 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-07-14 13:12:41 +00:00
Paul Gschwendtner ffc61749ff test: rework and enable outputs test to verify functionality 2023-07-14 13:12:41 +00:00
Paul Gschwendtner f70d80c8c7 refactor(compiler): properly sanitize identifier names in template pipeline
Currently, a listener on an element containing a dash, will result in
runtime errors because the function name will be generated using a dash.

e.g.

```
function MyApp_Template_some-comp_bla_0() {}
```

throwing with a syntax error due to the dash. We fix this by re-using
the sanitize identifier function from the current template definition
builder.
2023-07-14 13:12:41 +00:00
Pawel Kozlowski a4cb2e671c 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-07-14 13:12:41 +00:00
Paul Gschwendtner dc7882cf97 test: update partial output golden for content queries 2023-07-14 13:12:41 +00:00
Paul Gschwendtner 5f9bd59b14 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-07-14 13:12:40 +00:00
Pawel Kozlowski cfd466dfbb feat(core): reactive content queries
Runtime part of the reactive content queries.
2023-07-14 13:12:40 +00:00
Pawel Kozlowski 61ce057a09 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-07-14 13:12:40 +00:00
Pawel Kozlowski 8dc6c9fe22 fix(core): adjust reactive query types
Improvements to the types signature in viewChild / viewChildren.
2023-07-14 13:12:40 +00:00
Paul Gschwendtner e71e900708 feat(compiler): support generating queries for signal components 2023-07-14 13:12:40 +00:00
Pawel Kozlowski 1168fd585f fixup! refactor(core): rename queryCreate to viewQueryCreate 2023-07-14 13:12:40 +00:00
Pawel Kozlowski 18ea007390 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-07-14 13:12:40 +00:00
Paul Gschwendtner bd23af0639 test: update api golden for core for new query APIs 2023-07-14 13:12:40 +00:00
Pawel Kozlowski 6b0e1c7703 feat(core): reactive queries
Initial runtime implementation of the reactive view queries
(in preparation for the compiler work).
2023-07-14 13:12:40 +00:00
Pawel Kozlowski 6b89ce9e27 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-07-14 13:12:40 +00:00
Paul Gschwendtner 0bf724f4d4 feat: support property interpolation in signal components 2023-07-14 13:12:40 +00:00
Pawel Kozlowski 7acf163f51 test: fix signal tests after property binding changes
Property bindings are wrapped into computed now.
2023-07-14 13:12:40 +00:00
Paul Gschwendtner 6808375e50 ci: send test job failures to reactivity channel 2023-07-14 13:12:40 +00:00
Pawel Kozlowski 97d6781be7 refactor(core): fix lint issues
Fixing lint issues that I've just introduced...
2023-07-14 13:12:40 +00:00
Pawel Kozlowski f12e8e3ea2 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-07-14 13:12:40 +00:00
Paul Gschwendtner f5638c25e6 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-07-14 13:12:40 +00:00
Paul Gschwendtner d82eb27f69 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-07-14 13:12:40 +00:00
Paul Gschwendtner 3b5218b768 ci: setup initial slack notifications for test failures in signal branch
Note: only going into the dev-infra channel for now until we get
everything green to avoid spam.
2023-07-14 13:12:40 +00:00
Paul Gschwendtner 5fa6ce3e61 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-07-14 13:12:40 +00:00
Paul Gschwendtner d88ac6db12 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-07-14 13:12:40 +00:00
Paul Gschwendtner 46efe02e48 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-07-14 13:12:39 +00:00
Paul Gschwendtner cd76360ea8 fixup! refactor(compiler): support o.BinaryExpression for assignments
Fix babel factory for linker
2023-07-14 13:12:39 +00:00
Pawel Kozlowski 09ad1ef5a2 refactor(compiler): add required file header
Add required file header.
2023-07-14 13:12:39 +00:00
Pawel Kozlowski 48f4f86835 test: add basic test for event listeners
Adding a passing test for a listener invocation
that changes signal value.
2023-07-14 13:12:39 +00:00
Pawel Kozlowski 183c92baed 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-07-14 13:12:39 +00:00
Alex Rickabaugh e03ccb60ed 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-07-14 13:12:39 +00:00
Alex Rickabaugh c402bb81f5 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-07-14 13:12:39 +00:00
Paul Gschwendtner 0a7a8e496e test: disable input signal initialization test
Still needs to be implemented and discussed.
2023-07-14 13:12:39 +00:00
Paul Gschwendtner 779e1d872f test: update core API golden to add input API 2023-07-14 13:12:39 +00:00
Paul Gschwendtner 193a32e5c1 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-07-14 13:12:39 +00:00
Pawel Kozlowski 41f8584aad feat(core): propertyInterpolationCreate
Skeleton of the propertyInterpolationCreate instruction.
2023-07-14 13:12:39 +00:00
Paul Gschwendtner 4b8e6b36ab build: fix lint errors 2023-07-14 13:12:39 +00:00
Pawel Kozlowski 4c58b2f8cf test: add failing tests for property interpolation
Adding failing tests for the property interpolation so we
keep those instructions on our radar.
2023-07-14 13:12:39 +00:00
Pawel Kozlowski 0e96528a70 refactor: fix lint issue
Minor refactor to fix the linting job.
2023-07-14 13:12:39 +00:00
Pawel Kozlowski 1d1338fced 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-07-14 13:12:39 +00:00
Pawel Kozlowski 1344da62ad test: enable passing tests after recent code changes
Enabling newly passing tests.
2023-07-14 13:12:39 +00:00
Alex Rickabaugh 0c821b28f3 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-07-14 13:12:39 +00:00
Alex Rickabaugh ebef1a592a wip: ignore dep polling when input signals change their backing
Previously, input signals had a bug where when their backing computation or
value would change, it would notify consumers, but consumers would poll the
input signal to check if its value really changed. This polling operation
would transitively poll the input signal's previous dependencies, which of
course would report that they hadn't changed.

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

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