Compare commits

..

109 Commits

Author SHA1 Message Date
Pawel Kozlowski 50623ea1e1 release: cut the v17.2.4 release 2024-03-06 20:28:55 +01:00
Matthieu Riegler 443e5f1591 fix(core): return a readonly signal on asReadonly. (#54719)
Previous `asReadonly()` returned the signal value and not the signal itself.

Fixes #54704

PR Close #54719
2024-03-06 15:18:51 +01:00
Alan Agius c75c168ed5 fix(docs-infra): do not delete BUILD.bazel in help directory during updates (#54697)
In https://github.com/angular/angular/pull/54591, a `BUILD.bazel` file was created in `aio/content/cli/help/BUILD.bazel`, this however breaks the automatic CLI help pages updates as prior to this change, the `help` directory was being deleted. See: https://github.com/angular/angular/pull/54691/commits/17346e89da2fbddf8274eeadf12bab9c5b8983d8

This commit, updates the process to delete only the JSON files and copying the new files.

PR Close #54697
2024-03-06 10:18:04 +01:00
Jessica Janiuk 3b50885691 docs: add process documentation to CARETAKER.md (#54664)
This adds documentation regarding the latest changes to framework merge tools. It specifically covers the requires: TGP label and separate sync for primitives.

PR Close #54664
2024-03-05 19:11:43 +01:00
Matthieu Riegler 6148d76e21 docs: fix clipped docs-code block in docs-reference-api-tab (#54621)
PR Close #54621
2024-03-05 18:35:20 +01:00
Matthieu Riegler aa0fce6531 docs: fix tutorial preview size (#54621)
PR Close #54621
2024-03-05 18:35:19 +01:00
Matthieu Riegler 43b027f36e docs: fix label on API link. (#54621)
Remove the unecessary double quotes on a @see link.

PR Close #54621
2024-03-05 18:35:14 +01:00
Matthieu Riegler e7eadd9aff docs(docs-infra): remove unwanted styling on API page. (#54621)
`box-sizing: border-box` prevents overflow caused by borders.

PR Close #54621
2024-03-05 18:35:13 +01:00
Jessica Janiuk badae7254e ci: fix ng-dev build for pr merging (#54708)
This updates ng-dev to the latest hash fixing the merge files query.

PR Close #54708
2024-03-05 18:28:25 +01:00
Andrew Kushnir 586cc24a10 fix(core): apply TestBed provider overrides to @defer dependencies (#54667)
This commit updates TestBed logic to take into account situations when dependencies loaded within `@defer` blocks may import NgModules with providers. For such components, we invoke provider override function, which recursively inspects and applies the necessary changes.

PR Close #54667
2024-03-05 09:21:23 +01:00
Paul Gschwendtner b558a01c84 fix(core): generic inference for signal inputs may break with --strictFunctionTypes (#54652)
This commit fixes that the generic inference for signal inputs may break
with `--strictFunctionTypes`.

We are not using --strictFunctionTypes` in all tests, so function parameters are always
checked bivariantly- while in 1P function parameters are checked contravariantly. This
breaks sub-typing and e.g. `InputSignal<number>` is not a subtype of `InputSignalWithTransform<unknown, number>`.

Root cause is that the input signal captures the equality function that
is exclusively concerned with the `ReadT`. And `ReadT` is never equal, or a
supertype of unknown.

https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgSSTAFcYBlBAcyUwBsB1BGACwBUpMkBndaAWwA8AJWCYAJqwA0celCbBWAPjgBvAFBw4Aei1xWACXxlEXOCzxQIEeNkzEuwAIRmAnmDxhMHPsFRRTXnjYzMDYANbAYnDYECgcAG5eCJwwtC7SAHIA8qxwAEYIALSJcilpAHQacMAAjsR0AFxwABSYTSLiUvntohIAlHAAvMp5VrSiSADcalUA+rNQvaw9ndOa8wDucqjLMtsK0wC+aqCQsIgoaFi4BESkFNR0wkuKVaCoSGKmhCTkVDQMJhsDjcXhQQQdCTSSFKVTHNQxbjwEBNH73f50RgsdicHj8ATEJBhJAQDZIaRIYh8PJoZSDKqU2i0ZyYb53P6PWgCSnU2nTKpaABUM00rDceAA5GiOQDuVSaVBFBKTHASfBWVwMXlxmYIK53HApeyHgCscDcWDBITiaTyar5bSJZVReLTBB0HAwJZ3LAXIbavVaMrAhcYnxPDAENrgM7NHpxYbWk0eQrpHlkw6oANhvkxhNlQhTGq4BqtTqYHqYAmJUm4NaSWS00167bsyM85wnVU4-H3G6PZ5vL40KYJZhg59DeOS4tQxBw5hI9HYz2XQaJS2yQWi9YS1xNY9o7r9ZKU2gnc0AEwzDTZVgAUSaABF7wAFe8ZF8ZXJZDJwAAGhSFFwMByNgMAAGKEuBCCxGKfb-nA6C0JglArqugpaEAA

PR Close #54652
2024-03-04 19:07:47 +01:00
Jessica Janiuk 2fc1971ac6 ci: Add primitives configurations for ng-dev tools (#54663)
This adds the configs to enable validators and google sync patterns for primitives sharing.

PR Close #54663
2024-03-04 18:20:46 +01:00
cexbrayat 9637b5d03d refactor(compiler): cleanup unused code in template builder pipeline (#54654)
This removes some unused code and fixes a few typos across the pipeline code.

PR Close #54654
2024-03-04 18:15:58 +01:00
danieljancar 7ead0fccb0 docs: revise documentation for @for control flow, 'track $index' performance issues explanation, and confusing 'trackBy' mention (#53806)
PR Close #53806
2024-03-04 11:37:41 +01:00
Pawel Kozlowski 6ca8ecad54 docs: queries as signals adev guide (#54505)
Add a angular.dev version of queries as signals guide.

PR Close #54505
2024-03-04 11:11:17 +01:00
Andrew Scott e3bdab12fa refactor(core): Pending effects should make the application unstable (#53835)
In zone-full applications, this is already true because effects are
scheduled inside a microtask and tracked by ZoneJS. When not using
zones, this should stay consistent on principle and for testability
reasons. A general pattern with zoneless testing will be to update state
and `await` some promise (i.e. `fixture.whenStable`, which will be
linked to `ApplicationRef.isStable`).

PR Close #53835
2024-03-04 10:35:42 +01:00
Matthieu Riegler 342737e01f docs(docs-infra): Update tutorial deps (#54677)
PR Close #54677
2024-03-04 10:03:17 +01:00
Hozefa Mangalorewala 0767adc603 docs: fix broken link on the tutorial page (#54669)
PR Close #54669
2024-03-03 13:59:03 +01:00
Swami 37dcf8d658 refactor(core): remove unused EffectRef import statement (#53863)
Remove unused, leftover imports.

PR Close #53863
2024-02-29 14:54:51 +01:00
Alex Rickabaugh ffbafc7d4a fix(core): untrack various core operations (#54614)
One downside of implicit dependency tracking in `effect()`s is that it's easy
to for downstream code to end up running inside the effect context by accident.
For example, if an effect raises an event (e.g. by `next()`ing a `Subject`), the
subscribers to that `Observable` will run inside the effect's reactive context,
and any signals read within the subscriber will end up as dependencies of the
effect. This is why the `untracked` function is useful, to run certain
operations without incidental signal reads ending up tracked.

However, knowing when this is necessary is non-trivial. For example, injecting
a dependency might cause it to be instantiated, which would run the constructor
in the effect context unless the injection operation is untracked.

Therefore, Angular will automatically drop the reactive context within a number
of framework APIs. This commit addresses these use cases:

* creating and destroying views
* creating and destroying DI injectors
* injecting dependencies
* emitting outputs

Fixes #54548

There are likely other APIs which would benefit from this approach, but this
is a start.

PR Close #54614
2024-02-29 11:38:58 +01:00
Kristiyan Kostadinov 917b9bdd2e fix(compiler-cli): unwrap expressions with type parameters in query read property (#54647)
Fixes that a query like `viewChild('locator', {read: ElementRef<HTMLElement>})` would throw because we didn't account for expressions with type parameters.

I've also included support for parenthesized expressions and `as` expressions since it's pretty easy to support them.

Fixes #54645.

PR Close #54647
2024-02-28 18:05:15 +01:00
Tommy Gaessler 7ca08aaa5b docs: updated setting up Zone.js instructions (#52409)
Updated the "Setting up Zone.js" instructions to the latest convention from
how the Angular CLI configures it.
Include adding zone-flats.ts to the tsconfig.app.json file step.
Added src/ to zone-flag.ts in the code example.

PR Close #52409
2024-02-28 17:35:18 +01:00
Kristiyan Kostadinov c19c44d694 docs: fix broken link in AIO (#54649)
Fixes a broken link that is currently breaking the CI.

PR Close #54649
2024-02-28 17:15:11 +01:00
Nikita Barsukov b0f434a893 docs: fix broken markdown syntax (#53567)
PR Close #53567
2024-02-28 15:16:17 +01:00
mgechev c05bef0ee8 docs: update versioning and deprecation policies (#51423)
Add more details around:
- What we consider a breaking change
- What is a deprecation
- How do we guarantee Angular stays stable
- Update invalid links

PR Close #51423
2024-02-28 14:38:41 +01:00
mgechev 3f0e892638 docs: add updating to v17 guide (#52410)
PR Close #52410
2024-02-28 14:19:46 +01:00
Dylan Hunn edc45d5b6f release: cut the v17.2.3 release 2024-02-27 16:23:40 -08:00
Sumit Parakh 79a32816dc fix(core): fix typo in injectors.svg file (#54596)
this commit fixes the unnecessary single quote in injectors.svg file

Closes #54592

PR Close #54596
2024-02-27 15:21:17 -08:00
Angular Robot 11a0a5d017 build: update io_bazel_rules_sass digest to 888dd9c (#54615)
See associated pull request for more information.

PR Close #54615
2024-02-27 14:06:34 -08:00
Doug Parker 89db58691f release: bump Angular DevTools version to 1.0.11 (#54631)
PR Close #54631
2024-02-27 14:02:20 -08:00
Doug Parker 624767c076 Revert "refactor(devtools): implement iframe support for Angular DevTools' browser code (#53934)" (#54629)
This reverts commit dd3dac9cc9.

PR Close #54629
2024-02-27 14:00:17 -08:00
Doug Parker 7b30a869e1 Revert "refactor(devtools): implement multiframe support in devtools page (#53934)" (#54629)
This reverts commit ebcdc8dc96.

PR Close #54629
2024-02-27 14:00:17 -08:00
Doug Parker 986b9c3bea refactor: add --spawn_strategy local to --config debug (#54167)
This seems to be necessary to debug Node tests.

PR Close #54167
2024-02-26 18:36:42 -08:00
Andrew Scott 69598c0c6c refactor(core): Internal render hooks trigger view refresh before executing user hooks (#54224)
This commit ensures that any internal render hooks that cause views to
become dirty again first refresh those dirty views before running user
render hooks. This ensures that user render hooks have the most complete
render state possible and stops them from needlessly executing multiple
times. This is important to maintain the goal of the public render
hooks, which is to provide the safest place to place code that depends
on DOM state, especially in ways that may force a browser paint.

PR Close #54224
2024-02-26 18:31:16 -08:00
Andrew Scott ffe3aa1169 refactor(core): Add helper function for queuing state updates (#54224)
This adds a helper function to defer application state updates to the
first possible "safe" moment. If application-wide change detection
(ApplicationRef.tick) is currently executing when this function is used,
the callback will execute as soon as all views attached to the
`ApplicationRef` have been refreshed. Refreshing the application views
will happen again before `checkNoChanges` executes.

When a change detection is _not_ running, this state update will execute
in the microtask queue.

This function is necessary as a replacement for current
`Promise.resolve().then(() => stateUpdate())` to be zoneless compatible
while ensuring those state updates are synchronized to the DOM before
the browser repaint. Without this, updates done in `Promise.resolve(...)` would
queue another round of change detection in zoneless applications, and
this change detection could happen in the next browser frame, and cause
noticeable flicker for the user.

Additionally, this function provides a way to perform state updates that
will run on the server as well as in the browser.

Last, current applications using `ngZone: 'noop'` may not be
calling `ApplicationRef.tick` at all so this function provides a
mechanism to ensure the state update still happens by racing
a microtask with `afterNextRender` (which might never execute).

PR Close #54224
2024-02-26 18:31:15 -08:00
cexbrayat 2aefed8763 fix(compiler-cli): catch function instance properties in interpolated signal diagnostic (#54325)
Currently, the following template compiles without error, even if the signal is not properly called:

```
<div>{{ mySignal.name }}</div>
```

This is because `name` is one of the instance properties of Function (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function#instance_properties).

The interpolated signal diagnostic is now extended to catch such issues.

PR Close #54325
2024-02-26 18:30:06 -08:00
Angular Robot 377442d90f build: update scorecard action dependencies (#54407)
See associated pull request for more information.

PR Close #54407
2024-02-26 18:29:39 -08:00
cexbrayat 20a549f1de docs: use components in model inputs guide (#54606)
The code example showcases a `@Directive` with a template instead of a `@Component`.

PR Close #54606
2024-02-26 18:28:15 -08:00
Kristiyan Kostadinov daf7c611b2 fix(compiler-cli): identify aliased initializer functions (#54609)
Fixes that initializer functions weren't being recognized if they are aliased (e.g. `import {model as alias} from '@angular/core';`).

To do this efficiently, I had to introduce the `ImportedSymbolsTracker` which scans the top-level imports of a file and allows them to be checked quickly, without having to go through the type checker. It will be useful in the future when verifying that that initializer APIs aren't used in unexpected places.

I've also introduced tests specifically for the `tryParseInitializerApiMember` function so that we can test it in isolation instead of going through the various functions that call into it.

PR Close #54609
2024-02-26 18:27:19 -08:00
Dylan Hunn c1163535d1 Revert "fix(compiler-cli): identify aliased initializer functions (#54480)" (#54595)
This reverts commit f04ecc0cda.

PR Close #54595
2024-02-26 08:36:50 -08:00
Andrew Kushnir 57123524a2 fix(core): collect providers from NgModules while rendering @defer block (#52881)
Currently, when a `@defer` block contains standalone components that import NgModules with providers, those providers are not available to components declared within the same NgModule. The problem is that the standalone injector is not created for the host component (that hosts this `@defer` block), since dependencies become defer-loaded, thus no information is available at host component creation time.

This commit updates the logic to collect all providers from all NgModules used as a dependency for standalone components used within a `@defer` block. When an instance of a defer block is created, a new environment injector instance with those providers is created.

Resolves #52876.

PR Close #52881
2024-02-23 12:30:06 -08:00
Pawel Kozlowski dcd6c8c79b refactor(core): use performance API for signals (#54521)
This commit adds a standard performance marker that can be viewed in Chrome dev tools and other tooling.
See more info at https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark

PR Close #54521
2024-02-23 12:18:31 -08:00
Kristiyan Kostadinov fbb0398795 docs: add a guide for model inputs (#54514)
Adds a guide for `model()` inputs for angular.io. Will port this over to angular.dev once it's finalized.

PR Close #54514
2024-02-23 11:47:04 -08:00
Felix Geißler 56ad65afa7 docs: syntax error in signal input transform example (#54567)
PR Close #54567
2024-02-23 11:46:14 -08:00
loka shafeek b1b7bbffb8 docs: Update wildcard route component to PageNotFoundComponent (#54570)
This PR enhances the Angular documentation by replacing the placeholder `<component-name>` with the actual `PageNotFoundComponent` in the wildcard route configuration. This update ensures that the documentation accurately reflects the recommended practice for handling page-not-found scenarios. Additionally, it addresses the issue where HTML entities like `&lt;` were being displayed instead of the desired `<` symbol, resulting in clearer and more readable documentation for users.
PR Close #54570
2024-02-23 11:45:39 -08:00
Kristiyan Kostadinov 48aec63ee4 fix(compiler-cli): identify aliased initializer functions (#54480)
Fixes that initializer functions weren't being recognized if they are aliased (e.g. `import {model as alias} from '@angular/core';`).

To do this efficiently, I had to introduce the `ImportedSymbolsTracker` which scans the top-level imports of a file and allows them to be checked quickly, without having to go through the type checker. It will be useful in the future when verifying that that initializer APIs aren't used in unexpected places.

I've also introduced tests specifically for the `tryParseInitializerApiMember` function so that we can test it in isolation instead of going through the various functions that call into it.

PR Close #54480
2024-02-23 11:44:40 -08:00
Lukas Matta d68cf175a4 docs: remove mutate function mention (#54569)
PR Close #54569
2024-02-23 11:43:25 -08:00
Matthieu Riegler 89f8331b7e ci: adding Matthieu as approver for ADEV (#54575)
This is to help the team approve changes on the new doc site.

PR Close #54575
2024-02-23 11:42:46 -08:00
Kristiyan Kostadinov 84ade02df6 refactor(compiler-cli): changes to get signal diagnostic working internally (#54585)
The diagnostic for signals that haven't been invoked wasn't working internally, because the path to `@angular/core` is different. These changes resolve the issue and do some general cleanup.

PR Close #54585
2024-02-23 11:42:07 -08:00
Kristiyan Kostadinov dbe673b027 fix(migrations): resolve infinite loop for a single line element with a long tag name and angle bracket on a new line (#54588)
Fixes an edge case where a single-line elemnt with a long tag name a closing bracket on a new line was putting the control flow migration into an infinite loop.

Fixes #54587.

PR Close #54588
2024-02-23 11:40:31 -08:00
Andrew Scott 1a526f2881 perf(common): AsyncPipe should not call markForCheck on subscription (#54554)
This commit prevents `AsyncPipe` from calling `markForCheck` when values
are synchronously emit during subscription to an observable. This
prevents subscriptions to `Replay` observables from needlessly walking
up to the root of the view tree during template execution for each
new replay observable in the template.

PR Close #54554
2024-02-23 09:20:05 -08:00
Alex Rickabaugh 600a8cd59a release: cut the v17.2.2 release 2024-02-21 17:54:30 -08:00
Jessica Janiuk bb57d34110 fix(migrations): Fix cf migration regular expression to include underscores (#54533)
In rare cases people may use an underscore in their component names, which was not accounted for in the formatting portion of the migration.

fixes: #54532

PR Close #54533
2024-02-21 15:25:07 -08:00
Doug Parker ef94bb6bf6 docs(devtools): expand release docs (#54525)
This provides some more information on how to release and notably includes commands for zipping extension code and source code, which can otherwise be tricky to get exactly right.

PR Close #54525
2024-02-21 15:24:27 -08:00
Doug Parker e627c6eaef release: bump Angular DevTools version to 1.0.10 (#54523)
PR Close #54523
2024-02-21 15:23:37 -08:00
Kristiyan Kostadinov 65c9148c71 refactor(compiler-cli): move defer block tests into separate file (#54499)
Splits the tests for `@defer` blocks out into a separate file since the `ngtsc_spec.ts` is getting quite large.

PR Close #54499
2024-02-21 15:22:37 -08:00
Kristiyan Kostadinov 0a3edfb543 fix(compiler-cli): correctly detect deferred dependencies across scoped nodes (#54499)
This is based on an internal issue report.

An earlier change introduced a diagnostic to report cases where a symbol is in the `deferredImports` array, but is used eagerly. The check worked by looking through the deferred blocks in a scope, resolving the scope for each and checking if the element is within the scope. The problem is that resolving the scope won't work across scoped node boundaries. For example, if there's a control flow statement around the block or within the block but around the deferred dependency, it won't be able to resolve the scope since it isn't a direct child, e.g.

```
@if (true) {
  @defer {
   <deferred-dep/>
  }
}
```

To fix this the case where the deferred block is inside a scoped node, I've changed the `R3BoundTarget.deferBlocks` to be a `Map` holding both the deferred block and its corresponding scope. Then to resolve the case where the dependency is within a scoped node inside the deferred block, I've added a depth-first traversal through the scopes within the deferred block.

PR Close #54499
2024-02-21 15:22:37 -08:00
Kristiyan Kostadinov f00336c31f refactor(compiler-cli): add the ability to treat object literals as enums in docs (#54487)
We have a couple of cases now (#53753 and #54414) where we're forced to redefine enums as object literals. These literals aren't rendered in the best way in the docs so these changes introduce a new `object-literal-as-enum` tag that we can use to mark them so they're treated for documentation purposes.

PR Close #54487
2024-02-21 15:21:59 -08:00
Dean Davidson 8997260e29 docs: add closing tags to code examples (#54485)
There were two minor issues in the code examples that this addresses.
PR Close #54485
2024-02-21 15:20:17 -08:00
Lukas Matta 033608e17b docs: fix typos (#54475)
PR Close #54475
2024-02-21 15:19:40 -08:00
vladboisa 926063eb0a docs(docs-infra): fix tab shrinking (#54380)
Fix a behavior when block shrinks on Usage notes tab

Fixes #52818

PR Close #54380
2024-02-21 15:19:14 -08:00
cexbrayat 187b46a3a0 docs: view child static query differences formatting (#54370)
The docs were broken as the first line is the only one to be displayed in the table.

PR Close #54370
2024-02-21 15:18:48 -08:00
Dmitry Ivanitskiy 6447c0eecc fix(compiler): adding the inert property to the "SCHEMA" array (#53148)
This change allows template binding "inert" attribute with the following syntax: [inert]="isInert"

Fixes #51879

fixup! fix(compiler): adding the inert property to the "SCHEMA" array

revert: "fixup! fix(compiler): adding the inert property to the "SCHEMA" array"

This reverts commit b637b7ce646e8bab2f585339028a84018e8ea982.

This commit is being reverted because the inert property is safe as a boolean attribute

PR Close #53148
2024-02-21 15:13:56 -08:00
Matthieu Riegler ed8857e46f refactor(platform-browser): use performance API for async animations (#53963)
This commit adds a standard performance marker

PR Close #53963
2024-02-21 15:12:05 -08:00
Andrew Scott 3e31f1a34e fix(router): Clear internal transition when navigation finalizes (#54261)
This commit fixes a small memory issue in the router where a destroyed
component instance would be retained.

fixes #54241

PR Close #54261
2024-02-21 14:09:58 -08:00
cexbrayat d411ee735a docs: typos in signal queries guide (#54540)
PR Close #54540
2024-02-21 15:59:01 +00:00
Kristiyan Kostadinov d34e3298db fix(common): image placeholder not removed in OnPush component (#54515)
Fixes that the placeholder wasn't being removed when an optimized image is placed in an `OnPush` component.

Fixes #54478.

PR Close #54515
2024-02-20 09:50:00 -08:00
Pawel Kozlowski db35266021 test(core): more tests for queries as signals (#54508)
A couple of tests that illustrate combination of signal
and decorator queries in once component.

PR Close #54508
2024-02-20 09:49:20 -08:00
JoostK 790f4f7c26 fix(compiler-cli): use correct symbol name for default imported symbols in defer blocks (#54495)
This commit addresses a problem with PR #53695 that introduced support for default imports,
where the actual dynamic import used in the defer loading function continued to use the
symbol name, instead of `.default` for the dynamic import. This issue went unnoticed in the
testcase because a proper instance was being generated for the `ɵsetClassMetadataAsync` function,
but not the generated dependency loader function.

Fixes #54491

PR Close #54495
2024-02-20 09:45:33 -08:00
Jamie Couperwhite 5e80e6db1c docs: fix developer preview link in signal queries (#54492)
PR Close #54492
2024-02-20 09:44:43 -08:00
Alex Castle 5597e5d172 docs(common): add new faq entry to NgOptimizedImage docs (#54469)
add new FAQ entry explaining that we're not adding new loaders to the angular repo

PR Close #54469
2024-02-20 09:43:47 -08:00
Ahmed Hakeem f8a5b40489 docs: enhance Dependency injection description (#54464)
PR Close #54464
2024-02-20 09:29:54 -08:00
Pawel Kozlowski 3bd5860c74 fix(core): properly execute content queries for root components (#54457)
Prior to this fix an incorrect view instance (a dynamically created component
one instead of the root view) was passed to the content query function. Having
incorrect view instance meant that a component instance could not be found.

This is a pre-existing bug, introduction of signal-based queries just surfaced it.

Fixes #54450

PR Close #54457
2024-02-15 12:21:29 -08:00
Pawel Kozlowski b975ff1bc2 docs: add guide for signal queries (#54395)
Adding new guide for the signal-based queries.

PR Close #54395
2024-02-15 12:17:01 -08:00
Fabio Heer ba686cc43f docs: correct default application builder for new apps (#54386)
The documentation on angular.dev mentions build-angular:browser as default
application builder. This is not correct. Recent applications are setup
with build-angular:application by default.

PR Close #54386
2024-02-15 10:54:18 -08:00
AleksanderBodurri a7e4b99515 refactor(devtools): implement multiframe support in devtools page (#53934)
In the Angular DevTools Chrome DevTools page:

- Angular DevTools is able to ask the background script to list each frame that has been registered on a page.
- Angular Devtools is able to ask the background script to "enable" the connection on a particular frame. This enables the messaging between the content script <-> background script <-> devtools page
- Implements detection of non unique urls on the inspected page

Limitations:
- The `inspectedWindow.eval` API is only able to target frames by frameURL. This means some features that integrate with Chrome DevTools like inspect element and open source will not be available when inspecting frames that do not have a unique url on the page.

PR Close #53934
2024-02-14 17:15:26 -08:00
AleksanderBodurri 02fcd928b2 refactor(devtools): implement iframe support for Angular DevTools' browser code (#53934)
Modifies the messaging layer of devtools to allow for switching communication between frames on a page. When served as a browser extension.

Design:
- When a page renders, DevTools installs a content script onto it through it's manifest file. The all_frames option is used here to install this script onto every frame in a page.
- When Angular is detected, the content script will install a backend script into it's frame.
- Each content script / backend script pairing is kept track of in the background script. This pairing represents an angular devtools context in a particular frame.
- Angular DevTools is able to ask the background script to list each frame that has been registered on a page.
- Angular Devtools is able to ask the background script to "enable" the connection on a particular frame. This enables the messaging between the content script <-> background script <-> devtools page

Limitations:
- The `inspectedWindow.eval` API is only able to target frames by frameURL. This means some features that integrate with Chrome DevTools like inspect element and open source will not be available when inspecting frames that do not have a unique url on the page.

PR Close #53934
2024-02-14 17:15:26 -08:00
Angular Robot fba7fe175c build: update all non-major dependencies (#54406)
See associated pull request for more information.

PR Close #54406
2024-02-14 15:39:37 -08:00
Joey Perrott e5e8be4d72 ci: remove feature-request bot (#54442)
Remove feature request bot from usage.

PR Close #54442
2024-02-14 15:33:54 -08:00
Kisters-BS 8521188cd1 docs: fix typo (#54427)
PR Close #54427
2024-02-14 15:29:21 -08:00
Paul Gschwendtner 44a0ea4e2d docs: add guide for signal inputs (#54316)
Adds a guide for signal inputs.

We will port that to `angular.dev` once we finalized this version.

PR Close #54316
2024-02-14 15:22:49 -08:00
Andrew Kushnir 3eda005680 release: cut the v17.2.1 release 2024-02-14 14:55:28 -08:00
Alex Rickabaugh 7234824228 fix(compiler-cli): fix broken version detection condition (#54443)
The version detection condition for signal two-way bindings used an OR
instead of an AND, resulting in every `.0` patch version being considered
as supporting two-way bindings to signals.

This commit fixes the logic and adds additional parentheses to ensure the
meaning of the condition is more clear. Long term, we should switch to
semver version parsing instead.

PR Close #54443
2024-02-14 12:47:03 -08:00
Andrew Kushnir 0b95d141ab release: cut the v17.2.0 release 2024-02-14 10:55:44 -08:00
Kristiyan Kostadinov 78b48cc272 refactor(compiler-cli): do not emit signal unwrap calls in versions older than 17.2 (#54423)
In order to allow both signals and non-signals in two-way bindings, we have to pass the expression through `ɵunwrapWritableSignal`. The problem is that the language service uses a bundled compiler that is fairly new, but it may be compiling an older version of Angular that doesn't expose `ɵunwrapWritableSignal` (see https://github.com/angular/vscode-ng-language-service/issues/2001).

These changes add a `_angularCoreVersion` flag to the compiler which the language service can use to pass the parsed Angular version to the compiler which can then decide whether to emit the function.

PR Close #54423
2024-02-13 15:53:42 -08:00
Matthieu Riegler 1c536250b6 fix(http): Use string body to generate transfer cache key. (#54379)
This is particularly usefull for GraphQL queries where the string body might be the only discriminator.

Fixes #54377

PR Close #54379
2024-02-13 11:53:22 -08:00
Joey Perrott 674b6c36b0 fix(docs-infra): don't include prerender flag based on fast/full build in adev (#54400)
The serve command for architect does not support the `--prerender` flag

PR Close #54400
2024-02-13 11:52:40 -08:00
Lukas Matta 35925263bd docs: fix typo (#54403)
PR Close #54403
2024-02-13 11:51:56 -08:00
Kristiyan Kostadinov 501c159e05 build: update node (#54399)
Updates the repo to a newer version of Node to fix errors in the integration tests.

PR Close #54399
2024-02-12 15:36:19 -08:00
Kristiyan Kostadinov 277540c4fc build: reuse fake core in type checking tests (#54344) (#54398)
Currently we have two fake copies of `@angular/core` in the compiler tests which can be out of sync and cause inconsistent tests. These changes reuse a single copy instead.

PR Close #54344

PR Close #54398
2024-02-12 14:31:26 -08:00
Kristiyan Kostadinov 6d00115bf4 fix(core): show placeholder block on the server with immediate trigger (#54394)
Currently all triggers are set up to show the placeholder block on the server, except for `on immediate` which is basically a noop. These changes update `on immediate` to match the rest of the triggers.

Fixes #54385.

PR Close #54394
2024-02-12 11:02:41 -08:00
Kristiyan Kostadinov ef965ac87c test(language-service): add tests for model inputs (#54387)
Updates the language service tests to cover `model()` inputs.

PR Close #54387
2024-02-12 11:01:54 -08:00
Kristiyan Kostadinov 369e96c837 refactor(compiler-cli): split input and model tests (#54387)
Splits up the tests for `input()` and `model()` into separate files.

PR Close #54387
2024-02-12 11:01:54 -08:00
Kristiyan Kostadinov 2363e77002 refactor(compiler-cli): add diagnostic tests for models (#54387)
Sets up type checking diagnostic tests for model() inputs.

PR Close #54387
2024-02-12 11:01:54 -08:00
Kristiyan Kostadinov 1581988d2e test(core): add type tests for ɵunwrapWritableSignal (#54387)
Getting the typing for `ɵunwrapWritableSignal` just right was tricky so these changes add some tests to ensure that we don't regress.

Also reworks the type tester a bit to make it easier to find where to add new test files.

PR Close #54387
2024-02-12 11:01:54 -08:00
Kristiyan Kostadinov 0ecc2d12f1 refactor(core): reuse input signal node for models (#54387)
Reworks the model so that it reuses `INPUT_SIGNAL_NODE` instead of implementing its own.

PR Close #54387
2024-02-12 11:01:53 -08:00
Kristiyan Kostadinov 453e00c9b0 refactor(core): avoid wrapper around subscribe return value (#54387)
Reworks `ModelSignal.subscribe` so it doesn't have to wrap its value to look like a subscription.

PR Close #54387
2024-02-12 11:01:53 -08:00
arturovt 4d722a276f fix(zone.js): patch form-associated custom element callbacks (#50686)
This commit updates the implementation of the `customElements` patch and also
patches FACE callbacks (`formAssociatedCallback`, `formDisabledCallback`, `formResetCallback`
and `formStateRestoreCallback`). This now allows invoking those callbacks in the same zone
where the custom element has been defined.

PR Close #50686
2024-02-12 08:50:57 -08:00
Pawel Kozlowski 744cb1e561 fix(core): return the same children query results if there are no changes (#54392)
Assure that the same readonly array corresponding to the children query results
is returned for cases where a query is marked as dirty but there were no actual
changes to the content of the results array (this can happen if a view is added
and removed thus marking queries as dirty but not influencing final results).

Fixes #54376

PR Close #54392
2024-02-12 08:48:29 -08:00
Joey Perrott deb9ea07c5 refactor(docs-infra): create fast mode (#54364)
Creates a "fast mode" for building the adev site, currently only disabling prerender during fast build.
This is intended to be used for local development.

PR Close #54364
2024-02-09 10:59:08 -08:00
JoostK 62b87b4551 fix(core): do not crash for signal query that does not have any matches (#54353)
The newly introduced signal queries would error if no match exists, due to an
invalid read within the query internals.

This commit addresses the crash by allowing there to be no matches.

PR Close #54353
2024-02-09 14:59:51 +00:00
Hakeem 6c278a9069 docs(docs-infra): change deprecated modules' headers' style in the hover state (#54337)
PR Close #54337
2024-02-09 14:58:56 +00:00
Joey Perrott 5de985a565 fix(docs-infra): include manually defined api reference docs in adev (#54356)
Include the manual defined api reference docs in adev

PR Close #54356
2024-02-09 14:58:29 +00:00
Angular Robot 85c05514be build: update cross-repo angular dependencies (#54186)
See associated pull request for more information.

PR Close #54186
2024-02-09 14:57:46 +00:00
Paul Gschwendtner 7aa92fa8fe release: cut the v17.2.0-rc.1 release 2024-02-09 11:54:07 +01:00
Kristiyan Kostadinov 4b96f370ee fix(core): expose model signal subcribe for type checking purposes (#54357)
The `@internal` in the comment above `ModelSignal.subscribe` ended up marking the method as internal even though it wasn't meant to be.

PR Close #54357
2024-02-09 09:58:56 +00:00
Matthieu Riegler 9e2c1405d1 refactor(devtools): show tooltip for hydration icon on directive forest (#54326)
The MatTooltip was missing after migrating to standalone

PR Close #54326
2024-02-08 19:18:37 +00:00
Joey Perrott 9f05732656 refactor: migrate router to prettier formatting (#54318)
Migrate formatting to prettier for router from clang-format

PR Close #54318
2024-02-08 19:17:14 +00:00
Jessica Janiuk ed41bfcad7 release: cut the v17.2.0-rc.0 release 2024-02-08 10:38:52 -08:00
1507 changed files with 19461 additions and 16733 deletions
+42
View File
@@ -0,0 +1,42 @@
# This workflow builds the AIO previews for pull requests when a certain label is applied.
# The actual deployment happens as part of a dedicated second workflow to avoid security
# issues where the building would otherwise occur in an authorized context where secrets
# could be leaked. More details can be found here:
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
name: Build AIO app for preview deployment
on:
pull_request:
types: [synchronize, labeled]
permissions: read-all
jobs:
aio-build:
runs-on: ubuntu-latest
# We only want to build and deploy the AIO app if the `aio: preview` label has been
# added, or if the label is already applied and new changes have been made in the PR.
if: |
(github.event.action == 'labeled' && github.event.label.name == 'aio: preview') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'aio: preview'))
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: ./.github/actions/yarn-install
- uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
with:
bazelrc: ./.bazelrc.user
# Build the web package. Note: We run Bazel from a low-resource Github action container,
# so we manually need to instruct Bazel to run more actions concurrently as by default
# the number of concurrent actions is determined based on the host resources.
- run: bazel build //aio:build --jobs=32 --announce_rc --verbose_failures
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@c83e99a12397014162531ca125c94549db55dd84
with:
workflow-artifact-name: 'aio'
pull-number: '${{github.event.pull_request.number}}'
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
deploy-directory: './dist/bin/aio/build'
+44
View File
@@ -0,0 +1,44 @@
# This workflow runs whenever the AIO build workflow has completed. Deployment happens
# as part of a dedicated second workflow to avoid security issues where the building would
# otherwise occur in an authorized context where secrets could be leaked.
#
# More details can be found here:
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
name: Deploying AIO preview to Firebase
on:
workflow_run:
workflows: ['Build AIO app for preview deployment']
types: [completed]
permissions:
# Needed in order to be able to comment on the pull request.
pull-requests: write
env:
PREVIEW_PROJECT: ng-dev-previews
PREVIEW_SITE: ng-dev-previews-fw
jobs:
aio-deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- name: Configure Firebase deploy target
working-directory: aio/
run: |
# We can use `npx` as the Firebase deploy actions uses it too.
npx -y firebase-tools@latest target:clear --project ${{env.PREVIEW_PROJECT}} hosting aio
npx -y firebase-tools@latest target:apply --project ${{env.PREVIEW_PROJECT}} hosting aio ${{env.PREVIEW_SITE}}
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@c83e99a12397014162531ca125c94549db55dd84
with:
github-token: '${{secrets.GITHUB_TOKEN}}'
workflow-artifact-name: 'aio'
firebase-config-dir: './aio/'
firebase-public-dir: './aio/dist'
firebase-project-id: '${{env.PREVIEW_PROJECT}}'
firebase-service-key: '${{secrets.FIREBASE_PREVIEW_SERVICE_TOKEN}}'
+88 -32
View File
@@ -35,6 +35,8 @@ jobs:
run: yarn install --frozen-lockfile --cwd aio
- name: Check code lint
run: yarn -s tslint
- name: Check code lint in aio
run: yarn -s --cwd aio lint
- name: Check for circular dependencies
run: yarn -s ts-circular-deps:check
- name: Validate pull approve configuration
@@ -88,6 +90,26 @@ jobs:
- name: Run CI tests for framework
run: yarn test:ci
aio:
runs-on: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules for aio
run: yarn install --cwd aio --frozen-lockfile
- name: Run AIO tests with upstream packages
run: yarn --cwd aio test:ci
- name: Check generated bundle sizes
run: yarn --cwd aio payload-size
adev:
runs-on:
labels: ubuntu-latest-4core
@@ -103,6 +125,72 @@ jobs:
- name: Build adev to ensure it continues to work
run: yarn bazel test --config=aio_local_deps //adev:test
aio-local:
runs-on:
labels: ubuntu-latest-4core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules
run: yarn install --cwd aio --frozen-lockfile
- name: Run AIO tests with local packages
run: yarn --cwd aio test-local:ci
- name: Check generated bundle sizes
run: yarn --cwd aio payload-size aio-local
aio-deploy:
needs: [aio]
if: needs.aio.result == 'success' && github.event_name == 'push'
runs-on:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
with:
cache-node-modules: true
node-module-directories: |
./aio/node_modules
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules for aio
run: yarn install --cwd aio --frozen-lockfile
- name: Set the stable branch environment variable
id: latest_version
run: |
echo "CI_STABLE_BRANCH=$(npm info @angular/core dist-tags.latest | sed -r 's/^\s*([0-9]+\.[0-9]+)\.[0-9]+.*$/\1.x/')" >> $GITHUB_OUTPUT
echo "CI_BRANCH=$(echo ${{ github.event.ref }} | cut -d '/' -f 3)" >> $GITHUB_OUTPUT
- name: Deploy aio to production
env:
CI_AIO_MIN_PWA_SCORE: '95'
CI_COMMIT: ${{ github.event.after }}
CI_REPO_NAME: 'angular'
CI_PULL_REQUEST: 'false'
CI_REPO_OWNER: 'angular'
CI_BUILD_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
CI_STABLE_BRANCH: ${{ steps.latest_version.outputs.CI_STABLE_BRANCH }}
CI_BRANCH: ${{ steps.latest_version.outputs.CI_BRANCH }}
CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN: ${{ secrets.AIO_DEPLOY_TOKEN }}
CI_SECRET_PAYLOAD_FIREBASE_TOKEN: ${{ secrets.AIO_PAYLOAD_TOKEN }}
run: yarn --cwd aio deploy-production
- name: Notify about failed deployment
if: ${{ failure() }}
uses: slackapi/slack-github-action@2a8087d4af6f83146a87539a70defe909fe6dbe6 # v=v1.24.0
with:
channel-id: 'C07DT5M6V,CKV1F72BG'
slack-message: 'Deploy to aio job failed for ${{ github.base_ref }} branch failed on build ${{ github.event.after }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
env:
SLACK_BOT_TOKEN: ${{ secrets.ANGULAR_ROBOT_SLACK_TOKEN }}
publish-snapshots:
if: github.event_name == 'push'
runs-on:
@@ -197,35 +285,3 @@ jobs:
run: |
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "fixme-saucelabs", ...)')
yarn bazel test --config=saucelabs --jobs=$JOBS ${TESTS}
adev-deploy:
needs: [adev]
if: needs.adev.result == 'success' && github.event_name == 'push' && github.ref_name == 'main'
env:
DEPLOY_PROJECT: angular-dev-site
DEPLOY_SITE: angular-dev-site
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c83e99a12397014162531ca125c94549db55dd84
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c83e99a12397014162531ca125c94549db55dd84
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Build adev to ensure it continues to work
run: yarn bazel build --config=aio_local_deps //adev:build
- name: Move generated project files into deployment directory
run: cp -r dist/bin/adev/build adev/build
- name: Setup firebase context
run: |
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{ env.DEPLOY_PROJECT }} hosting angular-docs
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{ env.DEPLOY_PROJECT }} hosting angular-docs ${{ env.DEPLOY_SITE }}
- name: Deploy to firebase
uses: FirebaseExtended/action-hosting-deploy@120e124148ab7016bec2374e5050f15051255ba2 # v0.7.1
with:
entryPoint: './adev'
firebaseServiceAccount: ${{ secrets.ANGULAR_DEV_SITE_DEPLOY }}
projectId: ${{ env.DEPLOY_PROJECT }}
channelId: live
+48
View File
@@ -0,0 +1,48 @@
# Update the `events.json` file that powers the [AIO events page](https://angular.io/events) (if
# necessary) and create a pull request.
#
# For more details on the overall process, see
# [aio/scripts/generate-events/README.md](../../aio/scripts/generate-events/README.md).
name: Update AIO events
on:
workflow_dispatch:
inputs: {}
schedule:
# Run every day at 15:00.
- cron: '0 15 * * *'
# Declare default permissions as read only.
permissions:
contents: read
jobs:
update_events:
name: Update `events.json` (if necessary)
if: github.repository == 'angular/angular'
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
# Setting `persist-credentials: false` prevents the github-action account from being the
# account that is attempted to be used for authentication, instead the remote is set to
# an authenticated URL.
persist-credentials: false
# This is needed as otherwise the PR creation will fail with `shallow update not allowed` when the forked branch is not in sync.
fetch-depth: 0
- name: Generate `events.json`
run: node aio/scripts/generate-events/index.mjs --ignore-invalid-dates
- name: Create a PR (if necessary)
uses: angular/dev-infra/github-actions/create-pr-for-changes@c83e99a12397014162531ca125c94549db55dd84
with:
branch-prefix: docs-update-events
pr-title: 'docs: update events'
pr-description: |
Generated `events.json` with the latest events retrieved from the Firebase DB.
pr-labels: |
action: review
area: docs
target: patch
angular-robot-token: ${{ secrets.ANGULAR_ROBOT_ACCESS_TOKEN }}
-1
View File
@@ -7,7 +7,6 @@ export const format: FormatConfig = {
'prettier': {
'matchers': [
'**/*.{yaml,yml}',
'adev/**/*.{js,ts}',
'devtools/**/*.{js,ts}',
'tools/**/*.{js,ts}',
'modules/**/*.{js,ts}',
+1
View File
@@ -29,6 +29,7 @@
"packages/docs/**",
"packages/elements/schematics/**",
"packages/examples/**",
"packages/language-service/**",
"packages/localize/**",
"packages/private/**",
"packages/service-worker/**",
+26 -5
View File
@@ -1085,7 +1085,7 @@ groups:
- crisbeto
- atscott
- pkozlowski-opensource
- ~JeanMeche
- ~jeanmeche
# =========================================================
# Docs-infra
@@ -1164,7 +1164,7 @@ groups:
- josephperrott
- mgechev
- twerske
- ~JeanMeche
- ~jeanmeche
# =========================================================
# Dev-infra
@@ -1397,6 +1397,27 @@ groups:
author_value: 0 # The author of the PR cannot provide an approval for themself
reviewed_for: ignored # All reviews apply to this group whether noted via Reviewed-for or not
reviewers:
teams:
# Any member of the team can provide a review to perform the minimum review required
- team
users:
- alan-agius4 # Alan Agius
- AleksanderBodurri # Aleksander Bodurri
- alxhub # Alex Rickabaugh
- AndrewKushnir # Andrew Kushnir
- andrewseguin # Andrew Seguin
- atscott # Andrew Scott
- clydin # Charles Lyding
- crisbeto # Kristiyan Kostadinov
- devversion # Paul Gschwendtner
- dgp1130 # Doug Parker
- dylhunn # Dylan Hunn
- jeanmeche # Matthieu Riegler
- jelbourn # Jeremy Elbourn
- jessicajaniuk # Jessica Janiuk
- JiaLiPassion # Jia Li
- JoostK # Joost Koehoorn
- josephperrott # Joey Perrott
- MarkTechson # Mark Thompson (Techson)
- mgechev # Minko Gechev
- mmalerba # Miles Malerba
- pkozlowski-opensource # Pawel Kozlowski
- twerske # Emma Twersky
- zarend # Zach Arend
+5
View File
@@ -5,7 +5,12 @@
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"BazelBuild.vscode-bazel",
"gkalpak.aio-docs-utils",
"ms-vscode.vscode-typescript-tslint-plugin",
"xaver.clang-format",
// The following extensions are useful when working on angular.io (i.e. inside the `aio/` directory).
//"angular.ng-template",
//"dbaeumer.vscode-eslint",
//"errata-ai.vale-server",
],
}
+62 -127
View File
@@ -1,28 +1,3 @@
<a name="17.3.0-rc.0"></a>
# 17.3.0-rc.0 (2024-03-06)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [5afa4f0ec1](https://github.com/angular/angular/commit/5afa4f0ec1b64b88ef875d48bd143e0f36e0a955) | fix | support `ModuleWithProviders` literal detection with `typeof` ([#54650](https://github.com/angular/angular/pull/54650)) |
| [ae7dbe42de](https://github.com/angular/angular/commit/ae7dbe42de5779e578fb636938699b8ba85b33f6) | fix | unwrap expressions with type parameters in query read property ([#54647](https://github.com/angular/angular/pull/54647)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [fb540e169a](https://github.com/angular/angular/commit/fb540e169a78a61f38d611f538eea8fdb0971f1d) | feat | add migration for invalid two-way bindings ([#54630](https://github.com/angular/angular/pull/54630)) |
| [c687b8f453](https://github.com/angular/angular/commit/c687b8f4531252cd1c3dfbb9a7bd42bdbe666a36) | feat | expose new `output()` API ([#54650](https://github.com/angular/angular/pull/54650)) |
| [c809069f21](https://github.com/angular/angular/commit/c809069f213244afd0e2d803a6a43510b218e6f5) | feat | introduce `outputFromObservable()` interop function ([#54650](https://github.com/angular/angular/pull/54650)) |
| [aff65fd1f4](https://github.com/angular/angular/commit/aff65fd1f4a61ed76a6f9b623852f197eb3500e4) | feat | introduce `outputToObservable` interop helper ([#54650](https://github.com/angular/angular/pull/54650)) |
| [33a6fab094](https://github.com/angular/angular/commit/33a6fab094205cc74aceb916e45c8afa22293cf4) | fix | apply TestBed provider overrides to `@defer` dependencies ([#54667](https://github.com/angular/angular/pull/54667)) |
| [78e69117f0](https://github.com/angular/angular/commit/78e69117f07e8ae11aab3abe0343df815b3649be) | fix | generic inference for signal inputs may break with `--strictFunctionTypes` ([#54652](https://github.com/angular/angular/pull/54652)) |
| [7243c704cf](https://github.com/angular/angular/commit/7243c704cf8a4986fae419793027458e142658f0) | fix | return a readonly signal on `asReadonly`. ([#54706](https://github.com/angular/angular/pull/54706)) |
| [ffad7b8ea9](https://github.com/angular/angular/commit/ffad7b8ea9d1286ddb3ee7a2dac7dd33fd76b3aa) | fix | untrack various core operations ([#54614](https://github.com/angular/angular/pull/54614)) |
### router
| Commit | Type | Description |
| -- | -- | -- |
| [c1c7384e02](https://github.com/angular/angular/commit/c1c7384e02becc623c6a42985f7178ca98137264) | feat | Add reusable types for router guards ([#54580](https://github.com/angular/angular/pull/54580)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.2.4"></a>
# 17.2.4 (2024-03-06)
### compiler-cli
@@ -63,46 +38,6 @@
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.3.0-next.1"></a>
# 17.3.0-next.1 (2024-02-27)
### common
| Commit | Type | Description |
| -- | -- | -- |
| [707bfc9b32](https://github.com/angular/angular/commit/707bfc9b326f321f302dccfdfebef7380c914eb5) | perf | `AsyncPipe` should not call `markForCheck` on subscription ([#54554](https://github.com/angular/angular/pull/54554)) |
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [1a6beae8a2](https://github.com/angular/angular/commit/1a6beae8a2bdcff27d4c1e402f98246a52247906) | feat | Enable template pipeline by default. ([#54571](https://github.com/angular/angular/pull/54571)) |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [12dc4d074e](https://github.com/angular/angular/commit/12dc4d074e63edaff626003ad6136a8d122b2ba6) | fix | account for as expression in docs extraction ([#54414](https://github.com/angular/angular/pull/54414)) |
| [f578889ca2](https://github.com/angular/angular/commit/f578889ca2dbb22f2e050d4c4af6aa2d597bf70d) | fix | catch function instance properties in interpolated signal diagnostic ([#54325](https://github.com/angular/angular/pull/54325)) |
| [f04ecc0cda](https://github.com/angular/angular/commit/f04ecc0cdaeb3a292a748b1ccc94ce70a573fc79) | fix | identify aliased initializer functions ([#54480](https://github.com/angular/angular/pull/54480)) |
| [f5c566c079](https://github.com/angular/angular/commit/f5c566c0793eacf9ca146c8a6b8da15b0e8f4c4d) | fix | identify aliased initializer functions ([#54609](https://github.com/angular/angular/pull/54609)) |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [331b16efd2](https://github.com/angular/angular/commit/331b16efd2f5af876e6dc0ad2474ee7a87b00de5) | feat | add API to inject attributes on the host node ([#54604](https://github.com/angular/angular/pull/54604)) |
| [974958913c](https://github.com/angular/angular/commit/974958913ca632971f878a045537472f2c99c665) | feat | support TypeScript 5.4 ([#54414](https://github.com/angular/angular/pull/54414)) |
| [dcb9deb363](https://github.com/angular/angular/commit/dcb9deb3631ff4e839f738f2fb97ca6b894256ef) | fix | collect providers from NgModules while rendering `@defer` block ([#52881](https://github.com/angular/angular/pull/52881)) |
| [ff40c9f762](https://github.com/angular/angular/commit/ff40c9f762b2c14870ac2859201ece9660087e79) | fix | fix typo in injectors.svg file ([#54596](https://github.com/angular/angular/pull/54596)) |
### migrations
| Commit | Type | Description |
| -- | -- | -- |
| [71e0c7df69](https://github.com/angular/angular/commit/71e0c7df695f274a54d5ffb9dfc0587d06d3a953) | fix | resolve infinite loop for a single line element with a long tag name and angle bracket on a new line ([#54588](https://github.com/angular/angular/pull/54588)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.3.0-next.0"></a>
# 17.3.0-next.0 (2024-02-21)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [da7fbb40f0](https://github.com/angular/angular/commit/da7fbb40f06e6e37504f69e7b335f8219f424de2) | fix | detect when the linker is working in unpublished angular and widen supported versions ([#54439](https://github.com/angular/angular/pull/54439)) |
<!-- CHANGELOG SPLIT MARKER -->
<a name="17.2.2"></a>
# 17.2.2 (2024-02-21)
### common
@@ -215,7 +150,7 @@
<a name="17.1.2"></a>
# 17.1.2 (2024-01-31)
###
###
| Commit | Type | Description |
| -- | -- | -- |
| [ccddacf11d](https://github.com/angular/angular/commit/ccddacf11deaebeda12e1bdb6e93ec401397d352) | fix | cta clickability issue in adev homepage. ([#52905](https://github.com/angular/angular/pull/52905)) |
@@ -577,9 +512,9 @@ Live long and prosper 🖖🏻
[Blog post "Angular v17 is now available"](http://goo.gle/angular-v17).
## Breaking Changes
###
###
- Node.js v16 support has been removed and the minimum support version has been bumped to 18.13.0.
Node.js v16 is planned to be End-of-Life on 2023-09-11. Angular will stop supporting Node.js v16 in Angular v17. For Node.js release schedule details, please see: https://github.com/nodejs/release#release-schedule
### common
- the NgSwitch directive now defaults to the === equality operator,
@@ -593,30 +528,30 @@ Live long and prosper 🖖🏻
- The `mutate` method was removed from the `WritableSignal` interface and completely
dropped from the public API surface. As an alternative, please use the `update` method and
make immutable changes to the object.
Example before:
```typescript
items.mutate(itemsArray => itemsArray.push(newItem));
```
Example after:
```typescript
items.update(itemsArray => [itemsArray, …newItem]);
```
- The `mutate` method was removed from the `WritableSignal` interface and completely
dropped from the public API surface. As an alternative please use the update method and
make immutable changes to the object.
Example before:
```typescript
items.mutate(itemsArray => itemsArray.push(newItem));
```
Example after:
```typescript
items.update(itemsArray => [itemsArray, …newItem]);
```
@@ -632,7 +567,7 @@ Live long and prosper 🖖🏻
`detectChanges` on that component's `ChangeDetectorRef`.
### platform-browser
- `REMOVE_STYLES_ON_COMPONENT_DESTROY` default value is now `true`. This causes CSS of components to be removed from the DOM when destroyed. You retain the previous behaviour by providing the `REMOVE_STYLES_ON_COMPONENT_DESTROY` injection token.
```ts
import {REMOVE_STYLES_ON_COMPONENT_DESTROY} from '@angular/platform-browser';
...
@@ -655,13 +590,13 @@ Live long and prosper 🖖🏻
This should instead be configured through the provideRouter or RouterModule.forRoot APIs.
- The following Router properties have been removed from
the public API:
- canceledNavigationResolution
- paramsInheritanceStrategy
- titleStrategy
- urlUpdateStrategy
- malformedUriErrorHandler
These should instead be configured through the `provideRouter` or
`RouterModule.forRoot` APIs.
- The `setupTestingRouter` function has been removed. Use
@@ -672,7 +607,7 @@ Live long and prosper 🖖🏻
handled in the `UrlSerializer.parse` method.
### zone.js
- Deep and legacy `dist/` imports like `zone.js/bundles/zone-testing.js` and `zone.js/dist/zone` are no longer allowed. `zone-testing-bundle` and `zone-testing-node-bundle` are also no longer part of the package.
The proper way to import `zone.js` and `zone.js/testing` is:
```js
import 'zone.js';
@@ -683,7 +618,7 @@ Live long and prosper 🖖🏻
- The `AnimationDriver.NOOP` symbol is deprecated, use `NoopAnimationDriver` instead.
### core
- `ChangeDetectorRef.checkNoChanges` is deprecated.
Test code should use `ComponentFixture` instead of `ChangeDetectorRef`.
Application code should not call `ChangeDetectorRef.checkNoChanges` directly.
- Swapping out the context object for `EmbeddedViewRef`
@@ -692,12 +627,12 @@ Live long and prosper 🖖🏻
simple assignments in most cases, or `Object.assign` , or alternatively
still replace the full object by using a `Proxy` (see `NgTemplateOutlet`
as an example).
Also adds a warning if the deprecated
- NgProbeToken
The `NgProbeToken` is not used internally since the transition from View Engine to Ivy. The token has no utility and can be removed from applications and libraries.
###
###
| Commit | Type | Description |
| -- | -- | -- |
| [59aa0634f4](https://github.com/angular/angular/commit/59aa0634f4d4694203f2a69c40017fe5a3962514) | build | remove support for Node.js v16 ([#51755](https://github.com/angular/angular/pull/51755)) |
@@ -1311,7 +1246,7 @@ Live long and prosper 🖖🏻
[Blog post "Angular v16 is now available"](https://goo.gle/angular-v16).
## Breaking Changes
###
###
- Angular Compatibility Compiler (ngcc) has been removed and as a result Angular View Engine libraries will no longer work
- Deprecated `EventManager` method `addGlobalEventListener` has been removed as it is not used by Ivy.
### bazel
@@ -1334,14 +1269,14 @@ Live long and prosper 🖖🏻
The ideal fix would be to update the code to instead be compatible with
`MockPlatformLocation` instead.
- If the 'ngTemplateOutletContext' is different from the context, it will result in a compile-time error.
Before the change, the following template was compiling:
```typescript
interface MyContext {
$implicit: string;
}
@Component({
standalone: true,
imports: [NgTemplateOutlet],
@@ -1359,7 +1294,7 @@ Live long and prosper 🖖🏻
}
```
However, it does not compile now because the 'xxx' property does not exist in 'MyContext', resulting in the error: 'Type '{ $implicit: string; xxx: string; }' is not assignable to type 'MyContext'.'
The solution is either:
- add the 'xxx' property to 'MyContext' with the correct type or
- add '$any(...)' inside the template to make the error disappear. However, adding '$any(...)' does not correct the error but only preserves the previous behavior of the code.
@@ -1378,7 +1313,7 @@ Live long and prosper 🖖🏻
value differs from the previous call to `setInput`.
- `RendererType2.styles` no longer accepts a nested arrays.
- The `APP_ID` token value is no longer randomly generated. If you are bootstrapping multiple application on the same page you will need to set to provide the `APP_ID` yourself.
```ts
bootstrapApplication(ComponentA, {
providers: [
@@ -1389,21 +1324,21 @@ Live long and prosper 🖖🏻
```
- The `ReflectiveInjector` and related symbols were removed. Please update the code to avoid references to the `ReflectiveInjector` symbol. Use `Injector.create` as a replacement to create an injector instead.
- Node.js v14 support has been removed
Node.js v14 is planned to be End-of-Life on 2023-04-30. Angular will stop supporting Node.js v14 in Angular v16. Angular v16 will continue to officially support Node.js versions v16 and v18.
### platform-browser
- The deprecated `BrowserTransferStateModule` was removed, since it's no longer needed. The `TransferState` class can be injected without providing the module. The `BrowserTransferStateModule` was empty starting from v14 and you can just remove the reference to that module from your applications.
### platform-server
- Users that are using SSR with JIT mode will now need to add `import to @angular/compiler` before bootstrapping the application.
**NOTE:** this does not effect users using the Angular CLI.
- `renderApplication` method no longer accepts a root component as first argument. Instead, provide a bootstrapping function that returns a `Promise<ApplicationRef>`.
Before
```ts
const output: string = await renderApplication(RootComponent, options);
```
Now
```ts
const bootstrap = () => bootstrapApplication(RootComponent, appConfig);
@@ -1438,14 +1373,14 @@ Live long and prosper 🖖🏻
## Deprecations
### core
- `makeStateKey`, `StateKey` and `TransferState` exports have been moved from `@angular/platform-browser` to `@angular/core`. Please update the imports.
```diff
- import {makeStateKey, StateKey, TransferState} from '@angular/platform-browser';
+ import {makeStateKey, StateKey, TransferState} from '@angular/core';
```
- `EnvironmentInjector.runInContext` is now deprecated, with
`runInInjectionContext` functioning as a direct replacement:
```typescript
// Previous method version (deprecated):
envInjector.runInContext(fn);
@@ -1458,7 +1393,7 @@ Live long and prosper 🖖🏻
### platform-browser
- `BrowserModule.withServerTransition` has been deprecated. `APP_ID` should be used instead to set the application ID.
NB: Unless, you render multiple Angular applications on the same page, setting an application ID is not necessary.
Before:
```ts
imports: [
@@ -1466,7 +1401,7 @@ Live long and prosper 🖖🏻
...
]
```
After:
```ts
imports: [
@@ -1478,7 +1413,7 @@ Live long and prosper 🖖🏻
- `ApplicationConfig` has moved, please import `ApplicationConfig` from `@angular/core` instead.
### platform-server
- `PlatformConfig.baseUrl` and `PlatformConfig.useAbsoluteUrl` platform-server config options are deprecated as these were not used.
###
###
| Commit | Type | Description |
| -- | -- | -- |
| [48aa96ea13](https://github.com/angular/angular/commit/48aa96ea13ebfadf2f6b13516c7702dae740a7be) | refactor | remove Angular Compatibility Compiler (ngcc) ([#49101](https://github.com/angular/angular/pull/49101)) |
@@ -1787,11 +1722,11 @@ Alan Agius, Andrew Kushnir, Aristeidis Bampakos, Craig Spence, Doug Parker, Ivá
<a name="15.2.0"></a>
# 15.2.0 (2023-02-22)
## Deprecations
###
###
- Class and `InjectionToken` guards and resolvers are
deprecated. Instead, write guards as plain JavaScript functions and
inject dependencies with `inject` from `@angular/core`.
###
###
| Commit | Type | Description |
| -- | -- | -- |
| [926c35f4ac](https://github.com/angular/angular/commit/926c35f4ac70f5e4d142e545d6d056dd67aac97b) | docs | Deprecate class and InjectionToken and resolvers ([#47924](https://github.com/angular/angular/pull/47924)) |
@@ -1956,21 +1891,21 @@ Alan Agius, Alex Rickabaugh, Andrew Scott, Aristeidis Bampakos, Bob Watson, Jens
- CanLoad guards in the Router are deprecated. Use CanMatch
instead.
- router writable properties
The following strategies are meant to be configured by registering the
application strategy in DI via the `providers` in the root `NgModule` or
`bootstrapApplication`:
* `routeReuseStrategy`
* `titleStrategy`
* `urlHandlingStrategy`
The following options are meant to be configured using the options
available in `RouterModule.forRoot` or `provideRouter`.
* `onSameUrlNavigation`
* `paramsInheritanceStrategy`
* `urlUpdateStrategy`
* `canceledNavigationResolution`
The following options are available in `RouterModule.forRoot` but not
available in `provideRouter`:
* `malformedUriErrorHandler` - This was found to not be used anywhere
@@ -2165,23 +2100,23 @@ Andrew Kushnir
- Keyframes names are now prefixed with the component's "scope name".
For example, the following keyframes rule in a component definition,
whose "scope name" is host-my-cmp:
@keyframes foo { ... }
will become:
@keyframes host-my-cmp_foo { ... }
Any TypeScript/JavaScript code which relied on the names of keyframes rules
will no longer match.
The recommended solutions in this case are to either:
- change the component's view encapsulation to the `None` or `ShadowDom`
- define keyframes rules in global stylesheets (e.g styles.css)
- define keyframes rules programmatically in code.
### compiler-cli
- Invalid constructors for DI may now report compilation errors
When a class inherits its constructor from a base class, the compiler may now
report an error when that constructor cannot be used for DI purposes. This may
either be because the base class is missing an Angular decorator such as
@@ -2190,7 +2125,7 @@ Andrew Kushnir
These situations used to behave unexpectedly at runtime, where the class may be
constructed without any of its constructor parameters, so this is now reported
as an error during compilation.
Any new errors that may be reported because of this change can be resolved either
by decorating the base class from which the constructor is inherited, or by adding
an explicit constructor to the class for which the error is reported.
@@ -2224,16 +2159,16 @@ Andrew Kushnir
### core
- - The ability to pass an `NgModule` to the `providedIn` option for
`@Injectable` and `InjectionToken` is now deprecated.
`providedIn: NgModule` was intended to be a tree-shakable alternative to
NgModule providers. It does not have wide usage, and in most cases is used
incorrectly, in circumstances where `providedIn: 'root'` should be
preferred. If providers should truly be scoped to a specific NgModule, use
`NgModule.providers` instead.
- The ability to set `providedIn: 'any'` for an `@Injectable` or
`InjectionToken` is now deprecated.
`providedIn: 'any'` is an option with confusing semantics and is almost
never used apart from a handful of esoteric cases internal to the framework.
- The bit field signature of `Injector.get()` has been deprecated, in favor of the new options object.
@@ -2836,16 +2771,16 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
- Keyframes names are now prefixed with the component's "scope name".
For example, the following keyframes rule in a component definition,
whose "scope name" is host-my-cmp:
@keyframes foo { ... }
will become:
@keyframes host-my-cmp_foo { ... }
Any TypeScript/JavaScript code which relied on the names of keyframes rules
will no longer match.
The recommended solutions in this case are to either:
- change the component's view encapsulation to the `None` or `ShadowDom`
- define keyframes rules in global stylesheets (e.g styles.css)
@@ -2855,23 +2790,23 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
- Support for Node.js v12 has been removed as it will become EOL on 2022-04-30. Please use Node.js v14.15 or later.
- TypeScript versions older than 4.6 are no longer supported.
- Forms [email] input coercion
Forms [email] input value will be considered as true if it is defined with any value rather
than false and 'false'.
- Since Ivy, TestBed doesn't use AOT summaries. The `aotSummaries` fields in TestBed APIs were present, but unused. The fields were deprecated in previous major version and in v14 those fields are removed. The `aotSummaries` fields were completely unused, so you can just drop them from the TestBed APIs usage.
### forms
- Forms classes accept a generic.
Forms model classes now accept a generic type parameter. Untyped versions of these classes are available to opt-out of the new, stricter behavior.
- objects with a length key set to zero will no longer validate as empty.
This is technically a breaking change, since objects with a key `length` and value `0` will no longer validate as empty. This is a very minor change, and any reliance on this behavior is probably a bug anyway.
### http
- Queries including + will now actually query for + instead of space.
Most workarounds involving custom codecs will be unaffected.
Possible server-side workarounds will need to be undone.
- JSONP will throw an error when headers are set on a reques
JSONP does not support headers being set on requests. Before when
a request was sent to a JSONP backend that had headers set the headers
were ignored. The JSONP backend will now throw an error if it
@@ -2882,7 +2817,7 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
- This change may cause a breaking change in unit tests that are implicitly depending on a specific
number and sequence of change detections in order for their assertions to pass.
- This may break invalid calls to `TransferState` methods.
This tightens parameter types of `TransferState` usage, and is a minor breaking change which may reveal existing problematic calls.
### router
- The type of `Route.pathMatch` is now stricter. Places that use
@@ -2904,7 +2839,7 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
actually gets set to something completely different. It's set to the
current internal `UrlTree` of the Router at the time navigation
occurs.
With this change, there is no exact replacement for the old value of
`initialUrl` because it was never intended to be exposed.
`Router.url` is likely the best replacement for this.
@@ -2928,7 +2863,7 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
returned by other guards: only the first value is used.
### zone.js
- in TaskTrackingZoneSpec track a periodic task until it is cancelled
The breaking change is scoped only to the plugin
`zone.js/plugins/task-tracking`. If you used `TaskTrackingZoneSpec` and
checked the pending macroTasks e.g. using `(this.ngZone as any)._inner
@@ -2937,7 +2872,7 @@ Adrien Crivelli, Alan Agius, Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Dyla
previously the `setInterval` macrotask was no longer tracked after its
callback was executed for the first time. Now it's tracked until
the task is explicitly cancelled, e.g with `clearInterval(id)`.
## Deprecations
### common
- The `ngModuleFactory` input of the `NgComponentOutlet` directive is deprecated in favor of a newly added `ngModule` input. The `ngModule` input accepts references to the NgModule class directly, without the need to resolve module factory first.
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.io/license
Copyright (c) 2010-2023 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
-9
View File
@@ -1,5 +1,4 @@
load("//:packages.bzl", "link_packages")
load("@aio_npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
@@ -132,12 +131,7 @@ architect(
data = APPLICATION_DEPS + [
":application_files_bin",
],
# Network is required to inline fonts.
exec_properties = ENABLE_NETWORK,
output_dir = True,
tags = [
"no-remote-exec",
],
)
architect(
@@ -152,9 +146,6 @@ architect(
data = APPLICATION_DEPS + [
":application_files_bin",
],
tags = [
"no-remote-exec",
],
)
architect_test(
+7 -11
View File
@@ -21,7 +21,7 @@ module.exports = function (config) {
'/dummy/image': 'src/assets/images/logos/angular/angular.png',
},
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
clearContext: false, // leave Jasmine Spec Runner output visible in browser
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
@@ -32,12 +32,15 @@ module.exports = function (config) {
},
},
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/site'),
subdir: '.',
reporters: [{type: 'html'}, {type: 'text-summary'}],
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
],
},
reporters: ['progress', 'kjhtml', 'jasmine-seed'],
port: 9876,
@@ -48,14 +51,7 @@ module.exports = function (config) {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
// See /integration/README.md#browser-tests for more info on these args
flags: [
'--no-sandbox',
'--headless',
'--disable-gpu',
'--disable-dev-shm-usage',
'--hide-scrollbars',
'--mute-audio',
],
flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage', '--hide-scrollbars', '--mute-audio'],
},
},
browsers: ['ChromeHeadlessNoSandbox'],
+135
View File
@@ -0,0 +1,135 @@
/*!
* @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.dev/license
*/
import {input, confirm} from '@inquirer/prompts';
import {existsSync} from 'fs';
import {copyFile, mkdir, writeFile} from 'fs/promises';
import {dirname, join} from 'path';
import {fileURLToPath} from 'url';
import {copyFolder} from './utils/fs.mjs';
const EXAMPLES_BASE = join(
dirname(fileURLToPath(import.meta.url)),
'../../src/content/examples',
);
const STACKBLITZ_TEMPLATE = join(
dirname(fileURLToPath(import.meta.url)),
'../../src/content/stackblitz-template',
);
const MAIN_TS_FILENAME = 'main.ts';
const STACKBLITZ_CONFIG_FILENAME = 'stackblitz.json';
createExample();
async function createExample(): Promise<void> {
console.info('Create example');
const name = await input({message: 'What is the example name?'});
const description = await input({message: 'Could you provide the description of the example?'});
const tags: string[] = [];
let addMoreTags = true;
while (addMoreTags) {
const tag = await input({message: 'Could you provide the tag of the example?'});
if (!!tag) {
tags.push(tag);
}
console.info();
addMoreTags = await confirm({message: 'Would you like to add another tag?', default: true});
}
const shouldCopySrcAppFolderFromTemplate = await confirm({
message: 'Would you like to copy /src/app/**.* from template?',
default: true,
});
const shouldCopyMainTsFromTemplate = await confirm({
message: 'Would you like to copy /src/main.ts from template?',
default: false,
});
const examplePath = join(EXAMPLES_BASE, name);
await createEmptyExample(examplePath, name, description, tags);
await copyFilesFromTemplate(
examplePath,
shouldCopySrcAppFolderFromTemplate,
shouldCopyMainTsFromTemplate,
);
}
/**
* Create the directory and marker files for the new example.
*/
async function createEmptyExample(
examplePath: string,
exampleName: string,
description: string,
tags: string[],
): Promise<void> {
validateExampleName(exampleName);
await ensureExamplePath(examplePath);
await writeStackBlitzFile(examplePath, description, tags);
}
function validateExampleName(exampleName: string): void {
if (/\s/.test(exampleName)) {
throw new Error(`Unable to create example. The example name contains spaces: '${exampleName}'`);
}
}
/**
* Ensure that the new example directory exists.
*/
async function ensureExamplePath(examplePath: string): Promise<void> {
if (existsSync(examplePath)) {
throw new Error(
`Unable to create example. The path to the new example already exists: ${examplePath}`,
);
}
await mkdir(examplePath);
}
/**
* Write the `stackblitz.json` file into the new example.
*/
async function writeStackBlitzFile(
examplePath: string,
description: string,
tags: string[],
): Promise<void> {
const config = {
description,
ignore: ['**/*.d.ts', '**/*.js', '**/*.[1,2].*'],
tags,
};
await writeFile(
join(examplePath, STACKBLITZ_CONFIG_FILENAME),
JSON.stringify(config, null, 2) + '\n',
);
}
async function copyFilesFromTemplate(
examplePath: string,
shouldCopySrcAppFolderFromTemplate: boolean,
shouldCopyMainTsFromTemplate: boolean,
): Promise<void> {
if (shouldCopySrcAppFolderFromTemplate) {
const appTemplateFolderPath = join(STACKBLITZ_TEMPLATE, 'src/app');
const exampleAppFolderPath = join(examplePath, 'src/app');
await copyFolder(appTemplateFolderPath, exampleAppFolderPath);
}
if (shouldCopyMainTsFromTemplate) {
const mainTsTemplatePath = join(STACKBLITZ_TEMPLATE, 'src', MAIN_TS_FILENAME);
const exampleMainTsPath = join(examplePath, 'src', MAIN_TS_FILENAME);
await copyFile(mainTsTemplatePath, exampleMainTsPath);
}
}
@@ -0,0 +1,158 @@
/*!
* @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.dev/license
*/
import ts from 'typescript';
import * as fs from 'fs';
import {dirname, join, relative} from 'path';
import {fileURLToPath} from 'url';
import {readFile, writeFile} from 'fs/promises';
const TYPESCRIPT_EXTENSION = '.ts';
const SKIP_FILES_WITH_EXTENSIONS = ['.e2e-spec.ts', '.spec.ts', '.po.ts'];
const EXAMPLES_PATH = `../../content/examples`;
interface File {
path: string;
content: string;
}
interface AnalyzedFiles {
path: string;
componentNames: string[];
}
/** Absolute disk path to the project directory. */
const examplesDir = join(
dirname(fileURLToPath(import.meta.url)),
'../../src/content/examples',
);
const projectDir = join(dirname(fileURLToPath(import.meta.url)), '../../src');
const previewsComponentMapFileName = 'previews.ts';
main();
async function main() {
const files = await retrieveAllTypescriptFiles(
examplesDir,
(path) => !SKIP_FILES_WITH_EXTENSIONS.some((extensionToSkip) => path.endsWith(extensionToSkip)),
);
const filesWithComponent = files
.map((file) => ({
componentNames: analyzeFile(file),
path: file.path,
}))
.filter((result) => result.componentNames.length > 0);
const previewsComponentMap = generatePreviewsComponentMap(filesWithComponent);
fs.mkdirSync(join(projectDir, 'assets/previews'), {recursive: true});
await writeFile(
join(projectDir, 'assets/previews', previewsComponentMapFileName),
previewsComponentMap,
);
}
/** Recursively search the provided directory for all typescript files and asynchronously load them. */
function retrieveAllTypescriptFiles(
baseDir: string,
predicateFn: (path: string) => boolean,
): Promise<File[]> {
const typescriptFiles: Promise<File>[] = [];
const checkFilesInDirectory = (dir: string) => {
const files = fs.readdirSync(dir, {withFileTypes: true});
for (const file of files) {
const fullPathToFile = join(dir, file.name);
const relativeFilePath = relative(baseDir, fullPathToFile);
if (
file.isFile() &&
file.name.endsWith(TYPESCRIPT_EXTENSION) &&
predicateFn(relativeFilePath)
) {
typescriptFiles.push(
readFile(fullPathToFile, {encoding: 'utf-8'}).then((fileContent) => {
return {
path: relativeFilePath,
content: fileContent,
};
}),
);
} else if (file.isDirectory()) {
checkFilesInDirectory(fullPathToFile);
}
}
};
checkFilesInDirectory(baseDir);
return Promise.all(typescriptFiles);
}
/** Returns list of the `Standalone` @Component class names for given file */
function analyzeFile(file: File): string[] {
const componentClassNames: string[] = [];
const sourceFile = ts.createSourceFile(file.path, file.content, ts.ScriptTarget.Latest, false);
const visitNode = (node: ts.Node): void => {
if (ts.isClassDeclaration(node)) {
const decorators = ts.getDecorators(node);
const componentName = node.name ? node.name.text : null;
if (decorators && decorators.length) {
for (const decorator of decorators) {
const call = decorator.expression;
if (
ts.isCallExpression(call) &&
ts.isIdentifier(call.expression) &&
call.expression.text === 'Component' &&
call.arguments.length > 0 &&
ts.isObjectLiteralExpression(call.arguments[0])
) {
const hasStandaloneProperty = call.arguments[0].properties.some(
(property) =>
property.name &&
ts.isIdentifier(property.name) &&
property.name.text === 'standalone',
);
if (hasStandaloneProperty && componentName) {
componentClassNames.push(componentName);
}
}
}
}
}
ts.forEachChild(node, visitNode);
};
visitNode(sourceFile);
return componentClassNames;
}
function generatePreviewsComponentMap(data: AnalyzedFiles[]): string {
let result = '';
for (const fileData of data) {
for (const componentName of fileData.componentNames) {
const key = `${fileData.path}${
fileData.componentNames.length > 1 ? '_' + componentName : ''
}`.replace(/\\/g, '/');
result += `['${key}']: () => import('${EXAMPLES_PATH}/${fileData.path
.replace(/\\/g, '/')
.replace('.ts', '')}').then(c => c.${componentName}),\n`;
}
}
return fs
.readFileSync('scripts/examples/previews-components.template', 'utf8')
.replace(/\${previewsComponents}/g, result);
}
@@ -0,0 +1,23 @@
/*!
* @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.dev/license
*/
/**
******************************************************************************
* DO NOT MANUALLY EDIT THIS FILE. THIS FILE IS AUTOMATICALLY GENERATED.
******************************************************************************
*/
import {Type} from '@angular/core';
/**
* Map of the previews components, values are functions which returns the promise of the component type, which will be displayed as preview in the ExampleViewer component.
* Keys has to be equal to paths written down in the docs markdown files.
*/
export const PREVIEWS_COMPONENTS_MAP: Record<string, () => Promise<Type<unknown>>> = {
${previewsComponents}
};
@@ -0,0 +1,221 @@
/*!
* @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.dev/license
*/
import {dirname, join} from 'path';
import {readFile, writeFile} from 'fs/promises';
import {
ASSETS_EXAMPLE_PATH,
CSS_TS_COPYRIGHT,
EXAMPLES_PATH,
HTML_COPYRIGHT,
STACKBLITZ_TEMPLATE_PATH,
STACKBLITZ_CONFIG_FILENAME,
TEMPORARY_EXAMPLES_PATH,
EXCLUDE_FILES_FOR_STACKBLITZ,
} from './utils/examples-constants.mjs';
import {copyFolder, createFolder, removeFolder} from './utils/fs.mjs';
import jsdom from 'jsdom';
import {glob} from 'glob';
import {regionParser} from './../../prerender/markdown-pipeline/regions/region-parser.mjs';
interface StackblitzConfig {
ignore: string[];
file: string;
tags: string[];
description: string;
}
export async function generateStackblitzExample(
exampleFolderName: string,
primaryFilePath: string,
title: string,
) {
const exampleDir = join(EXAMPLES_PATH, exampleFolderName);
const temporaryExampleDir = join(TEMPORARY_EXAMPLES_PATH, exampleFolderName);
const config = await readFile(join(exampleDir, STACKBLITZ_CONFIG_FILENAME), 'utf-8');
const stackblitzConfig: StackblitzConfig = JSON.parse(config);
primaryFilePath = join(...primaryFilePath.split('/'));
await createFolder(temporaryExampleDir);
await combineTemplateWithExample(exampleDir, temporaryExampleDir);
await generateStackblitzHtml(
temporaryExampleDir,
stackblitzConfig,
exampleFolderName,
primaryFilePath,
title,
);
await removeFolder(temporaryExampleDir);
}
async function combineTemplateWithExample(
exampleDir: string,
temporaryExampleDir: string,
): Promise<void> {
// Copy template files to TEMP folder
await copyFolder(STACKBLITZ_TEMPLATE_PATH, temporaryExampleDir);
// Copy example files to TEMP folder
await copyFolder(exampleDir, temporaryExampleDir);
}
async function generateStackblitzHtml(
temporaryExampleDir: string,
stackBlitzConfig: StackblitzConfig,
exampleFolderName: string,
primaryFilePath: string,
title: string,
): Promise<void> {
const defaultIncludes = [
'**/*.ts',
'**/*.js',
'**/*.css',
'**/*.html',
'**/*.md',
'**/*.json',
'**/*.svg',
];
const exampleFilePaths = await glob(defaultIncludes, {
cwd: temporaryExampleDir,
nodir: true,
dot: true,
ignore: stackBlitzConfig.ignore,
});
const postData = await createPostData(
temporaryExampleDir,
stackBlitzConfig,
exampleFilePaths,
title,
);
const primaryFile = getPrimaryFile(primaryFilePath ?? stackBlitzConfig.file, exampleFilePaths);
const html = createStackblitzHtml(postData, primaryFile);
const stackblitzHtmlPath = join(
join(ASSETS_EXAMPLE_PATH, exampleFolderName),
`${primaryFile}.html`,
);
await createFolder(dirname(stackblitzHtmlPath));
await writeFile(stackblitzHtmlPath, html, 'utf-8');
}
function getPrimaryFile(primaryFilePath: string, exampleFilePaths: string[]): string {
if (primaryFilePath) {
if (!exampleFilePaths.some((filePath) => filePath === primaryFilePath)) {
throw new Error(`The specified primary file (${primaryFilePath}) does not exist!`);
}
return primaryFilePath;
} else {
const defaultPrimaryFilePaths = [
'src/app/app.component.html',
'src/app/app.component.ts',
'src/app/main.ts',
];
const primaryFile = defaultPrimaryFilePaths.find((path) =>
exampleFilePaths.some((filePath) => filePath === path),
);
if (!primaryFile) {
throw new Error(
`None of the default primary files (${defaultPrimaryFilePaths.join(', ')}) exists.`,
);
}
return primaryFile;
}
}
async function createPostData(
exampleDir: string,
config: StackblitzConfig,
exampleFilePaths: string[],
title: string,
): Promise<Record<string, string>> {
const postData: Record<string, string> = {};
for (const filePath of exampleFilePaths) {
if (EXCLUDE_FILES_FOR_STACKBLITZ.some((excludedFile) => filePath.endsWith(excludedFile))) {
continue;
}
let content = await readFile(join(exampleDir, filePath), 'utf-8');
content = appendCopyright(filePath, content);
content = extractRegions(filePath, content);
postData[`project[files][${filePath}]`] = content;
}
const tags = ['angular', 'example', ...(config.tags || [])];
tags.forEach((tag, index) => (postData[`project[tags][${index}]`] = tag));
postData['project[description]'] = `Angular Example - ${config.description}`;
postData['project[template]'] = 'node';
postData['project[title]'] = title ?? 'Angular Example';
return postData;
}
function createStackblitzHtml(postData: Record<string, string>, primaryFile: string): string {
const baseHtml = createBaseStackblitzHtml(primaryFile);
const doc = new jsdom.JSDOM(baseHtml).window.document;
const form = doc.querySelector('form');
for (const [key, value] of Object.entries(postData)) {
const element = htmlToElement(doc, `<input type="hidden" name="${key}">`);
if (element && form) {
element.setAttribute('value', value as string);
form.appendChild(element);
}
}
return doc.documentElement.outerHTML;
}
function createBaseStackblitzHtml(primaryFile: string) {
const file = `?file=${primaryFile}`;
const action = `https://stackblitz.com/run${file}`;
return `
<!DOCTYPE html><html lang="en"><body>
<form id="mainForm" method="post" action="${action}" target="_self"></form>
<script>
var embedded = 'ctl=1';
var isEmbedded = window.location.search.indexOf(embedded) > -1;
if (isEmbedded) {
var form = document.getElementById('mainForm');
var action = form.action;
var actionHasParams = action.indexOf('?') > -1;
var symbol = actionHasParams ? '&' : '?'
form.action = form.action + symbol + embedded;
}
document.getElementById("mainForm").submit();
</script>
</body></html>
`.trim();
}
function appendCopyright(filename: string, content: string): string {
if (filename.endsWith('.html')) {
return `${HTML_COPYRIGHT}${content}`;
} else if (filename.endsWith('.ts') || filename.endsWith('.css')) {
return `${CSS_TS_COPYRIGHT}${content}`;
}
return content;
}
function htmlToElement(document: Document, html: string) {
const div = document.createElement('div');
div.innerHTML = html;
return div.firstElementChild;
}
function extractRegions(path: string, contents: string): string {
const regionParserResult = regionParser(contents, path);
return regionParserResult.contents;
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"ts-node": {
"compilerOptions": {
"module": "es2020"
}
}
}
@@ -0,0 +1,55 @@
/*!
* @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.dev/license
*/
import {dirname, join} from 'path';
import {fileURLToPath} from 'url';
const BASE = dirname(fileURLToPath(import.meta.url));
// Directory paths
export const ASSETS_EXAMPLE_PATH = join(
BASE,
'../../../src/assets/content/examples',
);
export const EXAMPLES_PATH = join(BASE, '../../../src/content/examples');
export const STACKBLITZ_TEMPLATE_PATH = join(
BASE,
'../../../src/content/stackblitz-template',
);
export const TEMPORARY_EXAMPLES_PATH = join(fileURLToPath(import.meta.url), '../../../../TEMP');
// Extensions
export const TEST_FILES_EXTENSION_SUFFIX = '.spec.ts';
export const TEST_FILES_E2E_EXTENSION_SUFFIX = '.e2e-spec.ts';
// Filename
// TODO: BUILD.bazel and example-config will be deprecated when migration to standalone examples will be done.
export const BUILD_BAZEL_FILENAME = 'BUILD.bazel';
export const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
export const STACKBLITZ_CONFIG_FILENAME = 'stackblitz.json';
// Copyright
const PAD = '\n\n';
const COPYRIGHT =
'@license\n' +
'Copyright Google LLC All Rights Reserved.\n' +
'\n' +
'Use of this source code is governed by an MIT-style license that can be\n' +
'found in the LICENSE file at https://angular.dev/license\n';
export const CSS_TS_COPYRIGHT = `/*\n${COPYRIGHT}\n*/${PAD}`;
export const HTML_COPYRIGHT = `<!-- \n${COPYRIGHT}\n-->${PAD}`;
// Exclude
export const EXCLUDE_FILES_FOR_STACKBLITZ = [
STACKBLITZ_CONFIG_FILENAME,
BUILD_BAZEL_FILENAME,
EXAMPLE_CONFIG_FILENAME,
TEST_FILES_EXTENSION_SUFFIX,
TEST_FILES_E2E_EXTENSION_SUFFIX,
];
+77
View File
@@ -0,0 +1,77 @@
/*!
* @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.dev/license
*/
import {Dirent, existsSync, readdirSync} from 'fs';
import {copyFile, mkdir, readFile, readdir, rm, stat} from 'fs/promises';
import {join, relative} from 'path';
export async function copyFolder(source: string, destination: string) {
if (!existsSync(destination)) {
await mkdir(destination, {recursive: true});
}
const files = await readdir(source);
for (const file of files) {
const sourcePath = join(source, file);
const destPath = join(destination, file);
const stats = await stat(sourcePath);
const isDirectory = await stats.isDirectory();
if (isDirectory) {
await copyFolder(sourcePath, destPath);
} else {
await copyFile(sourcePath, destPath);
}
}
}
/** Recursively search the provided directory for all files which satisfy predicate and asynchronously load them. */
export function retrieveFiles(
baseDir: string,
predicate?: (file: Dirent) => boolean,
): Promise<{path: string; content: string}[]> {
const filesWithContent: Promise<{path: string; content: string}>[] = [];
const checkFilesInDirectory = (dir: string) => {
const files = readdirSync(dir, {withFileTypes: true});
for (const file of files) {
const fullPathToFile = join(dir, file.name);
if (file.isFile() && (predicate ? predicate(file) : true)) {
filesWithContent.push(
readFile(fullPathToFile, {encoding: 'utf-8'}).then((fileContent) => {
return {
path: relative(baseDir, fullPathToFile),
content: fileContent,
};
}),
);
} else if (file.isDirectory()) {
checkFilesInDirectory(fullPathToFile);
}
}
};
checkFilesInDirectory(baseDir);
return Promise.all(filesWithContent);
}
export async function createFolder(path: string): Promise<void> {
if (!existsSync(path)) {
await mkdir(path, {recursive: true});
}
}
export async function removeFolder(path: string): Promise<void> {
if (existsSync(path)) {
await rm(path, {recursive: true});
}
}
+1 -9
View File
@@ -7,15 +7,7 @@
*/
import {DOCUMENT, isPlatformBrowser} from '@angular/common';
import {
Component,
inject,
NgZone,
OnInit,
PLATFORM_ID,
signal,
WritableSignal,
} from '@angular/core';
import {Component, inject, NgZone, OnInit, PLATFORM_ID, signal, WritableSignal} from '@angular/core';
import {NavigationEnd, NavigationSkipped, Router, RouterLink, RouterOutlet} from '@angular/router';
import {filter, map, skip} from 'rxjs/operators';
import {
@@ -116,7 +116,7 @@
</div>
</div>
<p class="docs-license">
Super-powered by Google ©2010-2024. Code licensed under an
Super-powered by Google ©2010-2023. Code licensed under an
<a routerLink="/license" title="License text">MIT-style License</a>
. Documentation licensed under
<a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>
@@ -28,12 +28,12 @@ export class ContentLoader implements DocsContentLoader {
this.cache.set(
path,
this.httpClient
.get(`assets/content/${path}`, {
responseType: 'text',
})
.pipe(map((contents) => ({contents, id: path})))
.toPromise(),
);
.get(`assets/content/${path}`, {
responseType: 'text',
})
.pipe(map((contents) => ({contents, id: path})))
.toPromise()
);
} catch {
this.router.navigateByUrl('/404');
}
-207
View File
@@ -1,207 +0,0 @@
# EmbeddedEditor components, services and functionality
- [Scenarios](#scenarios)
- [Loading a project](#loading-a-project)
- [Updating the code](#updating-the-code)
- [Creating a new file](#creating-a-new-file)
- [Deleting a file](#deleting-a-file)
- [Switching a project](#switching-a-project)
- [Components and services](#components-and-services)
- [EmbeddedEditor](#EmbeddedEditor)
- [CodeEditor](#CodeEditor)
- [CodeMirrorEditor](#CodeMirrorEditor)
- [TypeScript Web Worker](#typescript-web-worker)
- [Preview](#Preview)
- [Terminal](#Terminal)
- [InteractiveTerminal](#InteractiveTerminal)
- [Console](#Console)
- [NodeRuntimeSandbox](#NodeRuntimeSandbox)
- [NodeRuntimeState](#NodeRuntimeState)
- [EmbeddedTutorialManager](#EmbeddedTutorialManager)
- [EditorUiState](#EditorUiState)
- [DownloadManager](#DownloadManager)
- [AlertManager](#AlertManager)
- [TypingsLoader](#TypingsLoader)
## External libraries
- [WebContainers API](https://webcontainers.io/)
- [CodeMirror](https://codemirror.net/)
- [@typescript/vfs](https://www.npmjs.com/package/@typescript/vfs)
- [Xterm.js](https://xtermjs.org/)
## Notes
- See [scripts/tutorials/README.md](/scripts/tutorials/README.md) for more information about the tutorials script.
- See [adev/src/content/tutorials/README.md](/adev/src/content/tutorials/README.md) for more information about the tutorials content.
---
## Scenarios
### Loading a project
1. The page responsible for the embedded editor lazy loads the [`EmbeddedEditor`](./embedded-editor.component.ts) component and the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts), then triggers the initialization of all components and services. The embedded editor is available in the following pages:
- homepage: https://angular.dev
- playground: https://angular.dev/playground
- tutorial pages: https://angular.dev/tutorials
2. The project assets are fetched by the [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts). Meanwhile:
- The code editor is initialized
- The code editor initializes the TypeScript Web Worker, which initializes the "default file system map" using TypeScript's CDN.
- The WebContainer is initialized
- The terminal is initialized
3. The tutorial source code is mounted in the `WebContainer`'s filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts)
4. The tutorial project dependencies are installed by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
5. The development server is started by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts) and the types are loaded by the [`TypingsLoader`](./typings-loader.service.ts) service.
6. The preview is loaded with the URL provided by the WebContainer API after the development server is started.
7. The project is ready.
### Updating the code
1. The user update the code in the code editor.
2. The code editor state is updated on real time, without debouncing so that the user can see the changes in the code editor and CodeMirror can handle the changes accordingly.
3. At the same time, the changes are sent to the TypeScript web worker to provide diagnostics, autocomplete and type features as soon as possible.
4. The code changes are debounced to be written in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
5. After the debounce time is reached, the code changes are written in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts), then the user can see the changes in the preview.
### Creating a new file
1. The user clicks on the new file button.
2. The new file tab is opened.
3. The user types the new file name.
4. If the file name is valid, the file is created in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
- `..` is disallowed in the file name to prevent users to create files outside the `src` directory.
5. The file is added to the TypeScript virtual file system, allowing the TypeScript web worker to provide diagnostics, autocomplete and type features for the new file. Also, exports from the new file are available in other files.
6. The new file is added as the last tab in the code editor and the new file can be edited.
Note: If the new file name matches a file that already exists but is hidden in the code editor, the content for that file will show up in the created file. An example for a file that always exists is `index.html`.
### Deleting a file
1. The user clicks on the delete file button.
2. The file is deleted from the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
3. The file is removed from the TypeScript virtual file system.
4. The file is removed from the code editor tabs.
Note: Some files can't be deleted to prevent users to break the app, being `src/main.ts`and `src/index.html`
### Switching a project
The embedded editor considers a project change when the embedded editor was already initialized and the user changes the page in the following scenarios:
- Navigating through tutorial steps
- Going from the homepage after the embedded editor is initialized to the playground
- Going from a tutorial page to the playground
- Going from a tutorial page to the homepage
- Going from the playground to the homepage
When a project change is detected, the [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts) emits the `tutorialChanged` observable, which is listened in multiple sub-components and services, then each component/service performs the necessary operations to switch the project.
The following steps are executed on project change:
1. The new project files are fetched by the [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts).
2. The new project files are mounted in the WebContainer filesystem.
3. The TypeScript virtual filesystem is updated with the new files and contents.
4. The previous project and new project files are compared.
1. Files that are not available in the new project are deleted from the WebContainer filesystem.
2. Files that have the same path and name have their content replaced on the previous step when the files are mounted.
5. The previous project dependencies are compared with the new project dependencies.
1. If there are differences, a `npm install` is triggered, hiding the preview and going to the install loading step.
2. If there are no differences, the project is ready.
6. Some states are resetted, for example the "reveal answer" state if the previous project was in the "reveal answer" state.
## Components and services
### [`EmbeddedEditor`](./embedded-editor.component.ts)
The embedded editor is the parent component that holds all the components and services that compose the embedded editor.
#### [`CodeEditor`](./code-editor/code-editor.component.ts)
The component that holds the code editor view and the code editor state.
##### [`CodeMirrorEditor`](./code-editor/code-mirror-editor.service.ts)
[CodeMirror](https://codemirror.net/) is the library used to handle the code editor.
The `CodeMirrorEditor` service manages the CodeMirror instance and all the interactions with the library used to handle the code editor.
- handle the file edits and the CodeMirror view and state
- handle the current project files in the code editor
- handle the file creations and deletions
- handle the file changes
- handle all the CodeMirror specific events and extensions
###### [TypeScript Web Worker](./code-editor/workers/typescript-vfs.worker.ts)
The TypeScript features are provided by the TypeScript web worker, that is initialized by the `CodeMirrorEditor` service.
The TypeScript web worker uses `@typescript/vfs` and the TypeScript language service to provide diagnostics, autocomplete and type features.
#### [`Preview`](./preview/preview.component.ts)
The preview component manages the `iframe` responsible for displaying the tutorial project preview, with the URL provided by the WebContainer API after the development server is started.
While the project is being initialized, the preview displays the loading state.
#### [`Terminal`](./terminal/terminal.component.ts)
[Xterm.js](https://xtermjs.org/) is the library used to handle the terminals.
The terminal component handles the Xterm.js instance for the console and for the interactive terminal.
##### [`InteractiveTerminal`](./terminal/interactive-terminal.ts)
The interactive terminal is the terminal where the user can interact with the terminal and run commands, supporting only commands for the Angular CLI.
##### Console
The console displays the output for `npm install` and `ng serve`.
#### [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts)
Responsible for managing the WebContainer instance and all communication with its API. This service handles:
- the WebContainer instance
- all Node.js scripts
- the WebContainer filesystem, mounting the tutorial project files, writing new content, deleting and creating files.
- the terminal session, reading and processing user inputs.
- the tutorial project dependencies, installing the dependencies.
- the processes running inside the WebContainer, being the npm scripts to install the dependencies, run the development server and the user inputs for the `ng` CLI.
##### [`NodeRuntimeState`](./node-runtime-state.service.ts)
Manages the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts) loading and error state.
#### [`EmbeddedTutorialManager`](./embedded-tutorial-manager.service.ts)
Manages the tutorial assets, being responsible for fetching the tutorial source code and metadata.
The source code is mounted in the WebContainer filesystem by the [`NodeRuntimeSandbox`](./node-runtime-sandbox.service.ts).
The metadata is used to manage the project, handle the project changes and the user interactivity with the app.
This service also handles the reveal answer and reset reveal answer feature.
#### [`EditorUiState`](./editor-ui-state.service.ts)
Manages the editor UI state, being responsible for handling the user interactions with the editor tabs, switching between the preview, the terminal and the console.
#### [`DownloadManager`](./download-manager.service.ts)
Responsible for handling the download button in the embedded editor, fetching the tutorial project files and generating a zip file with the project content.
#### [`AlertManager`](./alert-manager.service.ts)
Manage the alerts displayed in the embedded editor, being the out of memory alert when multiple tabs are opened, and unsupported enviroments alerts.
#### [`TypingsLoader`](./typings-loader.service.ts)
Manages the types definitions for the code editor.
@@ -16,7 +16,10 @@ import {NodeRuntimeSandbox} from '../node-runtime-sandbox.service';
import {EmbeddedTutorialManager} from '../embedded-tutorial-manager.service';
import {CodeMirrorEditor, EDITOR_CONTENT_CHANGE_DELAY_MILLIES} from './code-mirror-editor.service';
import {TutorialConfig, TutorialMetadata} from '@angular/docs';
import {
TutorialConfig,
TutorialMetadata,
} from '@angular/docs';
class FakeNodeRuntimeSandbox {
async writeFile(path: string, content: string) {}
+2 -1
View File
@@ -13,4 +13,5 @@ export const TUTORIALS_ASSETS_METADATA_DIRECTORY = 'metadata';
export const TUTORIALS_SOURCE_CODE_WEB_PATH = `${TUTORIALS_ASSETS_WEB_PATH}/${TUTORIALS_ASSETS_SOURCE_CODE_DIRECTORY}`;
export const TUTORIALS_METADATA_WEB_PATH = `${TUTORIALS_ASSETS_WEB_PATH}/${TUTORIALS_ASSETS_METADATA_DIRECTORY}`;
export const TUTORIALS_COMMON_DIRECTORY = 'common';
export const TUTORIALS_COMMON_DIRECTORY = 'common';
@@ -245,7 +245,7 @@ describe('EmbeddedTutorialManager', () => {
},
},
};
const metadata: TutorialMetadata = {
tutorialFiles: {'app.js': ''},
openFiles: ['app.js'],
@@ -295,6 +295,7 @@ describe('EmbeddedTutorialManager', () => {
);
});
it('should not set shouldReinstallDependencies if project did not change', async () => {
const fetchMock = spyOn(window, 'fetch');
@@ -324,6 +325,7 @@ describe('EmbeddedTutorialManager', () => {
it('should trigger shouldReInstallDependencies if new metadata has different dependencies', async () => {
const fetchMock = spyOn(window, 'fetch');
fetchMock
.withArgs(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/source-code.json`)
.and.returnValues(Promise.resolve(new Response('{}', {status: 200})));
@@ -11,7 +11,10 @@ import {FileSystemTree} from '@webcontainer/api';
import {BehaviorSubject} from 'rxjs';
import {TutorialMetadata} from '@angular/docs';
import {TUTORIALS_COMMON_DIRECTORY, TUTORIALS_ASSETS_WEB_PATH} from './constants';
import {
TUTORIALS_COMMON_DIRECTORY,
TUTORIALS_ASSETS_WEB_PATH
} from './constants';
/**
* A service responsible for the current tutorial, retrieving and providing
@@ -137,9 +140,7 @@ export class EmbeddedTutorialManager {
}
private async fetchTutorialSourceCode(tutorial: string): Promise<FileSystemTree> {
const tutorialSourceCode = await fetch(
`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/source-code.json`,
);
const tutorialSourceCode = await fetch(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/source-code.json`);
if (!tutorialSourceCode.ok) throw new Error(`Missing source code for tutorial ${tutorial}`);
@@ -147,9 +148,7 @@ export class EmbeddedTutorialManager {
}
private async fetchTutorialMetadata(tutorial: string): Promise<TutorialMetadata> {
const tutorialSourceCode = await fetch(
`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/metadata.json`,
);
const tutorialSourceCode = await fetch(`${TUTORIALS_ASSETS_WEB_PATH}/${tutorial}/metadata.json`);
if (!tutorialSourceCode.ok) throw new Error(`Missing metadata for ${tutorial}`);
+6 -5
View File
@@ -1,7 +1,8 @@
export {EmbeddedTutorialManager} from './embedded-tutorial-manager.service';
export {LoadingStep} from './enums/loading-steps';
export {NodeRuntimeState} from './node-runtime-state.service';
export { EmbeddedTutorialManager } from './embedded-tutorial-manager.service'
export { LoadingStep } from './enums/loading-steps'
export { NodeRuntimeState } from './node-runtime-state.service'
export {NodeRuntimeSandbox} from './node-runtime-sandbox.service';
export { NodeRuntimeSandbox } from './node-runtime-sandbox.service'
export {EmbeddedEditor, EMBEDDED_EDITOR_SELECTOR} from './embedded-editor.component'
export {EmbeddedEditor, EMBEDDED_EDITOR_SELECTOR} from './embedded-editor.component';
@@ -12,7 +12,10 @@ import {BehaviorSubject, of as observableOf} from 'rxjs';
import {signal} from '@angular/core';
import {WebContainer} from '@webcontainer/api';
import {TutorialType} from '@angular/docs';
import {FakeWebContainer, FakeWebContainerProcess} from '@angular/docs/testing';
import {
FakeWebContainer,
FakeWebContainerProcess,
} from '@angular/docs/testing';
import {AlertManager} from './alert-manager.service';
import {EmbeddedTutorialManager} from './embedded-tutorial-manager.service';
import {LoadingStep} from './enums/loading-steps';
@@ -77,7 +77,7 @@ export class NodeRuntimeState {
* recommend using desktop.
*/
private checkUnsupportedEnvironment(): void {
if (isIos) {
if (isIos || isFirefox) {
this.setError({
message: 'Unsupported environment',
type: ErrorType.UNSUPPORTED_BROWSER_ENVIRONMENT,
@@ -16,7 +16,7 @@ import {
} from '@angular/core';
import {injectAsync} from '../../../core/services/inject-async';
import {EmbeddedEditor, EmbeddedTutorialManager} from '../../../editor';
import { EmbeddedEditor, EmbeddedTutorialManager } from '../../../editor';
@Component({
selector: 'adev-code-editor',
@@ -39,7 +39,9 @@ export class CodeEditorComponent implements OnInit {
private async loadEmbeddedEditor() {
const nodeRuntimeSandbox = await injectAsync(this.environmentInjector, () =>
import('../../../editor/index').then((c) => c.NodeRuntimeSandbox),
import('../../../editor/index').then(
(c) => c.NodeRuntimeSandbox,
),
);
await this.embeddedTutorialManager.fetchAndSetTutorialFiles(this.tutorialFiles);
@@ -31,10 +31,7 @@ export class BuildText extends View {
/**
* Create the build text.
*/
constructor(
private readonly gl: OGLRenderingContext,
private readonly document: Document,
) {
constructor(private readonly gl: OGLRenderingContext, private readonly document: Document) {
super();
this.userData['x'] = 0;
@@ -27,10 +27,7 @@ export class BuildView extends View {
/**
* Create the build view.
*/
constructor(
private readonly gl: OGLRenderingContext,
private readonly document: Document,
) {
constructor(private readonly gl: OGLRenderingContext, private readonly document: Document) {
super();
this.visible = false;
@@ -34,10 +34,7 @@ export class Lines extends View {
/**
* Create the container view.
*/
constructor(
private readonly gl: OGLRenderingContext,
private readonly divisions: number,
) {
constructor(private readonly gl: OGLRenderingContext, private readonly divisions: number) {
super();
this.userData['x'] = 0;
@@ -23,7 +23,7 @@ import {RouterLink} from '@angular/router';
import {PlaygroundTemplate} from '@angular/docs';
import {ClickOutside, NavigationList} from '@angular/docs';
import {injectAsync} from '../../core/services/inject-async';
import {EmbeddedTutorialManager} from '../../editor/index';
import {EmbeddedTutorialManager} from '../../editor/index';
import PLAYGROUND_ROUTE_DATA_JSON from '../../../../src/assets/tutorials/playground/routes.json';
import {CdkMenu, CdkMenuItem, CdkMenuTrigger} from '@angular/cdk/menu';
@@ -69,7 +69,9 @@ export default class PlaygroundComponent implements AfterViewInit {
const [embeddedEditorComponent, nodeRuntimeSandbox] = await Promise.all([
import('../../editor/index').then((c) => c.EmbeddedEditor),
injectAsync(this.environmentInjector, () =>
import('../../editor/index').then((c) => c.NodeRuntimeSandbox),
import('../../editor/index').then(
(c) => c.NodeRuntimeSandbox,
),
),
]);
@@ -59,7 +59,7 @@ export default class CliReferenceDetailsPage implements OnInit {
private setPageContent(): void {
this.activatedRoute.data
.pipe(
map((data) => data['docContent']),
map(data => data['docContent']),
takeUntilDestroyed(this.destroyRef),
)
.subscribe((doc: DocContent | undefined) => {
@@ -18,12 +18,12 @@ import {
EMBEDDED_EDITOR_SELECTOR,
EmbeddedEditor,
EmbeddedTutorialManager,
NodeRuntimeSandbox,
NodeRuntimeSandbox
} from '../../editor';
import {mockAsyncProvider} from '../../core/services/inject-async';
import Tutorial from './tutorial.component';
import {TutorialConfig, TutorialType} from '@angular/docs';
import {TutorialConfig, TutorialType} from '@angular/docs'
@Component({
selector: EMBEDDED_EDITOR_SELECTOR,
@@ -38,11 +38,11 @@ import {
EmbeddedTutorialManager,
LoadingStep,
NodeRuntimeState,
EmbeddedEditor,
EmbeddedEditor
} from '../../editor/index';
import {SplitResizerHandler} from './split-resizer-handler.service';
import {TutorialType} from '@angular/docs';
import {TutorialNavigationData, TutorialNavigationItem} from '@angular/docs';
import { TutorialType } from '@angular/docs';
import { TutorialNavigationData, TutorialNavigationItem, } from '@angular/docs'
const INTRODUCTION_LABEL = 'Introduction';
@@ -138,7 +138,9 @@ export default class Tutorial implements AfterViewInit {
this.embeddedTutorialManager.revealAnswer();
const nodeRuntimeSandbox = await injectAsync(this.environmentInjector, () =>
import('../../editor/index').then((s) => s.NodeRuntimeSandbox),
import('../../editor/index').then(
(s) => s.NodeRuntimeSandbox,
),
);
await Promise.all(
@@ -211,10 +213,16 @@ export default class Tutorial implements AfterViewInit {
this.shouldRenderContent.set(routeData.type !== TutorialType.EDITOR_ONLY);
this.nextStepPath = routeData.nextStep ? `/${routeData.nextStep}` : undefined;
this.previousStepPath = routeData.previousStep ? `/${routeData.previousStep}` : undefined;
this.nextStepPath = routeData.nextStep
? `/${routeData.nextStep}`
: undefined;
this.previousStepPath = routeData.previousStep
? `/${routeData.previousStep}`
: undefined;
this.nextTutorialPath.set(routeData.nextTutorial ? `/${routeData.nextTutorial}` : null);
this.nextTutorialPath.set(
routeData.nextTutorial ? `/${routeData.nextTutorial}` : null,
);
}
/**
+6 -2
View File
@@ -6,7 +6,11 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {contentResolver, flatNavigationData, mapNavigationItemsToRoutes} from '@angular/docs';
import {
contentResolver,
flatNavigationData,
mapNavigationItemsToRoutes,
} from '@angular/docs';
import {Route} from '@angular/router';
import {DefaultPage, PagePrefix} from './core/enums/pages';
@@ -121,7 +125,7 @@ export const routes: Route[] = [
{
path: '',
loadComponent: () => import('./features/home/home.component'),
data: {label: 'Home'},
data: {label: 'Home'}
},
{
path: PagePrefix.DOCS,
+173 -188
View File
@@ -157,11 +157,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'guide/components/advanced-configuration',
contentPath: 'guide/components/advanced-configuration',
},
{
label: 'Custom Elements',
path: 'guide/elements',
contentPath: 'guide/elements',
},
],
},
{
@@ -223,39 +218,9 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
contentPath: 'guide/templates/control-flow',
},
{
label: 'Pipes',
children: [
{
label: 'Overview',
path: 'guide/pipes',
contentPath: 'guide/pipes/overview',
},
{
label: 'Using a pipe in a template',
path: 'guide/pipes/template',
contentPath: 'guide/pipes/template',
},
{
label: 'Custom pipes',
path: 'guide/pipes/transform-data',
contentPath: 'guide/pipes/transform-data',
},
{
label: 'Pipe precedence in expressions',
path: 'guide/pipes/precedence',
contentPath: 'guide/pipes/precedence',
},
{
label: 'Change detection with pipes',
path: 'guide/pipes/change-detection',
contentPath: 'guide/pipes/change-detection',
},
{
label: 'Unwrapping data from an observable',
path: 'guide/pipes/unwrapping-data-observables',
contentPath: 'guide/pipes/unwrapping-data-observables',
},
],
label: 'Deferrable views',
path: 'guide/defer',
contentPath: 'guide/defer',
},
{
label: 'Template reference variables',
@@ -352,16 +317,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'guide/signals/rxjs-interop',
contentPath: 'guide/signals/rxjs-interop',
},
{
label: 'Inputs as signals',
path: 'guide/signals/inputs',
contentPath: 'guide/signals/inputs',
},
{
label: 'Model inputs',
path: 'guide/signals/model',
contentPath: 'guide/signals/model',
},
{
label: 'Queries as signals',
path: 'guide/signals/queries',
@@ -369,6 +324,71 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
],
},
{
label: 'NgModule',
children: [
{
label: 'Overview',
path: 'guide/ngmodules',
contentPath: 'guide/ngmodules/overview',
},
{
label: 'JS Modules vs NgModules',
path: 'guide/ngmodules/vs-jsmodule',
contentPath: 'guide/ngmodules/vs-jsmodule',
},
{
label: 'Launching your app with a root module',
path: 'guide/ngmodules/bootstrapping',
contentPath: 'guide/ngmodules/bootstrapping',
},
{
label: 'Sharing NgModules',
path: 'guide/ngmodules/sharing',
contentPath: 'guide/ngmodules/sharing',
},
{
label: 'Frequently used NgModules',
path: 'guide/ngmodules/frequent',
contentPath: 'guide/ngmodules/frequent',
},
{
label: 'Feature modules',
path: 'guide/ngmodules/feature-modules',
contentPath: 'guide/ngmodules/feature-modules',
},
{
label: 'Types of feature modules',
path: 'guide/ngmodules/module-types',
contentPath: 'guide/ngmodules/module-types',
},
{
label: 'Providing dependencies',
path: 'guide/ngmodules/providers',
contentPath: 'guide/ngmodules/providers',
},
{
label: 'Singleton services',
path: 'guide/ngmodules/singleton-services',
contentPath: 'guide/ngmodules/singleton-services',
},
{
label: 'Lazy-loading feature modules',
path: 'guide/ngmodules/lazy-loading',
contentPath: 'guide/ngmodules/lazy-loading',
},
{
label: 'NgModule API',
path: 'guide/ngmodules/api',
contentPath: 'guide/ngmodules/api',
},
{
label: 'NgModule FAQs',
path: 'guide/ngmodules/faq',
contentPath: 'guide/ngmodules/faq',
},
],
},
{
label: 'Routing',
children: [
@@ -435,64 +455,29 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
],
},
{
label: 'HTTP Client',
children: [
{
label: 'Overview',
path: 'guide/http',
contentPath: 'guide/http/overview',
},
{
label: 'Setting up HttpClient',
path: 'guide/http/setup',
contentPath: 'guide/http/setup',
},
{
label: 'Making requests',
path: 'guide/http/making-requests',
contentPath: 'guide/http/making-requests',
},
{
label: 'Intercepting requests and responses',
path: 'guide/http/interceptors',
contentPath: 'guide/http/interceptors',
},
{
label: 'Testing',
path: 'guide/http/testing',
contentPath: 'guide/http/testing',
},
],
label: 'Server-side Rendering',
path: 'guide/ssr',
contentPath: 'guide/ssr',
},
{
label: 'Performance',
children: [
{
label: 'Deferrable views',
path: 'guide/defer',
contentPath: 'guide/defer',
},
{
label: 'Image Optimization',
path: 'guide/image-optimization',
contentPath: 'guide/image-optimization',
},
{
label: 'Server-side Rendering',
path: 'guide/ssr',
contentPath: 'guide/ssr',
},
{
label: 'Build-time prerendering',
path: 'guide/prerendering',
contentPath: 'guide/prerendering',
},
{
label: 'Hydration',
path: 'guide/hydration',
contentPath: 'guide/hydration',
},
],
label: 'Build-time prerendering',
path: 'guide/prerendering',
contentPath: 'guide/prerendering',
},
{
label: 'Hydration',
path: 'guide/hydration',
contentPath: 'guide/hydration',
},
{
label: 'Deferrable views',
path: 'guide/defer',
contentPath: 'guide/defer',
},
{
label: 'Image Optimization',
path: 'guide/image-optimization',
contentPath: 'guide/image-optimization',
},
{
label: 'Testing',
@@ -604,6 +589,81 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
],
},
{
label: 'Security',
path: 'guide/security',
contentPath: 'guide/security',
},
{
label: 'Pipes',
children: [
{
label: 'Overview',
path: 'guide/pipes',
contentPath: 'guide/pipes/overview',
},
{
label: 'Using a pipe in a template',
path: 'guide/pipes/template',
contentPath: 'guide/pipes/template',
},
{
label: 'Custom pipes',
path: 'guide/pipes/transform-data',
contentPath: 'guide/pipes/transform-data',
},
{
label: 'Pipe precedence in expressions',
path: 'guide/pipes/precedence',
contentPath: 'guide/pipes/precedence',
},
{
label: 'Change detection with pipes',
path: 'guide/pipes/change-detection',
contentPath: 'guide/pipes/change-detection',
},
{
label: 'Unwrapping data from an observable',
path: 'guide/pipes/unwrapping-data-observables',
contentPath: 'guide/pipes/unwrapping-data-observables',
},
],
},
{
label: 'Http Client',
children: [
{
label: 'Overview',
path: 'guide/http',
contentPath: 'guide/http/overview',
},
{
label: 'Setting up HttpClient',
path: 'guide/http/setup',
contentPath: 'guide/http/setup',
},
{
label: 'Making requests',
path: 'guide/http/making-requests',
contentPath: 'guide/http/making-requests',
},
{
label: 'Intercepting requests and responses',
path: 'guide/http/interceptors',
contentPath: 'guide/http/interceptors',
},
{
label: 'Security',
path: 'guide/http/security',
contentPath: 'guide/http/security',
},
{
label: 'Testing',
path: 'guide/http/testing',
contentPath: 'guide/http/testing',
},
],
},
{
label: 'Animations',
children: [
@@ -634,6 +694,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
],
},
{
label: 'Custom Elements',
path: 'guide/elements',
contentPath: 'guide/elements',
},
],
},
{
@@ -765,9 +830,9 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
contentPath: 'best-practices/style-guide',
},
{
label: 'Security',
path: 'best-practices/security',
contentPath: 'guide/security', // Have not refactored due to build issues
label: 'Keeping up-to-date',
path: 'update',
contentPath: 'best-practices/update',
},
{
label: 'Accessibility',
@@ -799,11 +864,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
],
},
{
label: 'Keeping up-to-date',
path: 'update',
contentPath: 'best-practices/update',
},
],
},
{
@@ -888,7 +948,7 @@ export const TUTORIALS_SUB_NAVIGATION_DATA: NavigationItem[] = [
{
path: DefaultPage.TUTORIALS,
contentPath: 'tutorials/home',
label: 'Tutorials',
label: 'Tutorials'
},
];
@@ -1289,81 +1349,6 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
},
],
},
{
label: 'Concepts',
children: [
{
label: 'Overview',
path: 'reference/concepts',
contentPath: 'reference/concepts/overview',
},
{
label: 'NgModule',
children: [
{
label: 'Overview',
path: 'guide/ngmodules',
contentPath: 'guide/ngmodules/overview',
},
{
label: 'JS Modules vs NgModules',
path: 'guide/ngmodules/vs-jsmodule',
contentPath: 'guide/ngmodules/vs-jsmodule',
},
{
label: 'Launching your app with a root module',
path: 'guide/ngmodules/bootstrapping',
contentPath: 'guide/ngmodules/bootstrapping',
},
{
label: 'Sharing NgModules',
path: 'guide/ngmodules/sharing',
contentPath: 'guide/ngmodules/sharing',
},
{
label: 'Frequently used NgModules',
path: 'guide/ngmodules/frequent',
contentPath: 'guide/ngmodules/frequent',
},
{
label: 'Feature modules',
path: 'guide/ngmodules/feature-modules',
contentPath: 'guide/ngmodules/feature-modules',
},
{
label: 'Types of feature modules',
path: 'guide/ngmodules/module-types',
contentPath: 'guide/ngmodules/module-types',
},
{
label: 'Providing dependencies',
path: 'guide/ngmodules/providers',
contentPath: 'guide/ngmodules/providers',
},
{
label: 'Singleton services',
path: 'guide/ngmodules/singleton-services',
contentPath: 'guide/ngmodules/singleton-services',
},
{
label: 'Lazy-loading feature modules',
path: 'guide/ngmodules/lazy-loading',
contentPath: 'guide/ngmodules/lazy-loading',
},
{
label: 'NgModule API',
path: 'guide/ngmodules/api',
contentPath: 'guide/ngmodules/api',
},
{
label: 'NgModule FAQs',
path: 'guide/ngmodules/faq',
contentPath: 'guide/ngmodules/faq',
},
],
},
],
},
];
const FOOTER_NAVIGATION_DATA: NavigationItem[] = [
-6
View File
@@ -5,7 +5,6 @@ package(default_visibility = ["//adev:__subpackages__"])
copy_to_directory(
name = "content",
srcs = [
"//adev/src/content",
"//adev/src/content/best-practices",
"//adev/src/content/best-practices/runtime-performance",
"//adev/src/content/ecosystem",
@@ -27,7 +26,6 @@ copy_to_directory(
"//adev/src/content/introduction",
"//adev/src/content/introduction/essentials",
"//adev/src/content/reference",
"//adev/src/content/reference/concepts",
"//adev/src/content/reference/configs",
"//adev/src/content/reference/errors",
"//adev/src/content/reference/extended-diagnostics",
@@ -38,7 +36,6 @@ copy_to_directory(
"//adev/src/content/tutorials",
"//adev/src/content/tutorials/first-app:first-app-guides",
"//adev/src/content/tutorials/learn-angular:learn-angular-guides",
"//aio/content/cli/help:cli_docs",
"//packages/animations:animations_docs",
"//packages/animations/browser:animations_browser_docs",
"//packages/animations/browser/testing:animations_browser_testing_docs",
@@ -48,13 +45,11 @@ copy_to_directory(
"//packages/common/testing:common_testing_docs",
"//packages/common/upgrade:common_upgrade_docs",
"//packages/core:core_docs",
"//packages/core/global:core_global_docs",
"//packages/core/rxjs-interop:core_rxjs-interop_docs",
"//packages/core/testing:core_testing_docs",
"//packages/elements:elements_docs",
"//packages/forms:forms_docs",
"//packages/localize:localize_docs",
"//packages/localize/init:localize_docs",
"//packages/platform-browser:platform-browser_docs",
"//packages/platform-browser-dynamic:platform-browser_dynamic_docs",
"//packages/platform-browser-dynamic/testing:platform-browser_dynamic_testing_docs",
@@ -74,7 +69,6 @@ copy_to_directory(
"//tools/manual_api_docs/elements:elements_docs",
],
replace_prefixes = {
"aio/content/cli/**/": "cli/",
"adev/src/content": "",
"packages/**/": "api/",
"tools/**/": "api/",
+1 -1
View File
@@ -6,7 +6,7 @@ exports_files(
copy_to_bin(
name = "images",
srcs = glob(["**/*"]),
srcs = glob(["*"]),
visibility = [
"//visibility:public",
],
+2 -1
View File
@@ -18,4 +18,5 @@ import {Type} from '@angular/core';
* Map of the previews components, values are functions which returns the promise of the component type, which will be displayed as preview in the ExampleViewer component.
* Keys has to be equal to paths written down in the docs markdown files.
*/
export const PREVIEWS_COMPONENTS_MAP: Record<string, () => Promise<Type<unknown>>> = {};
export const PREVIEWS_COMPONENTS_MAP: Record<string, () => Promise<Type<unknown>>> = {
};
-10
View File
@@ -1,10 +0,0 @@
load("@npm//@angular/docs:index.bzl", "generate_guides")
generate_guides(
name = "content",
srcs = [
"error.md",
],
data = [],
visibility = ["//adev:__subpackages__"],
)
@@ -82,10 +82,7 @@ export class NgComponentOutletCompleteExample implements OnInit {
@ViewChild('svet', {static: true}) svetTemplateRef!: TemplateRef<any>;
myContent?: any[][];
constructor(
injector: Injector,
private vcr: ViewContainerRef,
) {
constructor(injector: Injector, private vcr: ViewContainerRef) {
this.myInjector = Injector.create({
providers: [{provide: Greeter, deps: []}],
parent: injector,
@@ -51,10 +51,7 @@ class DataListProvider {
`,
})
class GiantList {
constructor(
private ref: ChangeDetectorRef,
public dataProvider: DataListProvider,
) {
constructor(private ref: ChangeDetectorRef, public dataProvider: DataListProvider) {
ref.detach();
setInterval(() => {
this.ref.detectChanges();
@@ -84,10 +81,7 @@ class DataProvider {
@Component({selector: 'live-data', inputs: ['live'], template: 'Data: {{dataProvider.data}}'})
class LiveData {
constructor(
private ref: ChangeDetectorRef,
public dataProvider: DataProvider,
) {}
constructor(private ref: ChangeDetectorRef, public dataProvider: DataProvider) {}
@Input()
set live(value: boolean) {
@@ -70,10 +70,7 @@ describe('navigate for test examples', () => {
`,
})
class SearchCmp {
constructor(
readonly route: ActivatedRoute,
readonly router: Router,
) {}
constructor(readonly route: ActivatedRoute, readonly router: Router) {}
async searchFor(thing: string) {
await this.router.navigate([], {queryParams: {query: thing}});
@@ -5,6 +5,5 @@ generate_guides(
srcs = glob([
"*.md",
]),
mermaid_blocks = True,
visibility = ["//adev:__subpackages__"],
)
@@ -33,7 +33,8 @@ If Angular handles an event within a component without `OnPush` strategy, the fr
As an example, if we set the change detection strategy of `MainComponent` to `OnPush` and the user interacts with a component outside the subtree with root `MainComponent`, Angular will check all the green components from the diagram below (`AppComponent`, `HeaderComponent`, `SearchComponent`, `ButtonComponent`) unless `MainComponent` receives new inputs:
```mermaid
<!-- TODO(josephperrott): enable this mermaid chart -->
```
graph TD;
app[AppComponent] --- header[HeaderComponent];
app --- main["MainComponent (OnPush)"];
@@ -59,9 +60,10 @@ If Angular handles an event within a component with OnPush strategy, the framewo
As an example, if Angular handles an event within `MainComponent`, the framework will run change detection in the entire component tree. Angular will ignore the subtree with root `LoginComponent` because it has `OnPush` and the event happened outside of its scope.
<img alt="Change detection propagation from OnPush component" src="assets/images/best-practices/runtime-performance/on-push-trigger.svg">
<img alt="Change detection propagation from OnPush component" src="assets/content/images/best-practices/runtime-performance/on-push-trigger.svg">
```mermaid
<!-- TODO(josephperrott): enable this mermaid chart -->
```
graph TD;
app[AppComponent] --- header[HeaderComponent];
app --- main["MainComponent (OnPush)"];
@@ -87,7 +89,8 @@ If Angular handles an event in a component with OnPush, the framework will execu
As an example, in the diagram below, Angular handles an event in `LoginComponent` which uses OnPush. Angular will invoke change detection in the entire component subtree including `MainComponent` (`LoginComponent`’s parent), even though `MainComponent` has `OnPush` as well. Angular checks `MainComponent` as well because `LoginComponent` is part of its view.
```mermaid
<!-- TODO(josephperrott): enable this mermaid chart -->
```
graph TD;
app[AppComponent] --- header[HeaderComponent];
app --- main["MainComponent (OnPush)"];
@@ -112,7 +115,8 @@ Angular will run change detection within a child component with `OnPush` when se
For example, in the diagram below, `AppComponent` passes a new input to `MainComponent`, which has `OnPush`. Angular will run change detection in `MainComponent` but will not run change detection in `LoginComponent`, which also has `OnPush`, unless it receives new inputs as well.
```mermaid
<!-- TODO(josephperrott): enable this mermaid chart -->
```
graph TD;
app[AppComponent] --- header[HeaderComponent];
app --- main["MainComponent (OnPush)"];
@@ -10,7 +10,7 @@ A single slow computation within a template or a lifecycle hook can slow down th
You can identify heavy computations with Angular DevTools’ profiler. In the performance timeline, click a bar to preview a particular change detection cycle. This displays a bar chart, which shows how long the framework spent in change detection for each component. When you click a component, you can preview how long Angular spent evaluating its template and lifecycle hooks.
<img alt="Angular DevTools profiler preview showing slow computation" src="assets/images/best-practices/runtime-performance/slow-computations.png">
<img alt="Angular DevTools profiler preview showing slow computation" src="assets/content/images/best-practices/runtime-performance/slow-computations.png">
For example, in the preceding screenshot, the second recorded change detection cycle is selected. Angular spent over 573 ms on this cycle, with the most time spent in the `EmployeeListComponent`. In the details panel, you can see that Angular spent over 297 ms evaluating the template of the `EmployeeListComponent`.
@@ -13,7 +13,7 @@ This section covers how to identify such conditions, and how to run code outside
You can detect unnecessary change detection calls using Angular DevTools. Often they appear as consecutive bars in the profiler’s timeline with source `setTimeout`, `setInterval`, `requestAnimationFrame`, or an event handler. When you have limited calls within your application of these APIs, the change detection invocation is usually caused by a third-party library.
<img alt="Angular DevTools profiler preview showing Zone pollution" src="assets/images/best-practices/runtime-performance/zone-pollution.png">
<img alt="Angular DevTools profiler preview showing Zone pollution" src="assets/content/images/best-practices/runtime-performance/zone-pollution.png">
In the image above, there is a series of change detection calls triggered by event handlers associated with an element. That’s a common challenge when using third-party, non-native Angular components, which do not alter the default behavior of `NgZone`.
@@ -884,7 +884,7 @@ Here are templates and/or snippets for some of the web development editors and I
<a href="https://marketplace.visualstudio.com/items?itemName=johnpapa.Angular2">
<img alt="Use Extension" src="assets/images/guide/styleguide/use-extension.gif">
<img alt="Use Extension" src="assets/content/images/guide/styleguide/use-extension.gif">
</a>
@@ -58,7 +58,7 @@ In Chrome:
1. Go to the **Network tab**.
1. Select **Offline** in the **Throttling** dropdown menu.
<img alt="The offline option in the Network tab is selected" src="assets/images/guide/service-worker/offline-option.png">
<img alt="The offline option in the Network tab is selected" src="assets/content/images/guide/service-worker/offline-option.png">
Now the application has no access to network interaction.
@@ -69,7 +69,7 @@ On a refresh, the page loads normally.
Look at the Network tab to verify that the service worker is active.
<img alt="Requests are marked as from ServiceWorker" src="assets/images/guide/service-worker/sw-active.png">
<img alt="Requests are marked as from ServiceWorker" src="assets/content/images/guide/service-worker/sw-active.png">
HELPFUL: Under the "Size" column, the requests state is `(ServiceWorker)`.
This means that the resources are not being loaded from the network.
@@ -121,7 +121,7 @@ Now look at how the browser and service worker handle the updated application.
1. Open [http://localhost:8080](http://localhost:8080) again in the same window.
What happens?
<img alt="It still says Welcome to Service Workers!" src="assets/images/guide/service-worker/welcome-msg-en.png">
<img alt="It still says Welcome to Service Workers!" src="assets/content/images/guide/service-worker/welcome-msg-en.png">
What went wrong?
_Nothing, actually!_
@@ -138,7 +138,7 @@ Now look at how the browser and service worker handle the updated application.
1. Refresh the page.
<img alt="The text has changed to say Bienvenue à app!" src="assets/images/guide/service-worker/welcome-msg-fr.png">
<img alt="The text has changed to say Bienvenue à app!" src="assets/content/images/guide/service-worker/welcome-msg-fr.png">
The service worker installed the updated version of your application _in the background_, and the next time the page is loaded or reloaded, the service worker switches to the latest version.
@@ -1,6 +1,7 @@
import {browser, element, by} from 'protractor';
import { browser, element, by } from 'protractor';
describe('Accessibility example e2e tests', () => {
beforeEach(() => browser.get(''));
it('should display Accessibility Example', async () => {
@@ -12,4 +13,5 @@ describe('Accessibility example e2e tests', () => {
expect(await element(by.css('input')).getAttribute('value')).toEqual('16');
expect(await element(by.css('app-example-progressbar div')).getCssValue('width')).toBe('48px');
});
});
@@ -1,5 +1,5 @@
import {AppPage} from './app.po';
import {browser, logging} from 'protractor';
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
@@ -13,10 +13,8 @@ describe('workspace-project App', () => {
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(
jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry),
);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});
@@ -1,10 +1,12 @@
import {TestBed} from '@angular/core/testing';
import {AppComponent} from './app.component';
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AppComponent],
declarations: [
AppComponent
],
}).compileComponents();
});
@@ -1,5 +1,5 @@
import {browser} from 'protractor';
import {logging} from 'selenium-webdriver';
import { browser } from 'protractor';
import { logging } from 'selenium-webdriver';
import * as openClose from './open-close.po';
import * as statusSlider from './status-slider.po';
import * as toggle from './toggle.po';
@@ -7,8 +7,8 @@ import * as enterLeave from './enter-leave.po';
import * as auto from './auto.po';
import * as filterStagger from './filter-stagger.po';
import * as heroGroups from './hero-groups';
import {getLinkById, sleepFor} from './util';
import {getComponentSection, getToggleButton} from './querying.po';
import { getLinkById, sleepFor } from './util';
import { getComponentSection, getToggleButton } from './querying.po';
describe('Animation Tests', () => {
const routingAnimationDuration = 350;
@@ -42,10 +42,7 @@ describe('Animation Tests', () => {
if (text.includes('Closed')) {
await toggleButton.click();
await browser.wait(
async () => (await container.getCssValue('height')) === openHeight,
2000,
);
await browser.wait(async () => await container.getCssValue('height') === openHeight, 2000);
}
text = await container.getText();
@@ -62,10 +59,7 @@ describe('Animation Tests', () => {
if (text.includes('Open')) {
await toggleButton.click();
await browser.wait(
async () => (await container.getCssValue('height')) === closedHeight,
2000,
);
await browser.wait(async () => await container.getCssValue('height') === closedHeight, 2000);
}
text = await container.getText();
@@ -82,7 +76,7 @@ describe('Animation Tests', () => {
await toggleButton.click();
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
const animationMessages = logs.filter(({message}) => message.includes('Animation'));
const animationMessages = logs.filter(({ message }) => message.includes('Animation'));
expect(animationMessages.length).toBeGreaterThan(0);
});
@@ -104,10 +98,7 @@ describe('Animation Tests', () => {
if (text === 'Active') {
await toggleButton.click();
await browser.wait(
async () => (await container.getCssValue('backgroundColor')) === inactiveColor,
3000,
);
await browser.wait(async () => await container.getCssValue('backgroundColor') === inactiveColor, 3000);
}
text = await container.getText();
@@ -124,10 +115,7 @@ describe('Animation Tests', () => {
if (text === 'Inactive') {
await toggleButton.click();
await browser.wait(
async () => (await container.getCssValue('backgroundColor')) === activeColor,
3000,
);
await browser.wait(async () => await container.getCssValue('backgroundColor') === activeColor, 3000);
}
text = await container.getText();
@@ -178,7 +166,7 @@ describe('Animation Tests', () => {
const hero = heroesList.get(0);
await hero.click();
await browser.wait(async () => (await heroesList.count()) < total, 2000);
await browser.wait(async () => await heroesList.count() < total, 2000);
});
});
@@ -202,7 +190,7 @@ describe('Animation Tests', () => {
const hero = heroesList.get(0);
await hero.click();
await browser.wait(async () => (await heroesList.count()) < total, 2000);
await browser.wait(async () => await heroesList.count() < total, 2000);
});
});
@@ -226,7 +214,7 @@ describe('Animation Tests', () => {
const input = filterStagger.getInput();
await input.sendKeys('Mag');
await browser.wait(async () => (await heroesList.count()) === 2, 2000);
await browser.wait(async () => await heroesList.count() === 2, 2000);
const newTotal = await heroesList.count();
expect(newTotal).toBeLessThan(total);
@@ -257,7 +245,7 @@ describe('Animation Tests', () => {
const hero = heroesList.get(0);
await hero.click();
await browser.wait(async () => (await heroesList.count()) < total, 2000);
await browser.wait(async () => await heroesList.count() < total, 2000);
});
});
@@ -303,5 +291,6 @@ describe('Animation Tests', () => {
await newPageSleepFor(queryingAnimationDuration);
expect(await toggleButton.isEnabled()).toBe(true);
});
});
});
@@ -1,5 +1,5 @@
import {by} from 'protractor';
import {locate} from './util';
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-hero-list-auto-page');
@@ -1,5 +1,5 @@
import {by} from 'protractor';
import {locate} from './util';
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-hero-list-enter-leave-page');
@@ -1,5 +1,5 @@
import {by} from 'protractor';
import {locate} from './util';
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-hero-list-page');
@@ -1,5 +1,5 @@
import {by} from 'protractor';
import {locate} from './util';
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-hero-list-groups-page');
@@ -1,5 +1,5 @@
import {by} from 'protractor';
import {locate} from './util';
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-open-close-page');
@@ -1,5 +1,5 @@
import {by} from 'protractor';
import {locate} from './util';
import { by } from 'protractor';
import { locate } from './util';
export function getComponent() {
return by.css('app-querying');
@@ -1,5 +1,5 @@
import {by} from 'protractor';
import {locate} from './util';
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-status-slider-page');
@@ -1,5 +1,5 @@
import {by} from 'protractor';
import {locate} from './util';
import { by } from 'protractor';
import { locate } from './util';
export function getPage() {
return by.css('app-toggle-animations-child-page');
@@ -1,4 +1,4 @@
import {Locator, ElementFinder, browser, by, element} from 'protractor';
import { Locator, ElementFinder, browser, by, element } from 'protractor';
/**
*
@@ -6,9 +6,8 @@ import {Locator, ElementFinder, browser, by, element} from 'protractor';
*/
export function locate(locator: Locator, ...locators: Locator[]) {
return locators.reduce(
(current: ElementFinder, next: Locator) => current.element(next),
element(locator),
) as ElementFinder;
(current: ElementFinder, next: Locator) => current.element(next),
element(locator)) as ElementFinder;
}
export async function sleepFor(time = 1000) {
@@ -1,9 +1,9 @@
import {Component} from '@angular/core';
import { Component } from '@angular/core';
@Component({
standalone: true,
selector: 'app-about',
templateUrl: './about.component.html',
styleUrls: ['./about.component.css'],
styleUrls: ['./about.component.css']
})
export class AboutComponent {}
@@ -1,15 +1,15 @@
// #docplaster
// #docregion animation-const, trigger-const
import {animation, style, animate, trigger, transition, useAnimation} from '@angular/animations';
import { animation, style, animate, trigger, transition, useAnimation } from '@angular/animations';
// #enddocregion trigger-const
export const transitionAnimation = animation([
style({
height: '{{ height }}',
opacity: '{{ opacity }}',
backgroundColor: '{{ backgroundColor }}',
backgroundColor: '{{ backgroundColor }}'
}),
animate('{{ time }}'),
animate('{{ time }}')
]);
// #enddocregion animation-const
@@ -21,9 +21,9 @@ export const triggerAnimation = trigger('openClose', [
height: 0,
opacity: 1,
backgroundColor: 'red',
time: '1s',
},
}),
]),
time: '1s'
}
})
])
]);
// #enddocregion trigger-const
@@ -1,71 +1,71 @@
import {
animation,
trigger,
animateChild,
group,
transition,
animate,
style,
query,
animation, trigger, animateChild, group,
transition, animate, style, query
} from '@angular/animations';
export const transitionAnimation = animation([
style({
height: '{{ height }}',
opacity: '{{ opacity }}',
backgroundColor: '{{ backgroundColor }}',
backgroundColor: '{{ backgroundColor }}'
}),
animate('{{ time }}'),
animate('{{ time }}')
]);
// Routable animations
// #docregion route-animations
export const slideInAnimation =
// #docregion style-view
// #docregion style-view
trigger('routeAnimations', [
transition('HomePage <=> AboutPage', [
style({position: 'relative'}),
style({ position: 'relative' }),
query(':enter, :leave', [
style({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
}),
width: '100%'
})
]),
// #enddocregion style-view
// #docregion query
query(':enter', [style({left: '-100%'})], {optional: true}),
query(':leave', animateChild(), {optional: true}),
// #enddocregion style-view
// #docregion query
query(':enter', [
style({ left: '-100%' })
], { optional: true }),
query(':leave', animateChild(), { optional: true }),
group([
query(':leave', [animate('300ms ease-out', style({left: '100%'}))], {optional: true}),
query(':enter', [animate('300ms ease-out', style({left: '0%'}))], {optional: true}),
query(':leave', [
animate('300ms ease-out', style({ left: '100%' }))
], { optional: true }),
query(':enter', [
animate('300ms ease-out', style({ left: '0%' }))
], { optional: true }),
]),
]),
transition('* <=> *', [
style({position: 'relative'}),
query(
':enter, :leave',
[
style({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
}),
],
{optional: true},
),
query(':enter', [style({left: '-100%'})], {optional: true}),
query(':leave', animateChild(), {optional: true}),
style({ position: 'relative' }),
query(':enter, :leave', [
style({
position: 'absolute',
top: 0,
left: 0,
width: '100%'
})
], { optional: true }),
query(':enter', [
style({ left: '-100%' })
], { optional: true }),
query(':leave', animateChild(), { optional: true }),
group([
query(':leave', [animate('200ms ease-out', style({left: '100%', opacity: 0}))], {
optional: true,
}),
query(':enter', [animate('300ms ease-out', style({left: '0%'}))], {optional: true}),
query('@*', animateChild(), {optional: true}),
query(':leave', [
animate('200ms ease-out', style({ left: '100%', opacity: 0 }))
], { optional: true }),
query(':enter', [
animate('300ms ease-out', style({ left: '0%' }))
], { optional: true }),
query('@*', animateChild(), { optional: true })
]),
]),
])
// #enddocregion query
]);
// #enddocregion route-animations
// #enddocregion route-animations
@@ -1,6 +1,6 @@
// #docplaster
// #docregion imports
import {Component, HostBinding} from '@angular/core';
import { Component, HostBinding } from '@angular/core';
import {
trigger,
state,
@@ -11,8 +11,8 @@ import {
} from '@angular/animations';
// #enddocregion imports
import {ChildrenOutletContexts, RouterLink, RouterOutlet} from '@angular/router';
import {slideInAnimation} from './animations';
import { ChildrenOutletContexts, RouterLink, RouterOutlet } from '@angular/router';
import { slideInAnimation } from './animations';
// #docregion decorator, toggle-app-animations, define
@Component({
@@ -22,31 +22,31 @@ import {slideInAnimation} from './animations';
styleUrls: ['app.component.css'],
imports: [RouterLink, RouterOutlet],
animations: [
// #enddocregion decorator
slideInAnimation,
// #docregion decorator
// #enddocregion toggle-app-animations, define
// #enddocregion decorator
slideInAnimation
// #docregion decorator
// #enddocregion toggle-app-animations, define
// animation triggers go here
// #docregion toggle-app-animations, define
],
// #docregion toggle-app-animations, define
]
})
// #enddocregion decorator, define
export class AppComponent {
@HostBinding('@.disabled')
public animationsDisabled = false;
// #enddocregion toggle-app-animations
// #enddocregion toggle-app-animations
// #docregion get-route-animations-data
// #docregion get-route-animations-data
constructor(private contexts: ChildrenOutletContexts) {}
getRouteAnimationData() {
return this.contexts.getContext('primary')?.route?.snapshot?.data?.['animation'];
}
// #enddocregion get-route-animations-data
// #enddocregion get-route-animations-data
toggleAnimations() {
this.animationsDisabled = !this.animationsDisabled;
}
// #docregion toggle-app-animations
// #docregion toggle-app-animations
}
// #enddocregion toggle-app-animations
@@ -1,14 +1,15 @@
import {ApplicationConfig} from '@angular/core';
import {routes} from './app.routes';
import {provideRouter} from '@angular/router';
import {provideProtractorTestingSupport} from '@angular/platform-browser';
import {provideAnimations} from '@angular/platform-browser/animations';
import { ApplicationConfig } from '@angular/core';
import { routes } from './app.routes';
import { provideRouter } from '@angular/router';
import { provideProtractorTestingSupport } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
providers: [
// needed for supporting e2e tests
provideProtractorTestingSupport(),
provideRouter(routes),
provideAnimations(),
],
providers: [
// needed for supporting e2e tests
provideProtractorTestingSupport(),
provideRouter(routes),
provideAnimations(),
]
};
@@ -1,10 +1,13 @@
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
imports: [BrowserModule, BrowserAnimationsModule],
declarations: [],
bootstrap: [],
imports: [
BrowserModule,
BrowserAnimationsModule
],
declarations: [ ],
bootstrap: [ ]
})
export class AppModule {}
export class AppModule { }
@@ -1,73 +1,73 @@
import {Routes} from '@angular/router';
import {OpenClosePageComponent} from './open-close-page.component';
import {StatusSliderPageComponent} from './status-slider-page.component';
import {ToggleAnimationsPageComponent} from './toggle-animations-page.component';
import {HeroListPageComponent} from './hero-list-page.component';
import {HeroListGroupPageComponent} from './hero-list-group-page.component';
import {HeroListEnterLeavePageComponent} from './hero-list-enter-leave-page.component';
import {HeroListAutoCalcPageComponent} from './hero-list-auto-page.component';
import {InsertRemoveComponent} from './insert-remove.component';
import {QueryingComponent} from './querying.component';
import {HomeComponent} from './home.component';
import {AboutComponent} from './about.component';
import { Routes } from '@angular/router';
import { OpenClosePageComponent } from './open-close-page.component';
import { StatusSliderPageComponent } from './status-slider-page.component';
import { ToggleAnimationsPageComponent } from './toggle-animations-page.component';
import { HeroListPageComponent } from './hero-list-page.component';
import { HeroListGroupPageComponent } from './hero-list-group-page.component';
import { HeroListEnterLeavePageComponent } from './hero-list-enter-leave-page.component';
import { HeroListAutoCalcPageComponent } from './hero-list-auto-page.component';
import { InsertRemoveComponent } from './insert-remove.component';
import { QueryingComponent } from './querying.component';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
// #docregion route-animation-data
export const routes: Routes = [
{path: '', pathMatch: 'full', redirectTo: '/enter-leave'},
{
path: 'open-close',
component: OpenClosePageComponent,
data: {animation: 'openClosePage'},
},
{
path: 'status',
component: StatusSliderPageComponent,
data: {animation: 'statusPage'},
},
{
path: 'toggle',
component: ToggleAnimationsPageComponent,
data: {animation: 'togglePage'},
},
{
path: 'heroes',
component: HeroListPageComponent,
data: {animation: 'filterPage'},
},
{
path: 'hero-groups',
component: HeroListGroupPageComponent,
data: {animation: 'heroGroupPage'},
},
{
path: 'enter-leave',
component: HeroListEnterLeavePageComponent,
data: {animation: 'enterLeavePage'},
},
{
path: 'auto',
component: HeroListAutoCalcPageComponent,
data: {animation: 'autoPage'},
},
{
path: 'insert-remove',
component: InsertRemoveComponent,
data: {animation: 'insertRemovePage'},
},
{
path: 'querying',
component: QueryingComponent,
data: {animation: 'queryingPage'},
},
{
path: 'home',
component: HomeComponent,
data: {animation: 'HomePage'},
},
{
path: 'about',
component: AboutComponent,
data: {animation: 'AboutPage'},
},
{ path: '', pathMatch: 'full', redirectTo: '/enter-leave' },
{
path: 'open-close',
component: OpenClosePageComponent,
data: { animation: 'openClosePage' }
},
{
path: 'status',
component: StatusSliderPageComponent,
data: { animation: 'statusPage' }
},
{
path: 'toggle',
component: ToggleAnimationsPageComponent,
data: { animation: 'togglePage' }
},
{
path: 'heroes',
component: HeroListPageComponent,
data: { animation: 'filterPage' }
},
{
path: 'hero-groups',
component: HeroListGroupPageComponent,
data: { animation: 'heroGroupPage' }
},
{
path: 'enter-leave',
component: HeroListEnterLeavePageComponent,
data: { animation: 'enterLeavePage' }
},
{
path: 'auto',
component: HeroListAutoCalcPageComponent,
data: { animation: 'autoPage' }
},
{
path: 'insert-remove',
component: InsertRemoveComponent,
data: { animation: 'insertRemovePage' }
},
{
path: 'querying',
component: QueryingComponent,
data: { animation: 'queryingPage' }
},
{
path: 'home',
component: HomeComponent,
data: { animation: 'HomePage' }
},
{
path: 'about',
component: AboutComponent,
data: { animation: 'AboutPage' }
},
];
// #enddocregion route-animation-data
@@ -1,6 +1,6 @@
import {Component} from '@angular/core';
import {HEROES} from './mock-heroes';
import {HeroListAutoComponent} from './hero-list-auto.component';
import { Component } from '@angular/core';
import { HEROES } from './mock-heroes';
import { HeroListAutoComponent } from './hero-list-auto.component';
@Component({
standalone: true,
@@ -12,12 +12,12 @@ import {HeroListAutoComponent} from './hero-list-auto.component';
<app-hero-list-auto [heroes]="heroes" (remove)="onRemove($event)"></app-hero-list-auto>
</section>
`,
imports: [HeroListAutoComponent],
imports: [HeroListAutoComponent]
})
export class HeroListAutoCalcPageComponent {
heroes = HEROES.slice();
onRemove(id: number) {
this.heroes = this.heroes.filter((hero) => hero.id !== id);
this.heroes = this.heroes.filter(hero => hero.id !== id);
}
}
@@ -1,8 +1,19 @@
import {Component, Input, Output, EventEmitter} from '@angular/core';
import {trigger, state, style, animate, transition} from '@angular/animations';
import {
Component,
Input,
Output,
EventEmitter
} from '@angular/core';
import {
trigger,
state,
style,
animate,
transition
} from '@angular/animations';
import {Hero} from './hero';
import {NgFor} from '@angular/common';
import { Hero } from './hero';
import { NgFor } from '@angular/common';
@Component({
standalone: true,
@@ -13,18 +24,21 @@ import {NgFor} from '@angular/common';
// #docregion auto-calc
animations: [
trigger('shrinkOut', [
state('in', style({height: '*'})),
transition('* => void', [style({height: '*'}), animate(250, style({height: 0}))]),
]),
],
state('in', style({ height: '*' })),
transition('* => void', [
style({ height: '*' }),
animate(250, style({ height: 0 }))
])
])
]
// #enddocregion auto-calc
})
export class HeroListAutoComponent {
@Input() heroes: Hero[] = [];
@Input() heroes: Hero[] = [];
@Output() remove = new EventEmitter<number>();
@Output() remove = new EventEmitter<number>();
removeHero(id: number) {
this.remove.emit(id);
}
removeHero(id: number) {
this.remove.emit(id);
}
}
@@ -1,6 +1,6 @@
import {Component} from '@angular/core';
import {HEROES} from './mock-heroes';
import {HeroListEnterLeaveComponent} from './hero-list-enter-leave.component';
import { Component } from '@angular/core';
import { HEROES } from './mock-heroes';
import { HeroListEnterLeaveComponent } from './hero-list-enter-leave.component';
@Component({
standalone: true,
@@ -12,12 +12,12 @@ import {HeroListEnterLeaveComponent} from './hero-list-enter-leave.component';
<app-hero-list-enter-leave [heroes]="heroes" (remove)="onRemove($event)"></app-hero-list-enter-leave>
</section>
`,
imports: [HeroListEnterLeaveComponent],
imports: [HeroListEnterLeaveComponent]
})
export class HeroListEnterLeavePageComponent {
heroes = HEROES.slice();
onRemove(id: number) {
this.heroes = this.heroes.filter((hero) => hero.id !== id);
this.heroes = this.heroes.filter(hero => hero.id !== id);
}
}
@@ -1,8 +1,19 @@
import {Component, Input, Output, EventEmitter} from '@angular/core';
import {trigger, state, style, animate, transition} from '@angular/animations';
import {
Component,
Input,
Output,
EventEmitter
} from '@angular/core';
import {
trigger,
state,
style,
animate,
transition
} from '@angular/animations';
import {Hero} from './hero';
import {NgFor} from '@angular/common';
import { Hero } from './hero';
import { NgFor } from '@angular/common';
@Component({
standalone: true,
@@ -24,11 +35,16 @@ import {NgFor} from '@angular/common';
// #docregion animationdef
animations: [
trigger('flyInOut', [
state('in', style({transform: 'translateX(0)'})),
transition('void => *', [style({transform: 'translateX(-100%)'}), animate(100)]),
transition('* => void', [animate(100, style({transform: 'translateX(100%)'}))]),
]),
],
state('in', style({ transform: 'translateX(0)' })),
transition('void => *', [
style({ transform: 'translateX(-100%)' }),
animate(100)
]),
transition('* => void', [
animate(100, style({ transform: 'translateX(100%)' }))
])
])
]
// #enddocregion animationdef
})
export class HeroListEnterLeaveComponent {
@@ -1,6 +1,6 @@
import {Component} from '@angular/core';
import {HEROES} from './mock-heroes';
import {HeroListGroupsComponent} from './hero-list-groups.component';
import { Component } from '@angular/core';
import { HEROES } from './mock-heroes';
import { HeroListGroupsComponent } from './hero-list-groups.component';
@Component({
standalone: true,
@@ -12,12 +12,12 @@ import {HeroListGroupsComponent} from './hero-list-groups.component';
<app-hero-list-groups [heroes]="heroes" (remove)="onRemove($event)"></app-hero-list-groups>
</section>
`,
imports: [HeroListGroupsComponent],
imports: [HeroListGroupsComponent]
})
export class HeroListGroupPageComponent {
heroes = HEROES.slice();
onRemove(id: number) {
this.heroes = this.heroes.filter((hero) => hero.id !== id);
this.heroes = this.heroes.filter(hero => hero.id !== id);
}
}
@@ -1,8 +1,20 @@
import {Component, Input, Output, EventEmitter} from '@angular/core';
import {trigger, state, style, animate, transition, group} from '@angular/animations';
import {
Component,
Input,
Output,
EventEmitter
} from '@angular/core';
import {
trigger,
state,
style,
animate,
transition,
group
} from '@angular/animations';
import {Hero} from './hero';
import {NgFor} from '@angular/common';
import { Hero } from './hero';
import { NgFor } from '@angular/common';
@Component({
standalone: true,
@@ -24,59 +36,43 @@ import {NgFor} from '@angular/common';
// #docregion animationdef
animations: [
trigger('flyInOut', [
state(
'in',
style({
width: '*',
transform: 'translateX(0)',
opacity: 1,
}),
),
state('in', style({
width: '*',
transform: 'translateX(0)', opacity: 1
})),
transition(':enter', [
style({width: 10, transform: 'translateX(50px)', opacity: 0}),
style({ width: 10, transform: 'translateX(50px)', opacity: 0 }),
group([
animate(
'0.3s 0.1s ease',
style({
transform: 'translateX(0)',
width: '*',
}),
),
animate(
'0.3s ease',
style({
opacity: 1,
}),
),
]),
animate('0.3s 0.1s ease', style({
transform: 'translateX(0)',
width: '*'
})),
animate('0.3s ease', style({
opacity: 1
}))
])
]),
transition(':leave', [
group([
animate(
'0.3s ease',
style({
transform: 'translateX(50px)',
width: 10,
}),
),
animate(
'0.3s 0.2s ease',
style({
opacity: 0,
}),
),
]),
]),
]),
],
animate('0.3s ease', style({
transform: 'translateX(50px)',
width: 10
})),
animate('0.3s 0.2s ease', style({
opacity: 0
}))
])
])
])
]
// #enddocregion animationdef
})
export class HeroListGroupsComponent {
@Input() heroes: Hero[] = [];
@Input() heroes: Hero[] = [];
@Output() remove = new EventEmitter<number>();
@Output() remove = new EventEmitter<number>();
removeHero(id: number) {
this.remove.emit(id);
}
removeHero(id: number) {
this.remove.emit(id);
}
}
@@ -1,65 +1,66 @@
// #docplaster
// #docregion
import {Component, HostBinding, OnInit} from '@angular/core';
import {trigger, transition, animate, style, query, stagger} from '@angular/animations';
import {HEROES} from './mock-heroes';
import {Hero} from './hero';
import {NgFor} from '@angular/common';
import { Component, HostBinding, OnInit } from '@angular/core';
import { trigger, transition, animate, style, query, stagger } from '@angular/animations';
import { HEROES } from './mock-heroes';
import { Hero } from './hero';
import { NgFor } from '@angular/common';
// #docregion filter-animations
@Component({
// #enddocregion filter-animations
// #enddocregion filter-animations
standalone: true,
imports: [NgFor],
selector: 'app-hero-list-page',
templateUrl: 'hero-list-page.component.html',
styleUrls: ['hero-list-page.component.css'],
// #docregion page-animations, filter-animations
// #docregion page-animations, filter-animations
animations: [
// #enddocregion filter-animations
// #enddocregion filter-animations
trigger('pageAnimations', [
transition(':enter', [
query('.hero', [
style({opacity: 0, transform: 'translateY(-100px)'}),
stagger(30, [
animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', style({opacity: 1, transform: 'none'})),
]),
]),
]),
animate('500ms cubic-bezier(0.35, 0, 0.25, 1)',
style({ opacity: 1, transform: 'none' }))
])
])
])
]),
// #enddocregion page-animations
// #docregion increment
// #docregion filter-animations
// #enddocregion page-animations
// #docregion increment
// #docregion filter-animations
trigger('filterAnimation', [
transition(':enter, * => 0, * => -1', []),
transition(':increment', [
query(
':enter',
[
style({opacity: 0, width: 0}),
stagger(50, [animate('300ms ease-out', style({opacity: 1, width: '*'}))]),
],
{optional: true},
),
query(':enter', [
style({ opacity: 0, width: 0 }),
stagger(50, [
animate('300ms ease-out', style({ opacity: 1, width: '*' })),
]),
], { optional: true })
]),
transition(':decrement', [
query(':leave', [stagger(50, [animate('300ms ease-out', style({opacity: 0, width: 0}))])]),
query(':leave', [
stagger(50, [
animate('300ms ease-out', style({ opacity: 0, width: 0 })),
]),
])
]),
]),
// #enddocregion increment
],
// #enddocregion increment
]
})
export class HeroListPageComponent implements OnInit {
// #enddocregion filter-animations
// #enddocregion filter-animations
@HostBinding('@pageAnimations')
public animatePage = true;
// #docregion filter-animations
// #docregion filter-animations
heroesTotal = -1;
get heroes() {
return this._heroes;
}
get heroes() { return this._heroes; }
private _heroes: Hero[] = [];
ngOnInit() {
@@ -69,9 +70,7 @@ export class HeroListPageComponent implements OnInit {
updateCriteria(criteria: string) {
criteria = criteria ? criteria.trim() : '';
this._heroes = HEROES.filter((hero) =>
hero.name.toLowerCase().includes(criteria.toLowerCase()),
);
this._heroes = HEROES.filter(hero => hero.name.toLowerCase().includes(criteria.toLowerCase()));
const newTotal = this.heroes.length;
if (this.heroesTotal !== newTotal) {
@@ -1,9 +1,9 @@
import {Component} from '@angular/core';
import { Component } from '@angular/core';
@Component({
standalone: true,
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css'],
styleUrls: ['./home.component.css']
})
export class HomeComponent {}
@@ -1,22 +1,27 @@
// #docplaster
import {Component} from '@angular/core';
import {trigger, transition, animate, style} from '@angular/animations';
import {NgIf} from '@angular/common';
import { Component } from '@angular/core';
import { trigger, transition, animate, style } from '@angular/animations';
import { NgIf } from '@angular/common';
@Component({
standalone: true,
selector: 'app-insert-remove',
imports: [NgIf],
animations: [
// #docregion enter-leave-trigger
// #docregion enter-leave-trigger
trigger('myInsertRemoveTrigger', [
transition(':enter', [style({opacity: 0}), animate('100ms', style({opacity: 1}))]),
transition(':leave', [animate('100ms', style({opacity: 0}))]),
transition(':enter', [
style({ opacity: 0 }),
animate('100ms', style({ opacity: 1 })),
]),
transition(':leave', [
animate('100ms', style({ opacity: 0 }))
])
]),
// #enddocregion enter-leave-trigger
// #enddocregion enter-leave-trigger
],
templateUrl: 'insert-remove.component.html',
styleUrls: ['insert-remove.component.css'],
styleUrls: ['insert-remove.component.css']
})
export class InsertRemoveComponent {
isShown = false;
@@ -1,14 +1,14 @@
// #docregion
import {Hero} from './hero';
import { Hero } from './hero';
export const HEROES: Hero[] = [
{id: 12, name: 'Dr. Nice'},
{id: 13, name: 'Bombasto'},
{id: 14, name: 'Celeritas'},
{id: 15, name: 'Magneta'},
{id: 16, name: 'RubberMan'},
{id: 17, name: 'Dynama'},
{id: 18, name: 'Dr. IQ'},
{id: 19, name: 'Magma'},
{id: 20, name: 'Tornado'},
{ id: 12, name: 'Dr. Nice' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr. IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
@@ -1,5 +1,5 @@
import {Component} from '@angular/core';
import {OpenCloseComponent} from './open-close.component';
import { Component } from '@angular/core';
import { OpenCloseComponent } from './open-close.component';
@Component({
standalone: true,
@@ -13,7 +13,7 @@ import {OpenCloseComponent} from './open-close.component';
<app-open-close [logging]="logging"></app-open-close>
</section>
`,
imports: [OpenCloseComponent],
imports: [OpenCloseComponent]
})
export class OpenClosePageComponent {
logging = false;
@@ -1,53 +1,36 @@
import {Component, Input} from '@angular/core';
import {
trigger,
transition,
state,
animate,
style,
keyframes,
AnimationEvent,
} from '@angular/animations';
import { Component, Input } from '@angular/core';
import { trigger, transition, state, animate, style, keyframes, AnimationEvent } from '@angular/animations';
@Component({
standalone: true,
selector: 'app-open-close',
animations: [
// #docregion trigger
// #docregion trigger
trigger('openClose', [
state(
'open',
style({
height: '200px',
opacity: 1,
backgroundColor: 'yellow',
}),
),
state(
'close',
style({
height: '100px',
opacity: 0.5,
backgroundColor: 'green',
}),
),
state('open', style({
height: '200px',
opacity: 1,
backgroundColor: 'yellow'
})),
state('close', style({
height: '100px',
opacity: 0.5,
backgroundColor: 'green'
})),
// ...
transition('* => *', [
animate(
'1s',
keyframes([
style({opacity: 0.1, offset: 0.1}),
style({opacity: 0.6, offset: 0.2}),
style({opacity: 1, offset: 0.5}),
style({opacity: 0.2, offset: 0.7}),
]),
),
]),
]),
// #enddocregion trigger
animate('1s', keyframes ( [
style({ opacity: 0.1, offset: 0.1 }),
style({ opacity: 0.6, offset: 0.2 }),
style({ opacity: 1, offset: 0.5 }),
style({ opacity: 0.2, offset: 0.7 })
]))
])
])
// #enddocregion trigger
],
templateUrl: 'open-close.component.html',
styleUrls: ['open-close.component.css'],
styleUrls: ['open-close.component.css']
})
export class OpenCloseKeyframeComponent {
isOpen = false;
@@ -1,20 +1,20 @@
import {Component} from '@angular/core';
import {trigger, transition, state, animate, style} from '@angular/animations';
import { Component } from '@angular/core';
import { trigger, transition, state, animate, style } from '@angular/animations';
@Component({
standalone: true,
selector: 'app-open-close-boolean',
// #docregion trigger-boolean
// #docregion trigger-boolean
animations: [
trigger('openClose', [
state('true', style({height: '*'})),
state('false', style({height: '0px'})),
transition('false <=> true', animate(500)),
]),
state('true', style({ height: '*' })),
state('false', style({ height: '0px' })),
transition('false <=> true', animate(500))
])
],
// #enddocregion trigger-boolean
// #enddocregion trigger-boolean
templateUrl: 'open-close.component.2.html',
styleUrls: ['open-close.component.css'],
styleUrls: ['open-close.component.css']
})
export class OpenCloseBooleanComponent {
isOpen = false;
@@ -1,8 +1,8 @@
// #docplaster
// #docregion reusable
import {Component, Input} from '@angular/core';
import {transition, trigger, useAnimation, AnimationEvent} from '@angular/animations';
import {transitionAnimation} from './animations';
import { Component, Input } from '@angular/core';
import { transition, trigger, useAnimation, AnimationEvent } from '@angular/animations';
import { transitionAnimation } from './animations';
@Component({
standalone: true,
@@ -15,14 +15,14 @@ import {transitionAnimation} from './animations';
height: 0,
opacity: 1,
backgroundColor: 'red',
time: '1s',
},
}),
]),
]),
time: '1s'
}
})
])
])
],
templateUrl: 'open-close.component.html',
styleUrls: ['open-close.component.css'],
styleUrls: ['open-close.component.css']
})
// #enddocregion reusable
export class OpenCloseBooleanComponent {
@@ -38,4 +38,4 @@ export class OpenCloseBooleanComponent {
return;
}
}
}
}
@@ -1,11 +1,11 @@
// #docplaster
// #docregion
import {Component} from '@angular/core';
import {trigger, transition, state, animate, style} from '@angular/animations';
import { Component } from '@angular/core';
import { trigger, transition, state, animate, style } from '@angular/animations';
// #docregion toggle-animation
@Component({
// #enddocregion toggle-animation
// #enddocregion toggle-animation
standalone: true,
selector: 'app-open-close-toggle',
templateUrl: 'open-close.component.4.html',
@@ -14,32 +14,28 @@ import {trigger, transition, state, animate, style} from '@angular/animations';
animations: [
trigger('childAnimation', [
// ...
// #enddocregion toggle-animation
state(
'open',
style({
width: '250px',
opacity: 1,
backgroundColor: 'yellow',
}),
),
state(
'closed',
style({
width: '100px',
opacity: 0.8,
backgroundColor: 'blue',
}),
),
transition('* => *', [animate('1s')]),
// #docregion toggle-animation
// #enddocregion toggle-animation
state('open', style({
width: '250px',
opacity: 1,
backgroundColor: 'yellow'
})),
state('closed', style({
width: '100px',
opacity: 0.8,
backgroundColor: 'blue'
})),
transition('* => *', [
animate('1s')
]),
// #docregion toggle-animation
]),
],
})
export class OpenCloseChildComponent {
isDisabled = false;
isOpen = false;
// #enddocregion toggle-animation
// #enddocregion toggle-animation
toggleAnimations() {
this.isDisabled = !this.isDisabled;
}
@@ -47,6 +43,6 @@ export class OpenCloseChildComponent {
toggle() {
this.isOpen = !this.isOpen;
}
// #docregion toggle-animation
// #docregion toggle-animation
}
// #enddocregion toggle-animation
@@ -1,80 +1,90 @@
// #docplaster
import {Component, Input} from '@angular/core';
import {trigger, transition, state, animate, style, AnimationEvent} from '@angular/animations';
import { Component, Input } from '@angular/core';
import { trigger, transition, state, animate, style, AnimationEvent } from '@angular/animations';
// #docregion component, events1
@Component({
standalone: true,
selector: 'app-open-close',
// #docregion trigger-wildcard1, trigger-transition
// #docregion trigger-wildcard1, trigger-transition
animations: [
trigger('openClose', [
// #docregion state1
// #docregion state1
// ...
// #enddocregion events1
state(
'open',
style({
height: '200px',
opacity: 1,
backgroundColor: 'yellow',
}),
),
// #enddocregion state1
// #docregion state2
state(
'closed',
style({
height: '100px',
opacity: 0.8,
backgroundColor: 'blue',
}),
),
// #enddocregion state2, trigger-wildcard1
// #docregion transition1
transition('open => closed', [animate('1s')]),
// #enddocregion transition1
// #docregion transition2
transition('closed => open', [animate('0.5s')]),
// #enddocregion transition2, component
// #docregion trigger-wildcard1
transition('* => closed', [animate('1s')]),
transition('* => open', [animate('0.5s')]),
// #enddocregion trigger-wildcard1
// #docregion trigger-wildcard2
transition('open <=> closed', [animate('0.5s')]),
// #enddocregion trigger-wildcard2
// #docregion transition4
transition('* => open', [animate('1s', style({opacity: '*'}))]),
// #enddocregion transition4
transition('* => *', [animate('1s')]),
// #enddocregion trigger-transition
// #docregion component, trigger-wildcard1, events1
// #enddocregion events1
state('open', style({
height: '200px',
opacity: 1,
backgroundColor: 'yellow'
})),
// #enddocregion state1
// #docregion state2
state('closed', style({
height: '100px',
opacity: 0.8,
backgroundColor: 'blue'
})),
// #enddocregion state2, trigger-wildcard1
// #docregion transition1
transition('open => closed', [
animate('1s')
]),
// #enddocregion transition1
// #docregion transition2
transition('closed => open', [
animate('0.5s')
]),
// #enddocregion transition2, component
// #docregion trigger-wildcard1
transition('* => closed', [
animate('1s')
]),
transition('* => open', [
animate('0.5s')
]),
// #enddocregion trigger-wildcard1
// #docregion trigger-wildcard2
transition('open <=> closed', [
animate('0.5s')
]),
// #enddocregion trigger-wildcard2
// #docregion transition4
transition ('* => open', [
animate ('1s',
style ({ opacity: '*' }),
),
]),
// #enddocregion transition4
transition('* => *', [
animate('1s')
]),
// #enddocregion trigger-transition
// #docregion component, trigger-wildcard1, events1
]),
],
// #enddocregion trigger-wildcard1
// #enddocregion trigger-wildcard1
templateUrl: 'open-close.component.html',
styleUrls: ['open-close.component.css'],
styleUrls: ['open-close.component.css']
})
// #docregion events
export class OpenCloseComponent {
// #enddocregion events1, events, component
// #enddocregion events1, events, component
@Input() logging = false;
// #docregion component
// #docregion component
isOpen = true;
toggle() {
this.isOpen = !this.isOpen;
}
// #enddocregion component
// #docregion events1, events
// #enddocregion component
// #docregion events1, events
onAnimationEvent(event: AnimationEvent) {
// #enddocregion events1, events
// #enddocregion events1, events
if (!this.logging) {
return;
}
// #docregion events
// #docregion events
// openClose is trigger name in this example
console.warn(`Animation Trigger: ${event.triggerName}`);
@@ -92,8 +102,8 @@ export class OpenCloseComponent {
// the HTML element itself, the button in this case
console.warn(`Element: ${event.element}`);
// #docregion events1
// #docregion events1
}
// #docregion component
// #docregion component
}
// #enddocregion component
@@ -1,4 +1,6 @@
import {Component} from '@angular/core';
import {
Component,
} from '@angular/core';
import {
trigger,
style,
@@ -7,11 +9,11 @@ import {
group,
query,
animateChild,
keyframes,
keyframes
} from '@angular/animations';
import {HEROES} from './mock-heroes';
import {NgIf} from '@angular/common';
import { HEROES } from './mock-heroes';
import { NgIf } from '@angular/common';
@Component({
standalone: true,
@@ -39,50 +41,44 @@ import {NgIf} from '@angular/common';
animations: [
trigger('query', [
transition(':enter', [
style({height: 0}),
style({ height: 0 }),
group([
animate(500, style({height: '*'})),
animate(500, style({ height: '*' })),
query(':enter', [
style({opacity: 0, transform: 'scale(0)'}),
animate(2000, style({opacity: 1, transform: 'scale(1)'})),
style({ opacity: 0, transform: 'scale(0)'}),
animate(2000, style({ opacity: 1, transform: 'scale(1)' }))
]),
query('.hero', [
style({transform: 'translateX(-100%)'}),
animate('.7s 500ms ease-in', style({transform: 'translateX(0)'})),
style({ transform: 'translateX(-100%)'}),
animate('.7s 500ms ease-in', style({ transform: 'translateX(0)' }))
]),
]),
query('@animateMe', animateChild()),
]),
transition(':leave', [
style({height: '*'}),
style({ height: '*' }),
query('@animateMe', animateChild()),
group([
animate('500ms 500ms', style({height: '0', padding: '0'})),
animate('500ms 500ms', style({ height: '0', padding: '0' })),
query(':leave', [
style({opacity: 1, transform: 'scale(1)'}),
animate('1s', style({opacity: 0, transform: 'scale(0)'})),
style({ opacity: 1, transform: 'scale(1)'}),
animate('1s', style({ opacity: 0, transform: 'scale(0)' }))
]),
query('.hero', [
style({transform: 'translateX(0)'}),
animate('.7s ease-out', style({transform: 'translateX(-100%)'})),
style({ transform: 'translateX(0)'}),
animate('.7s ease-out', style({ transform: 'translateX(-100%)' }))
]),
]),
]),
]),
trigger('animateMe', [
transition(
'* <=> *',
animate(
'500ms cubic-bezier(.68,-0.73,.26,1.65)',
keyframes([
style({backgroundColor: 'transparent', color: '*', offset: 0}),
style({backgroundColor: 'blue', color: 'white', offset: 0.2}),
style({backgroundColor: 'transparent', color: '*', offset: 1}),
]),
),
),
transition('* <=> *', animate('500ms cubic-bezier(.68,-0.73,.26,1.65)', keyframes([
style({ backgroundColor: 'transparent', color: '*', offset: 0 }),
style({ backgroundColor: 'blue', color: 'white', offset: 0.2 }),
style({ backgroundColor: 'transparent', color: '*', offset: 1 })
])))
]),
],
]
})
export class QueryingComponent {
toggleDisabled = false;
@@ -1,5 +1,5 @@
import {Component} from '@angular/core';
import {StatusSliderComponent} from './status-slider.component';
import { Component } from '@angular/core';
import { StatusSliderComponent } from './status-slider.component';
@Component({
standalone: true,
@@ -10,6 +10,6 @@ import {StatusSliderComponent} from './status-slider.component';
<app-status-slider></app-status-slider>
</section>
`,
imports: [StatusSliderComponent],
imports: [StatusSliderComponent]
})
export class StatusSliderPageComponent {}
@@ -1,5 +1,5 @@
import {Component} from '@angular/core';
import {trigger, transition, state, animate, style, keyframes} from '@angular/animations';
import { Component } from '@angular/core';
import { trigger, transition, state, animate, style, keyframes } from '@angular/animations';
@Component({
standalone: true,
@@ -8,46 +8,37 @@ import {trigger, transition, state, animate, style, keyframes} from '@angular/an
styleUrls: ['status-slider.component.css'],
animations: [
trigger('slideStatus', [
state('inactive', style({backgroundColor: 'blue'})),
state('active', style({backgroundColor: '#754600'})),
state('inactive', style({ backgroundColor: 'blue' })),
state('active', style({ backgroundColor: '#754600' })),
// #docregion keyframesWithOffsets
// #docregion keyframesWithOffsets
transition('* => active', [
animate(
'2s',
keyframes([
style({backgroundColor: 'blue', offset: 0}),
style({backgroundColor: 'red', offset: 0.8}),
style({backgroundColor: '#754600', offset: 1.0}),
]),
),
animate('2s', keyframes([
style({ backgroundColor: 'blue', offset: 0}),
style({ backgroundColor: 'red', offset: 0.8}),
style({ backgroundColor: '#754600', offset: 1.0})
])),
]),
transition('* => inactive', [
animate(
'2s',
keyframes([
style({backgroundColor: '#754600', offset: 0}),
style({backgroundColor: 'red', offset: 0.2}),
style({backgroundColor: 'blue', offset: 1.0}),
]),
),
animate('2s', keyframes([
style({ backgroundColor: '#754600', offset: 0}),
style({ backgroundColor: 'red', offset: 0.2}),
style({ backgroundColor: 'blue', offset: 1.0})
]))
]),
// #enddocregion keyframesWithOffsets
// #enddocregion keyframesWithOffsets
// #docregion keyframes
// #docregion keyframes
transition('* => active', [
animate(
'2s',
keyframes([
style({backgroundColor: 'blue'}),
style({backgroundColor: 'red'}),
style({backgroundColor: 'orange'}),
]),
),
// #enddocregion keyframes
animate('2s', keyframes([
style({ backgroundColor: 'blue' }),
style({ backgroundColor: 'red' }),
style({ backgroundColor: 'orange' })
]))
// #enddocregion keyframes
]),
]),
],
])
]
})
export class StatusSliderComponent {
status: 'active' | 'inactive' = 'inactive';
@@ -1,5 +1,5 @@
import {Component} from '@angular/core';
import {OpenCloseChildComponent} from './open-close.component.4';
import { Component } from '@angular/core';
import { OpenCloseChildComponent } from './open-close.component.4';
@Component({
standalone: true,
@@ -11,6 +11,6 @@ import {OpenCloseChildComponent} from './open-close.component.4';
<app-open-close-toggle></app-open-close-toggle>
</section>
`,
imports: [OpenCloseChildComponent],
imports: [OpenCloseChildComponent]
})
export class ToggleAnimationsPageComponent {}
@@ -1,5 +1,5 @@
import {bootstrapApplication} from '@angular/platform-browser';
import {AppComponent} from './app/app.component';
import {appConfig} from './app/app.config';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig);
@@ -1,4 +1,4 @@
import {protractor, browser, element, by, ElementFinder} from 'protractor';
import { protractor, browser, element, by, ElementFinder } from 'protractor';
const nameSuffix = 'X';
@@ -8,6 +8,7 @@ interface Hero {
}
describe('Architecture', () => {
const expectedTitle = 'Architecture of Angular';
const expectedH2 = ['Hero List', 'Sales Tax Calculator'];
@@ -27,7 +28,8 @@ describe('Architecture', () => {
});
function heroTests() {
const targetHero: Hero = {id: 2, name: 'Dr Nice'};
const targetHero: Hero = { id: 2, name: 'Dr Nice' };
it('has the right number of heroes', async () => {
const page = getPageElts();
@@ -78,7 +80,7 @@ function getPageElts() {
heroes: element.all(by.css('app-root li')),
heroDetail: element(by.css('app-root app-hero-detail')),
salesTaxAmountInput: element(by.css('app-root app-sales-tax input')),
salesTaxDetail: element(by.css('app-root app-sales-tax div')),
salesTaxDetail: element(by.css('app-root app-sales-tax div'))
};
}
@@ -1,5 +1,5 @@
// #docregion import
import {Component} from '@angular/core';
import { Component } from '@angular/core';
// #enddocregion import
@Component({
@@ -8,6 +8,6 @@ import {Component} from '@angular/core';
<h1>Architecture Example</h1>
<app-hero-list></app-hero-list>
<app-sales-tax></app-sales-tax>
`,
`
})
export class AppComponent {}
export class AppComponent { }
@@ -1,22 +1,34 @@
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
// #docregion imports
import {NgModule} from '@angular/core';
import {AppComponent} from './app.component';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
// #enddocregion imports
import {HeroDetailComponent} from './hero-detail.component';
import {HeroListComponent} from './hero-list.component';
import {SalesTaxComponent} from './sales-tax.component';
import {HeroService} from './hero.service';
import {BackendService} from './backend.service';
import {Logger} from './logger.service';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroListComponent } from './hero-list.component';
import { SalesTaxComponent } from './sales-tax.component';
import { HeroService } from './hero.service';
import { BackendService } from './backend.service';
import { Logger } from './logger.service';
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: [AppComponent, HeroDetailComponent, HeroListComponent, SalesTaxComponent],
providers: [BackendService, HeroService, Logger],
bootstrap: [AppComponent],
imports: [
BrowserModule,
FormsModule
],
declarations: [
AppComponent,
HeroDetailComponent,
HeroListComponent,
SalesTaxComponent
],
providers: [
BackendService,
HeroService,
Logger
],
bootstrap: [ AppComponent ]
})
// #docregion export
export class AppModule {}
export class AppModule { }
// #enddocregion export
@@ -1,13 +1,13 @@
import {Injectable, Type} from '@angular/core';
import { Injectable, Type } from '@angular/core';
import {Logger} from './logger.service';
import {Hero} from './hero';
import { Logger } from './logger.service';
import { Hero } from './hero';
const HEROES = [
new Hero('Windstorm', 'Weather mastery'),
new Hero('Dr Nice', 'Killing them with kindness'),
new Hero('Magneta', 'Manipulates metallic objects'),
];
new Hero('Windstorm', 'Weather mastery'),
new Hero('Dr Nice', 'Killing them with kindness'),
new Hero('Magneta', 'Manipulates metallic objects')
];
@Injectable()
export class BackendService {
@@ -1,10 +1,10 @@
import {Component, Input} from '@angular/core';
import { Component, Input } from '@angular/core';
import {Hero} from './hero';
import { Hero } from './hero';
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
templateUrl: './hero-detail.component.html'
})
export class HeroDetailComponent {
@Input() hero!: Hero;
@@ -1,13 +1,13 @@
import {Component, OnInit} from '@angular/core';
import { Component, OnInit } from '@angular/core';
import {Hero} from './hero';
import {HeroService} from './hero.service';
import { Hero } from './hero';
import { HeroService } from './hero.service';
// #docregion metadata, providers
@Component({
selector: 'app-hero-list',
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
providers: [HeroService],
providers: [ HeroService ]
})
// #enddocregion providers
// #docregion class
@@ -17,15 +17,13 @@ export class HeroListComponent implements OnInit {
selectedHero: Hero | undefined;
// #docregion ctor
constructor(private service: HeroService) {}
constructor(private service: HeroService) { }
// #enddocregion ctor
ngOnInit() {
this.heroes = this.service.getHeroes();
}
selectHero(hero: Hero) {
this.selectedHero = hero;
}
selectHero(hero: Hero) { this.selectedHero = hero; }
// #docregion metadata
}

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