Compare commits
23 Commits
11.0.0-rc.
...
zone.js-0.
Author | SHA1 | Date | |
---|---|---|---|
a38293d213 | |||
4dc8d83d84 | |||
492e236ae6 | |||
34b74cecb6 | |||
81aa119739 | |||
49197d12a0 | |||
9ec2bad4dc | |||
e8d47c2d41 | |||
765fa337e3 | |||
6570292672 | |||
4610093c87 | |||
a3812c6cbd | |||
96f59f64a0 | |||
5bda62c51d | |||
f9f3c54c9a | |||
7669827436 | |||
178a0437b7 | |||
34dbba4a90 | |||
ef608bff83 | |||
06d0d8e31d | |||
60f761b4c2 | |||
93437bbc16 | |||
85be258845 |
@ -186,6 +186,7 @@ groups:
|
||||
- IgorMinar # Igor Minar
|
||||
- jbogarthyde # Judy Bogart
|
||||
- jelbourn # Jeremy Elbourn
|
||||
- jessicajaniuk # Jessica Janiuk
|
||||
- JiaLiPassion # Jia Li
|
||||
- JoostK # Joost Koehoorn
|
||||
- josephperrott # Joey Perrott
|
||||
@ -405,6 +406,7 @@ groups:
|
||||
'aio/content/guide/structural-directives.md',
|
||||
'aio/content/examples/structural-directives/**',
|
||||
'aio/content/guide/svg-in-templates.md',
|
||||
'aio/content/guide/style-precedence.md',
|
||||
'aio/content/images/guide/structural-directives/**',
|
||||
'aio/content/guide/template-statements.md',
|
||||
'aio/content/guide/user-input.md',
|
||||
@ -1120,6 +1122,7 @@ groups:
|
||||
'docs/SAVED_REPLIES.md',
|
||||
'docs/TOOLS.md',
|
||||
'docs/TRIAGE_AND_LABELS.md',
|
||||
'docs/images/**',
|
||||
'goldens/*',
|
||||
'modules/*',
|
||||
'packages/*',
|
||||
|
146
README.md
146
README.md
@ -1,27 +1,151 @@
|
||||
[](https://circleci.com/gh/angular/workflows/angular/tree/master)
|
||||
[](https://discord.gg/angular)
|
||||
[](https://gitter.im/angular/angular?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://www.npmjs.com/@angular/core)
|
||||
<h1 align="center">Angular - One framework. Mobile & desktop.</h1>
|
||||
|
||||
<p align="center">
|
||||
<img src="aio/src/assets/images/logos/angular/angular.png" alt="angular-logo" width="120px" height="120px"/>
|
||||
<br>
|
||||
<i>Angular is a development platform for building mobile and desktop web applications
|
||||
<br> using Typescript/JavaScript and other languages.</i>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
# Angular
|
||||
<p align="center">
|
||||
<a href="https://www.angular.io"><strong>www.angular.io</strong></a>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
Angular is a development platform for building mobile and desktop web applications using TypeScript/JavaScript and other languages.
|
||||
<p align="center">
|
||||
<a href="CONTRIBUTING.md">Contributing Guidelines</a>
|
||||
·
|
||||
<a href="https://github.com/angular/angular/issues">Submit an Issue</a>
|
||||
·
|
||||
<a href="https://blog.angular.io/">Blog</a>
|
||||
<br>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://circleci.com/gh/angular/workflows/angular/tree/master">
|
||||
<img src="https://img.shields.io/circleci/build/github/angular/angular/master.svg?logo=circleci&logoColor=fff&label=CircleCI" alt="CI status" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/@angular/core">
|
||||
<img src="https://img.shields.io/npm/v/@angular/core.svg?logo=npm&logoColor=fff&label=NPM+package&color=limegreen" alt="Angular on npm" />
|
||||
</a>
|
||||
<a href="https://discord.gg/angular">
|
||||
<img src="https://img.shields.io/discord/463752820026376202.svg?logo=discord&logoColor=fff&label=Discord&color=7389d8" alt="Discord conversation" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
## Documentation
|
||||
|
||||
Get started with Angular, learn the fundamentals and explore advanced topics on our documentation website.
|
||||
|
||||
- [Getting Started][quickstart]
|
||||
- [Architecture][architecture]
|
||||
- [Components and Templates][componentstemplates]
|
||||
- [Forms][forms]
|
||||
- [API][api]
|
||||
|
||||
### Advanced
|
||||
|
||||
- [Angular Elements][angularelements]
|
||||
- [Server Side Rendering][ssr]
|
||||
- [Schematics][schematics]
|
||||
- [Lazy Loading][lazyloading]
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Install [Node.js] which includes [Node Package Manager][npm]
|
||||
|
||||
### Setting Up a Project
|
||||
|
||||
Intall the Angular CLI globally:
|
||||
|
||||
```
|
||||
npm install -g @angular/cli
|
||||
```
|
||||
|
||||
Create workspace:
|
||||
|
||||
```
|
||||
ng new [PROJECT NAME]
|
||||
```
|
||||
|
||||
Run the application:
|
||||
|
||||
```
|
||||
cd [PROJECT NAME]
|
||||
ng serve
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
[Get started in 5 minutes][quickstart].
|
||||
|
||||
## Ecosystem
|
||||
|
||||
<p>
|
||||
<img src="/docs/images/angular-ecosystem-logos.png" alt="angular ecosystem logos" width="500px" height="auto">
|
||||
</p>
|
||||
|
||||
- [Angular Command Line (CLI)][cli]
|
||||
- [Angular Material][angularmaterial]
|
||||
|
||||
## Changelog
|
||||
|
||||
[Learn about the latest improvements][changelog].
|
||||
|
||||
## Want to help?
|
||||
## Upgrading
|
||||
|
||||
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our
|
||||
guidelines for [contributing][contributing] and then check out one of our issues in the [hotlist: community-help](https://github.com/angular/angular/labels/hotlist%3A%20community-help).
|
||||
Check out our [upgrade guide](https://update.angular.io/) to find out the best way to upgrade your project.
|
||||
|
||||
[contributing]: https://github.com/angular/angular/blob/master/CONTRIBUTING.md
|
||||
## Contributing
|
||||
|
||||
### Contributing Guidelines
|
||||
|
||||
Read through our [contributing guidelines][contributing] to learn about our submission process, coding rules and more.
|
||||
|
||||
### Want to Help?
|
||||
|
||||
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our guidelines for [contributing][contributing] and then check out one of our issues in the [hotlist: community-help](https://github.com/angular/angular/labels/hotlist%3A%20community-help).
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
Help us keep Angular open and inclusive. Please read and follow our [Code of Conduct][codeofconduct].
|
||||
|
||||
## Community
|
||||
|
||||
Join the conversation and help the community.
|
||||
|
||||
- [Twitter][twitter]
|
||||
- [Gitter][gitter]
|
||||
- Find a Local [Meetup][meetup]
|
||||
|
||||
[](https://www.github.com/angular/angular)
|
||||
|
||||
**Love Angular? Give our repo a star :star: :arrow_up:.**
|
||||
|
||||
[contributing]: CONTRIBUTING.md
|
||||
[quickstart]: https://angular.io/start
|
||||
[changelog]: https://github.com/angular/angular/blob/master/CHANGELOG.md
|
||||
[changelog]: CHANGELOG.md
|
||||
[ng]: https://angular.io
|
||||
[documentation]: https://angular.io/docs
|
||||
[angularmaterial]: https://material.angular.io/
|
||||
[cli]: https://cli.angular.io/
|
||||
[architecture]: https://angular.io/guide/architecture
|
||||
[componentstemplates]: https://angular.io/guide/displaying-data
|
||||
[forms]: https://angular.io/guide/forms-overview
|
||||
[api]: https://angular.io/api
|
||||
[angularelements]: https://angular.io/guide/elements
|
||||
[ssr]: https://angular.io/guide/universal
|
||||
[schematics]: https://angular.io/guide/schematics
|
||||
[lazyloading]: https://angular.io/guide/lazy-loading-ngmodules
|
||||
[node.js]: https://nodejs.org/
|
||||
[npm]: https://www.npmjs.com/get-npm
|
||||
[codeofconduct]: CODE_OF_CONDUCT.md
|
||||
[twitter]: https://www.twitter.com/angular
|
||||
[gitter]: https://gitter.im/angular/angular
|
||||
[meetup]: https://www.meetup.com/find/?keywords=angular"
|
||||
|
@ -36,8 +36,9 @@ Here are the most important tasks you might need to use:
|
||||
|
||||
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally.
|
||||
* `yarn boilerplate:add:viewengine` - same as `boilerplate:add` but also turns on `ViewEngine` (pre-Ivy) mode.
|
||||
|
||||
* `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
|
||||
* `yarn create-example` - create a new example directory containing initial source files.
|
||||
|
||||
* `yarn generate-stackblitz` - generate the stackblitz files that are used by the `live-example` tags in the docs.
|
||||
* `yarn generate-zips` - generate the zip files from the examples. Zip available via the `live-example` tags in the docs.
|
||||
|
||||
|
2
aio/content/examples/.gitignore
vendored
2
aio/content/examples/.gitignore
vendored
@ -32,6 +32,8 @@
|
||||
**/karma-test-shim.js
|
||||
**/browser-test-shim.js
|
||||
**/node_modules
|
||||
**/yarn.lock
|
||||
**/package-lock.json
|
||||
|
||||
# built files
|
||||
*.map
|
||||
|
@ -3,8 +3,10 @@
|
||||
<h2>Attribute binding</h2>
|
||||
<!-- #docregion attrib-binding-colspan -->
|
||||
<table border=1>
|
||||
<!-- #docregion colspan -->
|
||||
<!-- expression calculates colspan=2 -->
|
||||
<tr><td [attr.colspan]="1 + 1">One-Two</td></tr>
|
||||
<!-- #enddocregion colspan -->
|
||||
|
||||
<!-- ERROR: There is no `colspan` property to set!
|
||||
<tr><td colspan="{{1 + 1}}">Three-Four</td></tr>
|
||||
@ -32,31 +34,31 @@
|
||||
<!-- #docregion basic-specificity -->
|
||||
<h3>Basic specificity</h3>
|
||||
|
||||
<!-- The `class.special` binding will override any value for the `special` class in `classExpr`. -->
|
||||
<div [class.special]="isSpecial" [class]="classExpr">Some text.</div>
|
||||
<!-- The `class.special` binding overrides any value for the `special` class in `classExpression`. -->
|
||||
<div [class.special]="isSpecial" [class]="classExpression">Some text.</div>
|
||||
|
||||
<!-- The `style.color` binding will override any value for the `color` property in `styleExpr`. -->
|
||||
<div [style.color]="color" [style]="styleExpr">Some text.</div>
|
||||
<!-- The `style.color` binding overrides any value for the `color` property in `styleExpression`. -->
|
||||
<div [style.color]="color" [style]="styleExpression">Some text.</div>
|
||||
<!-- #enddocregion basic-specificity -->
|
||||
|
||||
<!-- #docregion source-specificity -->
|
||||
<h3>Source specificity</h3>
|
||||
|
||||
<!-- The `class.special` template binding will override any host binding to the `special` class set by `dirWithClassBinding` or `comp-with-host-binding`.-->
|
||||
<!-- The `class.special` template binding overrides any host binding to the `special` class set by `dirWithClassBinding` or `comp-with-host-binding`.-->
|
||||
<comp-with-host-binding [class.special]="isSpecial" dirWithClassBinding>Some text.</comp-with-host-binding>
|
||||
|
||||
<!-- The `style.color` template binding will override any host binding to the `color` property set by `dirWithStyleBinding` or `comp-with-host-binding`. -->
|
||||
<!-- The `style.color` template binding overrides any host binding to the `color` property set by `dirWithStyleBinding` or `comp-with-host-binding`. -->
|
||||
<comp-with-host-binding [style.color]="color" dirWithStyleBinding>Some text.</comp-with-host-binding>
|
||||
<!-- #enddocregion source-specificity -->
|
||||
|
||||
<!-- #docregion dynamic-priority -->
|
||||
<h3>Dynamic vs static</h3>
|
||||
|
||||
<!-- If `classExpr` has a value for the `special` class, this value will override the `class="special"` below -->
|
||||
<div class="special" [class]="classExpr">Some text.</div>
|
||||
<!-- If `classExpression` has a value for the `special` class, this value overrides the `class="special"` below -->
|
||||
<div class="special" [class]="classExpression">Some text.</div>
|
||||
|
||||
<!-- If `styleExpr` has a value for the `color` property, this value will override the `style="color: blue"` below -->
|
||||
<div style="color: blue" [style]="styleExpr">Some text.</div>
|
||||
<!-- If `styleExpression` has a value for the `color` property, this value overrides the `style="color: blue"` below -->
|
||||
<div style="color: blue" [style]="styleExpression">Some text.</div>
|
||||
|
||||
<!-- #enddocregion dynamic-priority -->
|
||||
|
||||
|
@ -9,7 +9,7 @@ export class AppComponent {
|
||||
actionName = 'Go for it';
|
||||
isSpecial = true;
|
||||
canSave = true;
|
||||
classExpr = 'special clearance';
|
||||
styleExpr = 'color: red';
|
||||
classExpression = 'special clearance';
|
||||
styleExpression = 'color: red';
|
||||
color = 'blue';
|
||||
}
|
||||
|
@ -11,6 +11,9 @@ export class CompWithHostBindingComponent {
|
||||
@HostBinding('style.color')
|
||||
color = 'green';
|
||||
|
||||
// #docregion hostbinding
|
||||
@HostBinding('style.width')
|
||||
width = '200px';
|
||||
// #enddocregion hostbinding
|
||||
|
||||
}
|
||||
|
@ -23,13 +23,13 @@ describe('retry-on-error', () => {
|
||||
docRegionDefault(mockConsole, ajax);
|
||||
expect(mockConsole.log.calls.allArgs()).toEqual([
|
||||
['Subscribed to AJAX'],
|
||||
['Error occured.'],
|
||||
['Error occurred.'],
|
||||
['Subscribed to AJAX'],
|
||||
['Error occured.'],
|
||||
['Error occurred.'],
|
||||
['Subscribed to AJAX'],
|
||||
['Error occured.'],
|
||||
['Error occurred.'],
|
||||
['Subscribed to AJAX'],
|
||||
['Error occured.'],
|
||||
['Error occurred.'],
|
||||
['data: ', []],
|
||||
]);
|
||||
});
|
||||
|
@ -17,7 +17,7 @@ export function docRegionDefault(console, ajax) {
|
||||
const apiData = ajax('/api/data').pipe(
|
||||
map((res: any) => {
|
||||
if (!res.response) {
|
||||
console.log('Error occured.');
|
||||
console.log('Error occurred.');
|
||||
throw new Error('Value expected!');
|
||||
}
|
||||
return res.response;
|
||||
|
@ -89,7 +89,7 @@ This example from the `HeroListComponent` template uses three of these forms.
|
||||
|
||||
<code-example path="architecture/src/app/hero-list.component.1.html" header="src/app/hero-list.component.html (binding)" region="binding"></code-example>
|
||||
|
||||
* The `{{hero.name}}` [*interpolation*](guide/displaying-data#interpolation)
|
||||
* The `{{hero.name}}` [*interpolation*](guide/interpolation)
|
||||
displays the component's `hero.name` property value within the `<li>` element.
|
||||
|
||||
* The `[hero]` [*property binding*](guide/property-binding) passes the value of
|
||||
@ -166,8 +166,8 @@ The example template uses two built-in structural directives to add application
|
||||
|
||||
<code-example path="architecture/src/app/hero-list.component.1.html" header="src/app/hero-list.component.html (structural)" region="structural"></code-example>
|
||||
|
||||
* [`*ngFor`](guide/displaying-data#ngFor) is an iterative; it tells Angular to stamp out one `<li>` per hero in the `heroes` list.
|
||||
* [`*ngIf`](guide/displaying-data#ngIf) is a conditional; it includes the `HeroDetail` component only if a selected hero exists.
|
||||
* [`*ngFor`](guide/structural-directives#inside-ngfor) is an iterative; it tells Angular to stamp out one `<li>` per hero in the `heroes` list.
|
||||
* [`*ngIf`](guide/structural-directives#ngif-case-study) is a conditional; it includes the `HeroDetail` component only if a selected hero exists.
|
||||
|
||||
#### Attribute directives
|
||||
|
||||
|
@ -11,7 +11,7 @@ about the features and tools that can help you develop and deliver Angular appli
|
||||
|
||||
## Application architecture
|
||||
|
||||
* The [Components and templates](guide/displaying-data) guide explains how to connect the application data in your [components](guide/glossary#component) to your page-display [templates](guide/glossary#template), to create a complete interactive application.
|
||||
* The **Main Concepts** section located in the table of contents contains several topics that explain how to connect the application data in your [components](guide/glossary#component) to your page-display [templates](guide/glossary#template), to create a complete interactive application.
|
||||
|
||||
* The [NgModules](guide/ngmodules) guide provides in-depth information on the modular structure of an Angular application.
|
||||
|
||||
@ -21,7 +21,7 @@ about the features and tools that can help you develop and deliver Angular appli
|
||||
|
||||
## Responsive programming
|
||||
|
||||
The **Components and Templates** guide provides guidance and details of the [template syntax](guide/template-syntax) that you use to display your component data when and where you want it within a view, and to collect input from users that you can respond to.
|
||||
The [template syntax](guide/template-syntax) and related topics contain details about how to display your component data when and where you want it within a view, and how to collect input from users that you can respond to.
|
||||
|
||||
Additional pages and sections describe some basic programming techniques for Angular apps.
|
||||
|
||||
@ -52,8 +52,6 @@ For some platforms and applications, you might also want to use the PWA (Progres
|
||||
|
||||
## Support for the development cycle
|
||||
|
||||
The **Development Workflow** section describes the tools and processes you use to compile, test, and deploy Angular applications.
|
||||
|
||||
* [CLI Command Reference](cli): The Angular CLI is a command-line tool that you use to create projects, generate application and library code, and perform a variety of ongoing development tasks such as testing, bundling, and deployment.
|
||||
|
||||
* [Compilation](guide/aot-compiler): Angular provides just-in-time (JIT) compilation for the development environment, and ahead-of-time (AOT) compilation for the production environment.
|
||||
@ -68,7 +66,6 @@ The **Development Workflow** section describes the tools and processes you use t
|
||||
|
||||
* [Accessibility](guide/accessibility): Make your app accessible to all users.
|
||||
|
||||
|
||||
## File structure, configuration, and dependencies
|
||||
|
||||
* [Workspace and file structure](guide/file-structure): Understand the structure of Angular workspace and project folders.
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Attribute, class, and style bindings
|
||||
|
||||
The template syntax provides specialized one-way bindings for scenarios less well-suited to property binding.
|
||||
Attribute binding in Angular helps you set values for attributes directly.
|
||||
With attribute binding, you can improve accessibility, style your application dynamically, and manage multiple CSS classes or styles simultaneously.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
@ -8,23 +9,36 @@ See the <live-example></live-example> for a working example containing the code
|
||||
|
||||
</div>
|
||||
|
||||
## Binding to an attribute
|
||||
|
||||
## Attribute binding
|
||||
It is recommended that you set an element property with a [property binding](guide/property-binding) whenever possible.
|
||||
However, sometimes you don't have an element property to bind.
|
||||
In those situations, you can use attribute binding.
|
||||
|
||||
Set the value of an attribute directly with an **attribute binding**. This is the only exception to the rule that a binding sets a target property and the only binding that creates and sets an attribute.
|
||||
For example, [ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) and
|
||||
[SVG](https://developer.mozilla.org/en-US/docs/Web/SVG) are purely attributes.
|
||||
Neither ARIA nor SVG correspond to element properties and don't set element properties.
|
||||
In these cases, you must use attribute binding because there are no corresponding property targets.
|
||||
|
||||
Usually, setting an element property with a [property binding](guide/property-binding)
|
||||
is preferable to setting the attribute with a string. However, sometimes
|
||||
there is no element property to bind, so attribute binding is the solution.
|
||||
|
||||
Consider the [ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) and
|
||||
[SVG](https://developer.mozilla.org/en-US/docs/Web/SVG). They are purely attributes, don't correspond to element properties, and don't set element properties. In these cases, there are no property targets to bind to.
|
||||
## Syntax
|
||||
|
||||
Attribute binding syntax resembles property binding, but
|
||||
instead of an element property between brackets, start with the prefix `attr`,
|
||||
followed by a dot (`.`), and the name of the attribute.
|
||||
You then set the attribute value, using an expression that resolves to a string,
|
||||
or remove the attribute when the expression resolves to `null`.
|
||||
Attribute binding syntax resembles [property binding](guide/property-binding), but instead of an element property between brackets, you precede the name of the attribute with the prefix `attr`, followed by a dot.
|
||||
Then, you set the attribute value with an expression that resolves to a string.
|
||||
|
||||
<code-example language="html">
|
||||
|
||||
<p [attr.attribute-you-are-targeting]="expression"></p>
|
||||
|
||||
</code-example>
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
When the expression resolves to `null`, Angular removes the attribute altogether.
|
||||
|
||||
</div>
|
||||
|
||||
## Binding ARIA attributes
|
||||
|
||||
One of the primary use cases for attribute binding
|
||||
is to set ARIA attributes, as in this example:
|
||||
@ -33,32 +47,30 @@ is to set ARIA attributes, as in this example:
|
||||
|
||||
{@a colspan}
|
||||
|
||||
## Binding to `colspan`
|
||||
|
||||
Another common use case for attribute binding is with the `colspan` attribute in tables.
|
||||
Binding to the `colspan` attribute helps you keep your tables programmatically dynamic.
|
||||
Depending on the amount of data that your application populates a table with, the number of columns that a row spans could change.
|
||||
|
||||
To use attribute binding with the `<td>` attribute `colspan`:
|
||||
|
||||
1. Specify the `colspan` attribute by using the following syntax: `[attr.colspan]`.
|
||||
1. Set `[attr.colspan]` equal to an expression.
|
||||
|
||||
In the following example, binds the `colspan` attribute to the expression `1 + 1`.
|
||||
|
||||
<code-example path="attribute-binding/src/app/app.component.html" region="colspan" header="src/app/app.component.html"></code-example>
|
||||
|
||||
This binding causes the `<tr>` to span two columns.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
#### `colspan` and `colSpan`
|
||||
Sometimes there are differences between the name of property and an attribute.
|
||||
|
||||
Notice the difference between the `colspan` attribute and the `colSpan` property.
|
||||
|
||||
If you wrote something like this:
|
||||
|
||||
<code-example language="html">
|
||||
<tr><td colspan="{{1 + 1}}">Three-Four</td></tr>
|
||||
</code-example>
|
||||
|
||||
You'd get this error:
|
||||
|
||||
<code-example language="bash">
|
||||
Template parse errors:
|
||||
Can't bind to 'colspan' since it isn't a known native property
|
||||
</code-example>
|
||||
|
||||
As the message says, the `<td>` element does not have a `colspan` property. This is true
|
||||
because `colspan` is an attribute—`colSpan`, with a capital `S`, is the
|
||||
corresponding property. Interpolation and property binding can set only *properties*, not attributes.
|
||||
|
||||
Instead, you'd use property binding and write it like this:
|
||||
|
||||
<code-example path="attribute-binding/src/app/app.component.html" region="colSpan" header="src/app/app.component.html"></code-example>
|
||||
`colspan` is an attribute of `<tr>`, while `colSpan` with a capital "S" is a property.
|
||||
When using attribute binding, use `colspan` with a lowercase "s".
|
||||
For more information on how to bind to the `colSpan` property, see the [`colspan` and `colSpan`](guide/property-binding#colspan) section of [Property Binding](guide/property-binding).
|
||||
|
||||
</div>
|
||||
|
||||
@ -66,28 +78,32 @@ Instead, you'd use property binding and write it like this:
|
||||
|
||||
{@a class-binding}
|
||||
|
||||
## Class binding
|
||||
## Binding to the `class` attribute
|
||||
|
||||
Here's how to set the `class` attribute without a binding in plain HTML:
|
||||
You can use class binding to add and remove CSS class names from an element's `class` attribute.
|
||||
|
||||
```html
|
||||
<!-- standard class attribute setting -->
|
||||
<div class="foo bar">Some text</div>
|
||||
```
|
||||
### Binding to a single CSS `class`
|
||||
|
||||
You can also add and remove CSS class names from an element's `class` attribute with a **class binding**.
|
||||
To create a single class binding, use the prefix `class` followed by a dot and the name of the CSS class—for example, `[class.sale]="onSale"`.
|
||||
Angular adds the class when the bound expression, `onSale` is truthy, and it removes the class when the expression is falsy—with the exception of `undefined`.
|
||||
See [styling delegation](guide/style-precedence#styling-delegation) for more information.
|
||||
|
||||
To create a single class binding, start with the prefix `class` followed by a dot (`.`) and the name of the CSS class (for example, `[class.foo]="hasFoo"`).
|
||||
Angular adds the class when the bound expression is truthy, and it removes the class when the expression is falsy (with the exception of `undefined`, see [styling delegation](#styling-delegation)).
|
||||
### Binding to multiple CSS classes
|
||||
|
||||
To create a binding to multiple classes, use a generic `[class]` binding without the dot (for example, `[class]="classExpr"`).
|
||||
The expression can be a space-delimited string of class names, or you can format it as an object with class names as the keys and truthy/falsy expressions as the values.
|
||||
With object format, Angular will add a class only if its associated value is truthy.
|
||||
To bind to multiple classes, use `[class]` set to an expression—for example, `[class]="classExpression"`.
|
||||
The expression can be a space-delimited string of class names, or an object with class names as the keys and truthy or falsy expressions as the values.
|
||||
With an object format, Angular adds a class only if its associated value is truthy.
|
||||
|
||||
It's important to note that with any object-like expression (`object`, `Array`, `Map`, `Set`, etc), the identity of the object must change for the class list to be updated.
|
||||
Updating the property without changing object identity will have no effect.
|
||||
<div class="alert is-important">
|
||||
|
||||
If there are multiple bindings to the same class name, conflicts are resolved using [styling precedence](#styling-precedence).
|
||||
With any object-like expression—such as `object`, `Array`, `Map`, or `Set`—the identity of the object must change for Angular to update the class list.
|
||||
Updating the property without changing object identity has no effect.
|
||||
|
||||
</div>
|
||||
|
||||
If there are multiple bindings to the same class name, Angular uses [styling precedence](guide/style-precedence) to determine which binding to use.
|
||||
|
||||
The following table summarizes class binding syntax.
|
||||
|
||||
<style>
|
||||
td, th {vertical-align: top}
|
||||
@ -118,13 +134,13 @@ If there are multiple bindings to the same class name, conflicts are resolved us
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Single class binding</td>
|
||||
<td><code>[class.foo]="hasFoo"</code></td>
|
||||
<td><code>[class.sale]="onSale"</code></td>
|
||||
<td><code>boolean | undefined | null</code></td>
|
||||
<td><code>true</code>, <code>false</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan=3>Multi-class binding</td>
|
||||
<td rowspan=3><code>[class]="classExpr"</code></td>
|
||||
<td rowspan=3><code>[class]="classExpression"</code></td>
|
||||
<td><code>string</code></td>
|
||||
<td><code>"my-class-1 my-class-2 my-class-3"</code></td>
|
||||
</tr>
|
||||
@ -138,44 +154,44 @@ If there are multiple bindings to the same class name, conflicts are resolved us
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
The [NgClass](guide/built-in-directives/#ngclass) directive can be used as an alternative to direct `[class]` bindings.
|
||||
However, using the above class binding syntax without `NgClass` is preferred because due to improvements in class binding in Angular, `NgClass` no longer provides significant value, and might eventually be removed in the future.
|
||||
|
||||
|
||||
<hr/>
|
||||
|
||||
## Style binding
|
||||
{@a style-binding}
|
||||
|
||||
Here's how to set the `style` attribute without a binding in plain HTML:
|
||||
## Binding to the style attribute
|
||||
|
||||
```html
|
||||
<!-- standard style attribute setting -->
|
||||
<div style="color: blue">Some text</div>
|
||||
```
|
||||
You can use style binding to set styles dynamically.
|
||||
|
||||
You can also set styles dynamically with a **style binding**.
|
||||
### Binding to a single style
|
||||
|
||||
To create a single style binding, start with the prefix `style` followed by a dot (`.`) and the name of the CSS style property (for example, `[style.width]="width"`).
|
||||
The property will be set to the value of the bound expression, which is normally a string.
|
||||
To create a single style binding, use the prefix `style` followed by a dot and the name of the CSS style property—for example, `[style.width]="width"`.
|
||||
Angular sets the property to the value of the bound expression, which is usually a string.
|
||||
Optionally, you can add a unit extension like `em` or `%`, which requires a number type.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Note that a _style property_ name can be written in either
|
||||
[dash-case](guide/glossary#dash-case), as shown above, or
|
||||
[camelCase](guide/glossary#camelcase), such as `fontSize`.
|
||||
You can write a style property name in either [dash-case](guide/glossary#dash-case), or
|
||||
[camelCase](guide/glossary#camelcase).
|
||||
|
||||
</div>
|
||||
|
||||
If there are multiple styles you'd like to toggle, you can bind to the `[style]` property directly without the dot (for example, `[style]="styleExpr"`).
|
||||
The expression attached to the `[style]` binding is most often a string list of styles like `"width: 100px; height: 100px;"`.
|
||||
### Binding to multiple styles
|
||||
|
||||
You can also format the expression as an object with style names as the keys and style values as the values, like `{width: '100px', height: '100px'}`.
|
||||
It's important to note that with any object-like expression (`object`, `Array`, `Map`, `Set`, etc), the identity of the object must change for the class list to be updated.
|
||||
Updating the property without changing object identity will have no effect.
|
||||
To toggle multiple styles, bind to the `[style]` attribute—for example, `[style]="styleExpression"`).
|
||||
The expression is often a string list of styles such as `"width: 100px; height: 100px;"`.
|
||||
|
||||
If there are multiple bindings to the same style property, conflicts are resolved using [styling precedence rules](#styling-precedence).
|
||||
You can also format the expression as an object with style names as the keys and style values as the values, such as `{width: '100px', height: '100px'}`.
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
With any object-like expression—such as `object`, `Array`, `Map`, or `Set`—the identity of the object must change for Angular to update the class list.
|
||||
Updating the property without changing object identity has no effect.
|
||||
|
||||
</div>
|
||||
|
||||
If there are multiple bindings to the same style attribute, Angular uses [styling precedence](guide/style-precedence) to determine which binding to use.
|
||||
|
||||
The following table summarizes style binding syntax.
|
||||
|
||||
<style>
|
||||
td, th {vertical-align: top}
|
||||
@ -219,7 +235,7 @@ If there are multiple bindings to the same style property, conflicts are resolve
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan=3>Multi-style binding</td>
|
||||
<td rowspan=3><code>[style]="styleExpr"</code></td>
|
||||
<td rowspan=3><code>[style]="styleExpression"</code></td>
|
||||
<td><code>string</code></td>
|
||||
<td><code>"width: 100px; height: 100px"</code></td>
|
||||
</tr>
|
||||
@ -232,72 +248,3 @@ If there are multiple bindings to the same style property, conflicts are resolve
|
||||
<td><code>['width', '100px']</code></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
The [NgStyle](guide/built-in-directives/#ngstyle) directive can be used as an alternative to direct `[style]` bindings.
|
||||
However, using the above style binding syntax without `NgStyle` is preferred because due to improvements in style binding in Angular, `NgStyle` no longer provides significant value, and might eventually be removed in the future.
|
||||
|
||||
|
||||
<hr/>
|
||||
|
||||
{@a styling-precedence}
|
||||
|
||||
## Styling Precedence
|
||||
|
||||
A single HTML element can have its CSS class list and style values bound to multiple sources (for example, host bindings from multiple directives).
|
||||
|
||||
When there are multiple bindings to the same class name or style property, Angular uses a set of precedence rules to resolve conflicts and determine which classes or styles are ultimately applied to the element.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
<h4>Styling precedence (highest to lowest)</h4>
|
||||
|
||||
1. Template bindings
|
||||
1. Property binding (for example, `<div [class.foo]="hasFoo">` or `<div [style.color]="color">`)
|
||||
1. Map binding (for example, `<div [class]="classExpr">` or `<div [style]="styleExpr">`)
|
||||
1. Static value (for example, `<div class="foo">` or `<div style="color: blue">`)
|
||||
1. Directive host bindings
|
||||
1. Property binding (for example, `host: {'[class.foo]': 'hasFoo'}` or `host: {'[style.color]': 'color'}`)
|
||||
1. Map binding (for example, `host: {'[class]': 'classExpr'}` or `host: {'[style]': 'styleExpr'}`)
|
||||
1. Static value (for example, `host: {'class': 'foo'}` or `host: {'style': 'color: blue'}`)
|
||||
1. Component host bindings
|
||||
1. Property binding (for example, `host: {'[class.foo]': 'hasFoo'}` or `host: {'[style.color]': 'color'}`)
|
||||
1. Map binding (for example, `host: {'[class]': 'classExpr'}` or `host: {'[style]': 'styleExpr'}`)
|
||||
1. Static value (for example, `host: {'class': 'foo'}` or `host: {'style': 'color: blue'}`)
|
||||
|
||||
</div>
|
||||
|
||||
The more specific a class or style binding is, the higher its precedence.
|
||||
|
||||
A binding to a specific class (for example, `[class.foo]`) will take precedence over a generic `[class]` binding, and a binding to a specific style (for example, `[style.bar]`) will take precedence over a generic `[style]` binding.
|
||||
|
||||
<code-example path="attribute-binding/src/app/app.component.html" region="basic-specificity" header="src/app/app.component.html"></code-example>
|
||||
|
||||
Specificity rules also apply when it comes to bindings that originate from different sources.
|
||||
It's possible for an element to have bindings in the template where it's declared, from host bindings on matched directives, and from host bindings on matched components.
|
||||
|
||||
Template bindings are the most specific because they apply to the element directly and exclusively, so they have the highest precedence.
|
||||
|
||||
Directive host bindings are considered less specific because directives can be used in multiple locations, so they have a lower precedence than template bindings.
|
||||
|
||||
Directives often augment component behavior, so host bindings from components have the lowest precedence.
|
||||
|
||||
<code-example path="attribute-binding/src/app/app.component.html" region="source-specificity" header="src/app/app.component.html"></code-example>
|
||||
|
||||
In addition, bindings take precedence over static attributes.
|
||||
|
||||
In the following case, `class` and `[class]` have similar specificity, but the `[class]` binding will take precedence because it is dynamic.
|
||||
|
||||
<code-example path="attribute-binding/src/app/app.component.html" region="dynamic-priority" header="src/app/app.component.html"></code-example>
|
||||
|
||||
{@a styling-delegation}
|
||||
### Delegating to styles with lower precedence
|
||||
|
||||
It is possible for higher precedence styles to "delegate" to lower precedence styles using `undefined` values.
|
||||
Whereas setting a style property to `null` ensures the style is removed, setting it to `undefined` will cause Angular to fall back to the next-highest precedence binding to that style.
|
||||
|
||||
For example, consider the following template:
|
||||
|
||||
<code-example path="attribute-binding/src/app/app.component.html" region="style-delegation" header="src/app/app.component.html"></code-example>
|
||||
|
||||
Imagine that the `dirWithHostBinding` directive and the `comp-with-host-binding` component both have a `[style.width]` host binding.
|
||||
In that case, if `dirWithHostBinding` sets its binding to `undefined`, the `width` property will fall back to the value of the `comp-with-host-binding` host binding.
|
||||
However, if `dirWithHostBinding` sets its binding to `null`, the `width` property will be removed entirely.
|
||||
|
@ -194,7 +194,7 @@ which explains the following:
|
||||
* Using [`<ng-container>`](guide/structural-directives#ngcontainer "<ng-container>")
|
||||
to group elements when there is no suitable host element for the directive.
|
||||
* How to write your own structural directive.
|
||||
* That you can only apply [one structural directive](guide/structural-directives#one-per-element "one per host element") to an element.
|
||||
* Why you [can only apply one structural directive](guide/structural-directives#one-per-element "one per host element") to an element.
|
||||
|
||||
</div>
|
||||
|
||||
@ -281,7 +281,7 @@ You define a block of HTML that defines how a single item should be displayed
|
||||
and then you tell Angular to use that block as a template for rendering each item in the list.
|
||||
The text assigned to `*ngFor` is the instruction that guides the repeater process.
|
||||
|
||||
The following example shows `NgFor` applied to a simple `<div>`. (Don't forget the asterisk (`*`) in front of `ngFor`.)
|
||||
The following example shows `NgFor` applied to a simple `<div>`.
|
||||
|
||||
<code-example path="built-in-directives/src/app/app.component.html" region="NgFor-1" header="src/app/app.component.html"></code-example>
|
||||
|
||||
|
@ -1,312 +0,0 @@
|
||||
# Displaying data in views
|
||||
|
||||
Angular [components](guide/glossary#component) form the data structure of your application.
|
||||
The HTML [template](guide/glossary#template) associated with a component provides the means to display that data in the context of a web page.
|
||||
Together, a component's class and template form a [view](guide/glossary#view) of your application data.
|
||||
|
||||
The process of combining data values with their representation on the page is called [data binding](guide/glossary#data-binding).
|
||||
You display your data to a user (and collect data from the user) by *binding* controls in the HTML template to the data properties of the component class.
|
||||
|
||||
In addition, you can add logic to the template by including [directives](guide/glossary#directive), which tell Angular how to modify the page as it is rendered.
|
||||
|
||||
Angular defines a *template language* that expands HTML notation with syntax that allows you to define various kinds of data binding and logical directives.
|
||||
When the page is rendered, Angular interprets the template syntax to update the HTML according to your logic and current data state.
|
||||
Before you read the complete [template syntax guide](guide/template-syntax), the exercises on this page give you a quick demonstration of how template syntax works.
|
||||
|
||||
In this demo, you'll create a component with a list of heroes.
|
||||
You'll display the list of hero names and conditionally show a message below the list.
|
||||
The final UI looks like this:
|
||||
|
||||
<div class="lightbox">
|
||||
<img src="generated/images/guide/displaying-data/final.png" alt="Final UI">
|
||||
</div>
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
The <live-example></live-example> demonstrates all of the syntax and code snippets described in this page.
|
||||
|
||||
</div>
|
||||
|
||||
{@a interpolation}
|
||||
|
||||
## Showing component properties with interpolation
|
||||
The easiest way to display a component property is to bind the property name through interpolation.
|
||||
With interpolation, you put the property name in the view template, enclosed in double curly braces: `{{myHero}}`.
|
||||
|
||||
Use the CLI command [`ng new displaying-data`](cli/new) to create a workspace and app named `displaying-data`.
|
||||
|
||||
Delete the <code>app.component.html</code> file. It is not needed for this example.
|
||||
|
||||
Then modify the <code>app.component.ts</code> file by
|
||||
changing the template and the body of the component.
|
||||
|
||||
When you're done, it should look like this:
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.1.ts" header="src/app/app.component.ts"></code-example>
|
||||
|
||||
You added two properties to the formerly empty component: `title` and `myHero`.
|
||||
|
||||
The template displays the two component properties using double curly brace
|
||||
interpolation:
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.1.ts" header="src/app/app.component.ts (template)" region="template"></code-example>
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
The template is a multi-line string within ECMAScript 2015 backticks (<code>\`</code>).
|
||||
The backtick (<code>\`</code>)—which is *not* the same character as a single
|
||||
quote (`'`)—allows you to compose a string over several lines, which makes the
|
||||
HTML more readable.
|
||||
|
||||
</div>
|
||||
|
||||
Angular automatically pulls the value of the `title` and `myHero` properties from the component and
|
||||
inserts those values into the browser. Angular updates the display
|
||||
when these properties change.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
More precisely, the redisplay occurs after some kind of asynchronous event related to
|
||||
the view, such as a keystroke, a timer completion, or a response to an HTTP request.
|
||||
|
||||
</div>
|
||||
|
||||
Notice that you don't call **new** to create an instance of the `AppComponent` class.
|
||||
Angular is creating an instance for you. How?
|
||||
|
||||
The CSS `selector` in the `@Component` decorator specifies an element named `<app-root>`.
|
||||
That element is a placeholder in the body of your `index.html` file:
|
||||
|
||||
<code-example path="displaying-data/src/index.html" header="src/index.html (body)" region="body"></code-example>
|
||||
|
||||
When you bootstrap with the `AppComponent` class (in <code>main.ts</code>), Angular looks for a `<app-root>`
|
||||
in the `index.html`, finds it, instantiates an instance of `AppComponent`, and renders it
|
||||
inside the `<app-root>` tag.
|
||||
|
||||
Now run the app. It should display the title and hero name:
|
||||
|
||||
<div class="lightbox">
|
||||
<img src="generated/images/guide/displaying-data/title-and-hero.png" alt="Title and Hero">
|
||||
</div>
|
||||
|
||||
The next few sections review some of the coding choices in the app.
|
||||
|
||||
|
||||
## Choosing the template source
|
||||
|
||||
The `@Component` metadata tells Angular where to find the component's template.
|
||||
You can store your component's template in one of two places.
|
||||
|
||||
* You can define the template *inline* using the `template` property of the `@Component` decorator. An inline template is useful for a small demo or test.
|
||||
* Alternatively, you can define the template in a separate HTML file and link to that file in the `templateUrl` property of the `@Component` decorator. This configuration is typical for anything more complex than a small test or demo, and is the default when you generate a new component.
|
||||
|
||||
In either style, the template data bindings have the same access to the component's properties.
|
||||
Here the app uses inline HTML because the template is small and the demo is simpler without the additional HTML file.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
By default, the Angular CLI command [`ng generate component`](cli/generate) generates components with a template file.
|
||||
You can override that by adding the "-t" (short for `inlineTemplate=true`) option:
|
||||
|
||||
<code-example hideCopy language="sh" class="code-shell">
|
||||
ng generate component hero -t
|
||||
</code-example>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## Initialization
|
||||
|
||||
The following example uses variable assignment to initialize the components.
|
||||
|
||||
<code-example path="displaying-data/src/app/app-ctor.component.1.ts" region="class"></code-example>
|
||||
|
||||
You could instead declare and initialize the properties using a constructor.
|
||||
This app uses more terse "variable assignment" style simply for brevity.
|
||||
|
||||
|
||||
{@a ngFor}
|
||||
|
||||
## Add logic to loop through data
|
||||
|
||||
The `*ngFor` directive (predefined by Angular) lets you loop through data. The following example uses the directive to show all of the values in an array property.
|
||||
|
||||
To display a list of heroes, begin by adding an array of hero names to the component and redefine `myHero` to be the first name in the array.
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.2.ts" header="src/app/app.component.ts (class)" region="class"></code-example>
|
||||
|
||||
|
||||
Now use the Angular `ngFor` directive in the template to display each item in the `heroes` list.
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.2.ts" header="src/app/app.component.ts (template)" region="template"></code-example>
|
||||
|
||||
|
||||
This UI uses the HTML unordered list with `<ul>` and `<li>` tags. The `*ngFor`
|
||||
in the `<li>` element is the Angular "repeater" directive.
|
||||
It marks that `<li>` element (and its children) as the "repeater template":
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.2.ts" header="src/app/app.component.ts (li)" region="li"></code-example>
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
Don't forget the leading asterisk (\*) in `*ngFor`. It is an essential part of the syntax.
|
||||
Read more about `ngFor` and `*` in the [ngFor section](guide/built-in-directives#ngfor) of the [Built-in directives](guide/built-in-directives) page.
|
||||
|
||||
</div>
|
||||
|
||||
Notice the `hero` in the `ngFor` double-quoted instruction;
|
||||
it is an example of a template input variable. Read
|
||||
more about template input variables in the [microsyntax](guide/built-in-directives#microsyntax) section of
|
||||
the [Built-in directives](guide/built-in-directives) page.
|
||||
|
||||
Angular duplicates the `<li>` for each item in the list, setting the `hero` variable
|
||||
to the item (the hero) in the current iteration. Angular uses that variable as the
|
||||
context for the interpolation in the double curly braces.
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
In this case, `ngFor` is displaying an array, but `ngFor` can
|
||||
repeat items for any [iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) object.
|
||||
|
||||
</div>
|
||||
|
||||
Now the heroes appear in an unordered list.
|
||||
|
||||
<div class="lightbox">
|
||||
<img src="generated/images/guide/displaying-data/hero-names-list.png" alt="After ngfor">
|
||||
</div>
|
||||
|
||||
|
||||
## Creating a class for the data
|
||||
|
||||
The app's code defines the data directly inside the component, which isn't best practice.
|
||||
In a simple demo, however, it's fine.
|
||||
|
||||
At the moment, the binding is to an array of strings.
|
||||
In real applications, most bindings are to more specialized objects.
|
||||
|
||||
To convert this binding to use specialized objects, turn the array
|
||||
of hero names into an array of `Hero` objects. For that you'll need a `Hero` class:
|
||||
|
||||
<code-example language="sh" class="code-shell">
|
||||
ng generate class hero
|
||||
</code-example>
|
||||
|
||||
This command creates the following code.
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/app/hero.ts" header="src/app/hero.ts"></code-example>
|
||||
|
||||
You've defined a class with a constructor and two properties: `id` and `name`.
|
||||
|
||||
It might not look like the class has properties, but it does.
|
||||
The declaration of the constructor parameters takes advantage of a TypeScript shortcut.
|
||||
|
||||
Consider the first parameter:
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/app/hero.ts" header="src/app/hero.ts (id)" region="id"></code-example>
|
||||
|
||||
That brief syntax does a lot:
|
||||
|
||||
* Declares a constructor parameter and its type.
|
||||
* Declares a public property of the same name.
|
||||
* Initializes that property with the corresponding argument when creating an instance of the class.
|
||||
|
||||
|
||||
### Using the Hero class
|
||||
|
||||
After importing the `Hero` class, the `AppComponent.heroes` property can return a _typed_ array
|
||||
of `Hero` objects:
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.3.ts" header="src/app/app.component.ts (heroes)" region="heroes"></code-example>
|
||||
|
||||
|
||||
|
||||
Next, update the template.
|
||||
At the moment it displays the hero's `id` and `name`.
|
||||
Fix that to display only the hero's `name` property.
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.3.ts" header="src/app/app.component.ts (template)" region="template"></code-example>
|
||||
|
||||
|
||||
The display looks the same, but the code is clearer.
|
||||
|
||||
{@a ngIf}
|
||||
|
||||
## Conditional display with NgIf
|
||||
|
||||
Sometimes an app needs to display a view or a portion of a view only under specific circumstances.
|
||||
|
||||
Let's change the example to display a message if there are more than three heroes.
|
||||
|
||||
The Angular `ngIf` directive inserts or removes an element based on a _truthy/falsy_ condition.
|
||||
To see it in action, add the following paragraph at the bottom of the template:
|
||||
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.ts" header="src/app/app.component.ts (message)" region="message"></code-example>
|
||||
|
||||
|
||||
<div class="alert is-important">
|
||||
|
||||
Don't forget the leading asterisk (\*) in `*ngIf`. It is an essential part of the syntax.
|
||||
Read more about `ngIf` and `*` in the [ngIf section](guide/built-in-directives#ngIf) of the [Built-in directives](guide/built-in-directives) page.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
The template expression inside the double quotes,
|
||||
`*ngIf="heroes.length > 3"`, looks and behaves much like TypeScript.
|
||||
When the component's list of heroes has more than three items, Angular adds the paragraph
|
||||
to the DOM and the message appears.
|
||||
If there are three or fewer items, Angular omits the paragraph, so no message appears.
|
||||
|
||||
For more information, see [template expression operators](guide/interpolation#template-expressions).
|
||||
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
Angular isn't showing and hiding the message. It is adding and removing the paragraph element from the DOM. That improves performance, especially in larger projects when conditionally including or excluding
|
||||
big chunks of HTML with many data bindings.
|
||||
|
||||
</div>
|
||||
|
||||
Try it out. Because the array has four items, the message should appear.
|
||||
Go back into <code>app.component.ts</code> and delete or comment out one of the elements from the heroes array.
|
||||
The browser should refresh automatically and the message should disappear.
|
||||
|
||||
|
||||
## Summary
|
||||
Now you know how to use:
|
||||
|
||||
* **Interpolation** with double curly braces to display a component property.
|
||||
* **ngFor** to display an array of items.
|
||||
* A TypeScript class to shape the **model data** for your component and display properties of that model.
|
||||
* **ngIf** to conditionally display a chunk of HTML based on a boolean expression.
|
||||
|
||||
Here's the final code:
|
||||
|
||||
<code-tabs>
|
||||
|
||||
<code-pane header="src/app/app.component.ts" path="displaying-data/src/app/app.component.ts" region="final">
|
||||
|
||||
</code-pane>
|
||||
|
||||
<code-pane header="src/app/hero.ts" path="displaying-data/src/app/hero.ts">
|
||||
|
||||
</code-pane>
|
||||
|
||||
<code-pane header="src/app/app.module.ts" path="displaying-data/src/app/app.module.ts">
|
||||
|
||||
</code-pane>
|
||||
|
||||
<code-pane header="main.ts" path="displaying-data/src/main.ts">
|
||||
|
||||
</code-pane>
|
||||
|
||||
</code-tabs>
|
@ -39,7 +39,34 @@ which is the attribute, spelled with a lowercase `s`.
|
||||
|
||||
For more details, see the [MDN HTMLTableCellElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTableCellElement) documentation.
|
||||
|
||||
For more information about `colSpan` and `colspan`, see the [Attribute binding](guide/attribute-binding#colspan) guide.
|
||||
{@a colspan}
|
||||
|
||||
#### `colspan` and `colSpan`
|
||||
|
||||
A common point of confusion is between the attribute, `colspan`, and the property, `colSpan`.
|
||||
Notice that these two names differ by only a single letter.
|
||||
|
||||
If you wrote something like this:
|
||||
|
||||
<code-example language="html">
|
||||
<tr><td colspan="{{1 + 1}}">Three-Four</td></tr>
|
||||
</code-example>
|
||||
|
||||
You'd get this error:
|
||||
|
||||
<code-example language="bash">
|
||||
Template parse errors:
|
||||
Can't bind to 'colspan' since it isn't a known native property
|
||||
</code-example>
|
||||
|
||||
As the message says, the `<td>` element does not have a `colspan` property. This is true
|
||||
because `colspan` is an attribute—`colSpan`, with a capital `S`, is the
|
||||
corresponding property. Interpolation and property binding can set only *properties*, not attributes.
|
||||
|
||||
Instead, you'd use property binding and write it like this:
|
||||
|
||||
<code-example path="attribute-binding/src/app/app.component.html" region="colSpan" header="src/app/app.component.html"></code-example>
|
||||
|
||||
|
||||
Another example is disabling a button when the component says that it `isUnchanged`:
|
||||
|
||||
|
@ -1656,7 +1656,7 @@ _before_ the `AppRoutingModule`:
|
||||
|
||||
</code-tabs>
|
||||
|
||||
Remove the initial crisis center route from the `app-routing.module.ts` because now the `HeroesModule` and the `CrisisCenter` modules provide teh feature routes.
|
||||
Remove the initial crisis center route from the `app-routing.module.ts` because now the `HeroesModule` and the `CrisisCenter` modules provide the feature routes.
|
||||
|
||||
The `app-routing.module.ts` file retains the top-level application routes such as the default and wildcard routes.
|
||||
|
||||
|
133
aio/content/guide/style-precedence.md
Normal file
133
aio/content/guide/style-precedence.md
Normal file
@ -0,0 +1,133 @@
|
||||
# Style Precedence
|
||||
|
||||
When there are multiple bindings to the same class name or style attribute, Angular uses a set of precedence rules to determine which classes or styles to apply to the element.
|
||||
These rules specify an order for which style- and class-related bindings have priority.
|
||||
This styling precedence is as follows, from the most specific with the highest priority to least specific with the lowest priorty:
|
||||
|
||||
1. Template bindings are the most specific because they apply to the element directly and exclusively, so they have the highest precedence.
|
||||
<table width="100%">
|
||||
<col width="40%"></col>
|
||||
<col width="60%"></col>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Binding type</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Property binding</td>
|
||||
<td><code><div [class.foo]="hasFoo"></code><br><code><div [style.color]="color"></code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Map binding</td>
|
||||
<td><code><div [class]="classExpression"></code><br><code><div [style]="styleExpression"></code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Static value</td>
|
||||
<td><code><div class="foo"></code><br><code><div style="color: blue"></code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
1. Directive host bindings are less specific because you can use directives in multiple locations, so they have a lower precedence than template bindings.
|
||||
<table width="100%">
|
||||
<col width="40%"></col>
|
||||
<col width="60%"></col>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Binding type</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Property binding</td>
|
||||
<td><code>host: {'[class.foo]': 'hasFoo'}</code><br><code>host: {'[style.color]': 'color'}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Map binding</td>
|
||||
<td><code>host: {'[class]': 'classExpr'}</code><br><code>host: {'[style]': 'styleExpr'}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Static value</td>
|
||||
<td><code>host: {'class': 'foo'}</code><br><code>host: {'style': 'color: blue'}</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
1. Component host bindings have the lowest precedence.
|
||||
<table width="100%">
|
||||
<col width="40%"></col>
|
||||
<col width="60%"></col>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Binding type</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Property binding</td>
|
||||
<td><code>host: {'[class.foo]': 'hasFoo'}</code><br><code>host: {'[style.color]': 'color'}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Map binding</td>
|
||||
<td><code>host: {'[class]': 'classExpression'}</code><br><code>host: {'[style]': 'styleExpression'}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Static value</td>
|
||||
<td><code>host: {'class': 'foo'}</code><br><code>host: {'style': 'color: blue'}</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Precedence and specificity
|
||||
|
||||
In the following example, binding to a specific class, as in `[class.special]`, takes precedence over a generic `[class]` binding.
|
||||
Similarly, binding to a specific style, as in `[style.color]`, takes precedence over a generic `[style]` binding.
|
||||
|
||||
<code-example path="attribute-binding/src/app/app.component.html" region="basic-specificity" header="src/app/app.component.html"></code-example>
|
||||
|
||||
## Precedence and bindings from different sources
|
||||
|
||||
Specificity rules also apply to bindings even when they originate from different sources.
|
||||
An element can have bindings that originate from its own template, from host bindings on matched directives, and from host bindings on matched components.
|
||||
|
||||
<code-example path="attribute-binding/src/app/app.component.html" region="source-specificity" header="src/app/app.component.html"></code-example>
|
||||
|
||||
## Precedence of bindings and static attributes
|
||||
|
||||
Bindings take precedence over static attributes because they are dynamic.
|
||||
In the following case, `class` and `[class]` have similar specificity, but the `[class]` binding takes precedence.
|
||||
|
||||
<code-example path="attribute-binding/src/app/app.component.html" region="dynamic-priority" header="src/app/app.component.html"></code-example>
|
||||
|
||||
{@a styling-delegation}
|
||||
|
||||
## Delegating to styles with lower precedence
|
||||
|
||||
Higher precedence styles can defer to lower precedence styles using `undefined` values.
|
||||
For example, consider the following template:
|
||||
|
||||
<code-example path="attribute-binding/src/app/app.component.html" region="style-delegation" header="src/app/app.component.html"></code-example>
|
||||
|
||||
Imagine that the `dirWithHostBinding` directive and the `comp-with-host-binding` component both have a `[style.width]` host binding.
|
||||
|
||||
<code-example path="attribute-binding/src/app/comp-with-host-binding.component.ts" region="hostbinding" header="src/app/comp-with-host-binding.component.ts and dirWithHostBinding.directive.ts"></code-example>
|
||||
|
||||
If `dirWithHostBinding` sets its binding to `undefined`, the `width` property falls back to the value of the `comp-with-host-binding` host binding.
|
||||
|
||||
<code-example header="dirWithHostBinding directive">
|
||||
@HostBinding('style.width')
|
||||
width = ''; // undefined
|
||||
</code-example>
|
||||
|
||||
<div class="alert is-helpful">
|
||||
|
||||
If `dirWithHostBinding` sets its binding to `null`, Angular removes the `width` property entirely.
|
||||
|
||||
<code-example header="dirWithHostBinding">
|
||||
@HostBinding('style.width')
|
||||
width = null;
|
||||
</code-example>
|
||||
|
||||
</div>
|
@ -14,7 +14,7 @@ In the following example, the template statement `deleteHero()` appears in quote
|
||||
|
||||
<code-example path="template-syntax/src/app/app.component.html" region="context-component-statement" header="src/app/app.component.html"></code-example>
|
||||
|
||||
When the user clicks the **Delete hero** button, Angular calls the `deleteHero()` function in the component class.
|
||||
When the user clicks the **Delete hero** button, Angular calls the `deleteHero()` method in the component class.
|
||||
|
||||
You can use template statements with elements, components, or directives in response to events.
|
||||
|
||||
|
@ -234,8 +234,8 @@ To fix this issue, listen to both the _Enter_ key and the _blur_ event.
|
||||
|
||||
|
||||
## Put it all together
|
||||
The previous page showed how to [display data](guide/displaying-data).
|
||||
This page demonstrated event binding techniques.
|
||||
|
||||
This page demonstrated several event binding techniques.
|
||||
|
||||
Now, put it all together in a micro-app
|
||||
that can display a list of heroes and add new heroes to the list.
|
||||
|
@ -9,7 +9,7 @@ To understand the benefits of `NgZone`, it is important to have a clear grasp of
|
||||
|
||||
### Displaying and updating data in Angular
|
||||
|
||||
In Angular, you can [display data](guide/displaying-data) by binding controls in an HTML template to the properties of an Angular component.
|
||||
In Angular, you can display data by binding controls in an HTML template to the properties of an Angular component.
|
||||
|
||||
<code-example path="displaying-data/src/app/app.component.1.ts" header="src/app/app.component.ts"></code-example>
|
||||
|
||||
@ -102,13 +102,13 @@ In Angular, this step is unnecessary. Whenever you update the data, your HTML is
|
||||
|
||||
To understand how change detection works, first consider when the application needs to update the HTML. Typically, updates occur for one of the following reasons:
|
||||
|
||||
1. Component initialization. For example, when bootstrapping an Angular application, Angular loads the bootstrap component and triggers the [ApplicationRef.tick()](api/core/ApplicationRef#tick) to call change detection and View Rendering. Just as in the [displaying data](guide/displaying-data) sample, the `AppComponent` is the bootstrap component. This component has the properties `title` and `myHero`, which the application renders in the HTML.
|
||||
1. Component initialization. For example, when bootstrapping an Angular application, Angular loads the bootstrap component and triggers the [ApplicationRef.tick()](api/core/ApplicationRef#tick) to call change detection and View Rendering.
|
||||
|
||||
2. Event listener. The DOM event listener can update the data in an Angular component and also trigger change detection, as in the following example.
|
||||
1. Event listener. The DOM event listener can update the data in an Angular component and also trigger change detection, as in the following example.
|
||||
|
||||
<code-example path="user-input/src/app/click-me.component.ts" region="click-me-component" header="src/app/click-me.component.ts"></code-example>
|
||||
|
||||
3. HTTP Data Request. You can also get data from a server through an HTTP request. For example:
|
||||
1. HTTP Data Request. You can also get data from a server through an HTTP request. For example:
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
|
@ -106,11 +106,6 @@
|
||||
"title": "User Input",
|
||||
"tooltip": "User input triggers DOM events. Angular listens to those events with event bindings that funnel updated values back into your app's components and models."
|
||||
},
|
||||
{
|
||||
"url": "guide/pipes",
|
||||
"title": "Pipes",
|
||||
"tooltip": "Pipes transform displayed values within a template."
|
||||
},
|
||||
{
|
||||
"url": "guide/lifecycle-hooks",
|
||||
"title": "Component Lifecycle",
|
||||
@ -167,6 +162,11 @@
|
||||
"title": "Template statements",
|
||||
"tooltip": "Introductory guide to statements in templates that respond to events that components, directives, or elements raise."
|
||||
},
|
||||
{
|
||||
"url": "guide/pipes",
|
||||
"title": "Pipes",
|
||||
"tooltip": "Pipes transform displayed values within a template."
|
||||
},
|
||||
{
|
||||
"url": "guide/binding-syntax",
|
||||
"title": "Binding syntax",
|
||||
@ -609,11 +609,6 @@
|
||||
"title": "Building a Template-driven Form",
|
||||
"tooltip": "Create a template-driven form using directives and Angular template syntax."
|
||||
},
|
||||
{
|
||||
"url": "guide/displaying-data",
|
||||
"title": "Data binding",
|
||||
"tooltip": "Property binding helps show app data in the UI."
|
||||
},
|
||||
{
|
||||
"url": "guide/web-worker",
|
||||
"title": "Web Workers",
|
||||
|
@ -31,6 +31,7 @@
|
||||
{"type": 301, "source": "/guide/quickstart", "destination": "/start"},
|
||||
{"type": 301, "source": "/getting-started", "destination": "/start"},
|
||||
{"type": 301, "source": "/getting-started/:rest*", "destination": "/start/:rest*"},
|
||||
{"type": 301, "source": "/guide/displaying-data", "destination": "/start#template-syntax"},
|
||||
|
||||
// Renaming of Getting Started topics
|
||||
{"type": 301, "source": "/start/data", "destination": "/start/start-data"},
|
||||
|
@ -102,6 +102,9 @@
|
||||
"!/guide/cli-quickstart",
|
||||
"!/guide/cli-quickstart.html",
|
||||
"!/guide/cli-quickstart/",
|
||||
"!/guide/displaying-data",
|
||||
"!/guide/displaying-data.html",
|
||||
"!/guide/displaying-data/",
|
||||
"!/guide/learning-angular",
|
||||
"!/guide/learning-angular.html",
|
||||
"!/guide/learning-angular/",
|
||||
|
@ -23,7 +23,7 @@
|
||||
"build-local-with-viewengine": "yarn ~~build",
|
||||
"prebuild-local-with-viewengine-ci": "node scripts/switch-to-viewengine && yarn setup-local-ci",
|
||||
"build-local-with-viewengine-ci": "yarn ~~build --progress=false",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js d807b8240",
|
||||
"extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 06ad10668",
|
||||
"lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint",
|
||||
"test": "yarn check-env && ng test",
|
||||
"pree2e": "yarn check-env && yarn update-webdriver",
|
||||
@ -71,6 +71,7 @@
|
||||
"boilerplate:test": "node tools/examples/test.js",
|
||||
"generate-stackblitz": "node ./tools/stackblitz-builder/generateStackblitz",
|
||||
"generate-zips": "node ./tools/example-zipper/generateZips",
|
||||
"create-example": "node ./tools/examples/create-example.js",
|
||||
"build-404-page": "node scripts/build-404-page",
|
||||
"update-webdriver": "node ../scripts/webdriver-manager-update.js",
|
||||
"~~audit-web-app": "node scripts/audit-web-app",
|
||||
@ -172,6 +173,6 @@
|
||||
"unist-util-visit-parents": "^1.1.1",
|
||||
"watchr": "^3.0.1",
|
||||
"xregexp": "^4.0.0",
|
||||
"yargs": "^7.0.2"
|
||||
"yargs": "^16.1.0"
|
||||
}
|
||||
}
|
||||
|
@ -177,6 +177,7 @@
|
||||
/getting-started/forms /start/start-forms
|
||||
/getting-started/deployment /start/start-deployment
|
||||
/guide/cli-quickstart /start
|
||||
/guide/displaying-data /start#template-syntax
|
||||
/guide/learning-angular /start
|
||||
/guide/learning-angular.html /start
|
||||
/guide/metadata /guide/aot-compiler
|
||||
|
@ -29,11 +29,12 @@ sub-folder. Also there are a number of common boilerplate files that are needed
|
||||
example's project. We maintain these common boilerplate files centrally to reduce the amount of effort
|
||||
if one of them needs to change.
|
||||
|
||||
This `examples` tool folder contains two utilities:
|
||||
This `examples` tool folder contains three utilities:
|
||||
|
||||
* example-boilerplate.js - install/remove the npm dependencies and boilerplate files into/from each of the
|
||||
examples' subfolders.
|
||||
* run-example-e2e.js - run the e2e tests for one or more examples
|
||||
* create-example.js - create a new example from the `example-scaffold/` directory or by importing files from a CLI project.
|
||||
|
||||
See the [README.md](examples/README.md) for more details.
|
||||
|
||||
|
@ -150,6 +150,14 @@ See [aio/README.md](../../README.md#developer-tasks) for the available command-l
|
||||
|
||||
Running the script will create an `aio/protractor-results.txt` file with the results of the tests.
|
||||
|
||||
### `create-example.js`
|
||||
|
||||
The [create-example.js](./create-example.js) script creates a new example under the `aio/content/examples` directory.
|
||||
|
||||
You must provide a new name for the example.
|
||||
By default the script will place basic scaffold files into the new example (from [shared/example-scaffold](./shared/example-scaffold)).
|
||||
But you can also specify the path to a separate CLI project, from which the script will copy files that would not be considered "boilerplate".
|
||||
See the [Boilerplate overview](#boilerplate-overview) for more information.
|
||||
|
||||
### Updating example dependencies
|
||||
|
||||
|
@ -7,9 +7,9 @@ Follow these steps to update the examples to the latest versions of Angular (and
|
||||
> NOTE:
|
||||
> The [angular-cli-diff](https://github.com/cexbrayat/angular-cli-diff) repo can be a useful resource for discovering what dependency versions are used for a basic CLI app at a specific CLI version.
|
||||
|
||||
- In the [shared/](./shared) folder, run `yarn` to update the dependencies in the [shared/node_modules/](./shared/node_modules) folder and the [shared/yarn.lock](./shared/yarn.lock) file.
|
||||
- In the [shared/](./shared) directory, run `yarn` to update the dependencies in the [shared/node_modules/](./shared/node_modules) directory and the [shared/yarn.lock](./shared/yarn.lock) file.
|
||||
|
||||
- In the [shared/](./shared) folder, run `yarn sync-deps` to update the dependency versions of the `package.json` files in each sub-folder of [shared/boilerplate/](./shared/boilerplate) to match the ones in [shared/package.json](./shared/package.json).
|
||||
- In the [shared/](./shared) directory, run `yarn sync-deps` to update the dependency versions of the `package.json` files in each sub-folder of [shared/boilerplate/](./shared/boilerplate) to match the ones in [shared/package.json](./shared/package.json).
|
||||
|
||||
- Follow the steps in the following section to update the rest of the boilerplate files.
|
||||
|
||||
@ -24,7 +24,7 @@ Any necessary changes to boilerplate files will be done automatically through mi
|
||||
> You have to make these changes (if any) manually.
|
||||
> Again, the [angular-cli-diff](https://github.com/cexbrayat/angular-cli-diff) repo can be a useful resource for discovering changes between versions.
|
||||
|
||||
- In the [shared/boilerplate/cli/](./shared/boilerplate/cli) folder, run the following commands to migrate the the project to the current versions of Angular CLI and the Angular framework (updated in previous steps):
|
||||
- In the [shared/boilerplate/cli/](./shared/boilerplate/cli) directory, run the following commands to migrate the the project to the current versions of Angular CLI and the Angular framework (updated in previous steps):
|
||||
```sh
|
||||
# Ensure dependencies are installed.
|
||||
yarn install
|
||||
@ -38,8 +38,11 @@ Any necessary changes to boilerplate files will be done automatically through mi
|
||||
> In order for `ng update` to work, there must be a `node_modules/` directory with installed dependencies inside the [shared/boilerplate/cli/](./shared/boilerplate/cli) directory.
|
||||
> This `node_modules/` directory is only needed during the update operation and is otherwise ignored (both by git and by the [example-boilerplate.js](./example-boilerplate.js) script) by means of the [shared/boilerplate/.gitignore](./shared/boilerplate/.gitignore) file.
|
||||
|
||||
- The previous command made any necessary changes to boilerplate files inside the `cli/` folder, but the same changes need to be applied to the other CLI-based boilerplate folders.
|
||||
Inspect the changes in `cli/` and manually apply the necessary ones to other CLI-based boilerplate folders.
|
||||
- The previous command made any necessary changes to boilerplate files inside the `cli/` directory, but the same changes need to be applied to the other CLI-based boilerplate directories.
|
||||
Inspect the changes in `cli/` and manually apply the necessary ones to other CLI-based boilerplate directories.
|
||||
|
||||
- Also ensure that any relevant changes in the [shared/boilerplate/cli/](./shared/boilerplate/cli) directory are copied to the [shared/example-scaffold/](./shared/example-scaffold) directory, which is used when creating new examples (via `yarn create-example ...`).
|
||||
Only files that would not be considered boilerplate should be added to the `example-scaffold/` directory.
|
||||
|
||||
- Ensure any changes to [cli/tslint.json](./shared/boilerplate/cli/tslint.json) are ported over to [systemjs/tslint.json](./shared/boilerplate/systemjs/tslint.json) and also [aio/content/examples/tslint.json](../../content/examples/tslint.json).
|
||||
This last part is important, since this file is used to lint example code on CI.
|
||||
|
6
aio/tools/examples/constants.js
Normal file
6
aio/tools/examples/constants.js
Normal file
@ -0,0 +1,6 @@
|
||||
const path = require('canonical-path');
|
||||
|
||||
exports.EXAMPLES_BASE_PATH = path.resolve(__dirname, '../../content/examples');
|
||||
exports.EXAMPLE_CONFIG_FILENAME = 'example-config.json';
|
||||
exports.SHARED_PATH = path.resolve(__dirname, 'shared');
|
||||
exports.STACKBLITZ_CONFIG_FILENAME = 'stackblitz.json';
|
140
aio/tools/examples/create-example.js
Normal file
140
aio/tools/examples/create-example.js
Normal file
@ -0,0 +1,140 @@
|
||||
const fs = require('fs-extra');
|
||||
const glob = require('glob');
|
||||
const ignore = require('ignore');
|
||||
const path = require('canonical-path');
|
||||
const shelljs = require('shelljs');
|
||||
const yargs = require('yargs');
|
||||
|
||||
const {EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, SHARED_PATH, STACKBLITZ_CONFIG_FILENAME} =
|
||||
require('./constants');
|
||||
const BASIC_SOURCE_PATH = path.resolve(SHARED_PATH, 'example-scaffold');
|
||||
|
||||
shelljs.set('-e');
|
||||
|
||||
if (require.main === module) {
|
||||
const options =
|
||||
yargs(process.argv.slice(2))
|
||||
.command(
|
||||
'$0 <name> [source]',
|
||||
[
|
||||
'Create a new <name> example.',
|
||||
'',
|
||||
'If [source] is provided then the relevant files from the CLI project at that path are copied into the example.',
|
||||
].join('\n'))
|
||||
.strict()
|
||||
.version(false)
|
||||
.argv;
|
||||
|
||||
const exampleName = options.name;
|
||||
const examplePath = path.resolve(EXAMPLES_BASE_PATH, exampleName);
|
||||
|
||||
console.log('Creating new example at', examplePath);
|
||||
createEmptyExample(exampleName, examplePath);
|
||||
|
||||
const sourcePath =
|
||||
options.source !== undefined ? path.resolve(options.source) : BASIC_SOURCE_PATH;
|
||||
console.log('Copying files from', sourcePath);
|
||||
copyExampleFiles(sourcePath, examplePath, exampleName);
|
||||
|
||||
console.log(`The new "${exampleName}" example has been created.`);
|
||||
console.log('Now run "yarn boilerplate:add" to set it up for development.');
|
||||
console.log(
|
||||
'You can find more info on working with docs examples in aio/tools/examples/README.md.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the directory and marker files for the new example.
|
||||
*/
|
||||
function createEmptyExample(exampleName, examplePath) {
|
||||
ensureExamplePath(examplePath);
|
||||
writeExampleConfigFile(examplePath);
|
||||
writeStackBlitzFile(exampleName, examplePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the new example directory exists.
|
||||
*/
|
||||
function ensureExamplePath(examplePath) {
|
||||
if (fs.existsSync(examplePath)) {
|
||||
throw new Error(
|
||||
`Unable to create example. The path to the new example already exists: ${examplePath}`);
|
||||
}
|
||||
fs.ensureDirSync(examplePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the `example-config.json` file to the new example.
|
||||
*/
|
||||
function writeExampleConfigFile(examplePath) {
|
||||
fs.writeFileSync(path.resolve(examplePath, EXAMPLE_CONFIG_FILENAME), '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the `stackblitz.json` file into the new example.
|
||||
*/
|
||||
function writeStackBlitzFile(exampleName, examplePath) {
|
||||
const config = {
|
||||
description: titleize(exampleName),
|
||||
files: ['!**/*.d.ts', '!**/*.js', '!**/*.[1,2].*'],
|
||||
tags: [exampleName.split('-')]
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.resolve(examplePath, STACKBLITZ_CONFIG_FILENAME),
|
||||
JSON.stringify(config, null, 2) + '\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all the files from the `sourcePath`, which are not ignored by the `.gitignore` file in the
|
||||
* `EXAMPLES_BASE_PATH`, to the `examplePath`.
|
||||
*/
|
||||
function copyExampleFiles(sourcePath, examplePath, exampleName) {
|
||||
const gitIgnoreSource = getGitIgnore(sourcePath);
|
||||
const gitIgnoreExamples = getGitIgnore(EXAMPLES_BASE_PATH);
|
||||
|
||||
// Grab the files in the source folder and filter them based on the gitignore rules.
|
||||
const sourceFiles =
|
||||
glob.sync('**/*', {
|
||||
cwd: sourcePath,
|
||||
dot: true,
|
||||
ignore: ['**/node_modules/**', '.git/**', '.gitignore'],
|
||||
mark: true
|
||||
})
|
||||
// Filter out the directories, leaving only files
|
||||
.filter(filePath => !/\/$/.test(filePath))
|
||||
// Filter out files that match the source directory .gitignore rules
|
||||
.filter(filePath => !gitIgnoreSource.ignores(filePath))
|
||||
// Filter out files that match the examples directory .gitignore rules
|
||||
.filter(filePath => !gitIgnoreExamples.ignores(path.join(exampleName, filePath)));
|
||||
|
||||
for (const sourceFile of sourceFiles) {
|
||||
console.log(' - ', sourceFile);
|
||||
const destPath = path.resolve(examplePath, sourceFile)
|
||||
fs.ensureDirSync(path.dirname(destPath));
|
||||
fs.copySync(path.resolve(sourcePath, sourceFile), destPath);
|
||||
}
|
||||
}
|
||||
|
||||
function getGitIgnore(directory) {
|
||||
const gitIgnoreMatcher = ignore();
|
||||
const gitignoreFilePath = path.resolve(directory, '.gitignore');
|
||||
if (fs.existsSync(gitignoreFilePath)) {
|
||||
const gitignoreFile = fs.readFileSync(gitignoreFilePath, 'utf8');
|
||||
gitIgnoreMatcher.add(gitignoreFile);
|
||||
}
|
||||
return gitIgnoreMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a kebab-case string to space separated Title Case string.
|
||||
*/
|
||||
function titleize(input) {
|
||||
return input.replace(
|
||||
/(-|^)(.)/g, (_, pre, char) => `${pre === '-' ? ' ' : ''}${char.toUpperCase()}`);
|
||||
}
|
||||
|
||||
exports.createEmptyExample = createEmptyExample;
|
||||
exports.ensureExamplePath = ensureExamplePath;
|
||||
exports.writeExampleConfigFile = writeExampleConfigFile;
|
||||
exports.writeStackBlitzFile = writeStackBlitzFile;
|
||||
exports.copyExampleFiles = copyExampleFiles;
|
||||
exports.titleize = titleize;
|
130
aio/tools/examples/create-example.spec.js
Normal file
130
aio/tools/examples/create-example.spec.js
Normal file
@ -0,0 +1,130 @@
|
||||
const path = require('canonical-path');
|
||||
const fs = require('fs-extra');
|
||||
const {glob} = require('glob');
|
||||
|
||||
const {EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, SHARED_PATH, STACKBLITZ_CONFIG_FILENAME} =
|
||||
require('./constants');
|
||||
|
||||
const {
|
||||
copyExampleFiles,
|
||||
createEmptyExample,
|
||||
ensureExamplePath,
|
||||
titleize,
|
||||
writeExampleConfigFile,
|
||||
writeStackBlitzFile
|
||||
} = require('./create-example');
|
||||
|
||||
describe('create-example tool', () => {
|
||||
describe('createEmptyExample', () => {
|
||||
it('should create an empty example with marker files', () => {
|
||||
spyOn(fs, 'existsSync').and.returnValue(false);
|
||||
spyOn(fs, 'ensureDirSync');
|
||||
const writeFileSpy = spyOn(fs, 'writeFileSync');
|
||||
|
||||
createEmptyExample('foo-bar', '/path/to/foo-bar');
|
||||
expect(writeFileSpy).toHaveBeenCalledTimes(2);
|
||||
expect(writeFileSpy)
|
||||
.toHaveBeenCalledWith(`/path/to/foo-bar/${EXAMPLE_CONFIG_FILENAME}`, jasmine.any(String));
|
||||
expect(writeFileSpy)
|
||||
.toHaveBeenCalledWith(
|
||||
`/path/to/foo-bar/${STACKBLITZ_CONFIG_FILENAME}`, jasmine.any(String));
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureExamplePath', () => {
|
||||
it('should error if the path already exists', () => {
|
||||
spyOn(fs, 'existsSync').and.returnValue(true);
|
||||
expect(() => ensureExamplePath('foo/bar'))
|
||||
.toThrowError(
|
||||
`Unable to create example. The path to the new example already exists: foo/bar`);
|
||||
});
|
||||
|
||||
it('should create the directory on disk', () => {
|
||||
spyOn(fs, 'existsSync').and.returnValue(false);
|
||||
const spy = spyOn(fs, 'ensureDirSync');
|
||||
ensureExamplePath('foo/bar');
|
||||
expect(spy).toHaveBeenCalledWith('foo/bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeExampleConfigFile', () => {
|
||||
it('should write a JSON file to disk', () => {
|
||||
const spy = spyOn(fs, 'writeFileSync');
|
||||
writeExampleConfigFile('/foo/bar');
|
||||
expect(spy).toHaveBeenCalledWith(`/foo/bar/${EXAMPLE_CONFIG_FILENAME}`, '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeStackBlitzFile', () => {
|
||||
it('should write a JSON file to disk', () => {
|
||||
const spy = spyOn(fs, 'writeFileSync');
|
||||
writeStackBlitzFile('bar-bar', '/foo/bar-bar');
|
||||
expect(spy).toHaveBeenCalledWith(`/foo/bar-bar/${STACKBLITZ_CONFIG_FILENAME}`, [
|
||||
'{',
|
||||
' "description": "Bar Bar",',
|
||||
' "files": [',
|
||||
' "!**/*.d.ts",',
|
||||
' "!**/*.js",',
|
||||
' "!**/*.[1,2].*"',
|
||||
' ],',
|
||||
' "tags": [',
|
||||
' [',
|
||||
' "bar",',
|
||||
' "bar"',
|
||||
' ]',
|
||||
' ]',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyExampleFiles', () => {
|
||||
it('should copy over files that are not ignored by git', () => {
|
||||
const examplesGitIgnorePath = path.resolve(EXAMPLES_BASE_PATH, '.gitignore');
|
||||
const sourceGitIgnorePath = path.resolve('/source/path', '.gitignore');
|
||||
|
||||
spyOn(console, 'log');
|
||||
spyOn(fs, 'existsSync').and.returnValue(true);
|
||||
const readFileSyncSpy = spyOn(fs, 'readFileSync').and.callFake(p => {
|
||||
switch (p) {
|
||||
case examplesGitIgnorePath:
|
||||
return '**/a/b/**';
|
||||
case sourceGitIgnorePath:
|
||||
return '**/*.bad';
|
||||
default:
|
||||
throw new Error('Unexpected path');
|
||||
}
|
||||
});
|
||||
spyOn(glob, 'sync').and.returnValue([
|
||||
'a/', 'a/b/', 'a/c', 'x.ts', 'x.bad', 'a/b/y.ts', 'a/b/y.bad'
|
||||
]);
|
||||
const ensureDirSyncSpy = spyOn(fs, 'ensureDirSync');
|
||||
const copySyncSpy = spyOn(fs, 'copySync');
|
||||
|
||||
copyExampleFiles('/source/path', '/path/to/test-example', 'test-example');
|
||||
|
||||
expect(readFileSyncSpy).toHaveBeenCalledWith(examplesGitIgnorePath, 'utf8');
|
||||
expect(readFileSyncSpy).toHaveBeenCalledWith(sourceGitIgnorePath, 'utf8');
|
||||
|
||||
expect(ensureDirSyncSpy.calls.allArgs()).toEqual([
|
||||
['/path/to/test-example/a'],
|
||||
['/path/to/test-example'],
|
||||
]);
|
||||
|
||||
expect(copySyncSpy.calls.allArgs()).toEqual([
|
||||
['/source/path/a/c', '/path/to/test-example/a/c'],
|
||||
['/source/path/x.ts', '/path/to/test-example/x.ts'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('titleize', () => {
|
||||
it('should convert a kebab-case string to title-case', () => {
|
||||
expect(titleize('abc')).toEqual('Abc');
|
||||
expect(titleize('abc-def')).toEqual('Abc Def');
|
||||
expect(titleize('123')).toEqual('123');
|
||||
expect(titleize('abc---def')).toEqual('Abc - Def');
|
||||
});
|
||||
});
|
||||
});
|
@ -4,8 +4,8 @@ const ignore = require('ignore');
|
||||
const path = require('canonical-path');
|
||||
const shelljs = require('shelljs');
|
||||
const yargs = require('yargs');
|
||||
const {EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, SHARED_PATH} = require('./constants');
|
||||
|
||||
const SHARED_PATH = path.resolve(__dirname, 'shared');
|
||||
const SHARED_NODE_MODULES_PATH = path.resolve(SHARED_PATH, 'node_modules');
|
||||
|
||||
const BOILERPLATE_BASE_PATH = path.resolve(SHARED_PATH, 'boilerplate');
|
||||
@ -13,9 +13,6 @@ const BOILERPLATE_CLI_PATH = path.resolve(BOILERPLATE_BASE_PATH, 'cli');
|
||||
const BOILERPLATE_COMMON_PATH = path.resolve(BOILERPLATE_BASE_PATH, 'common');
|
||||
const BOILERPLATE_VIEWENGINE_PATH = path.resolve(BOILERPLATE_BASE_PATH, 'viewengine');
|
||||
|
||||
const EXAMPLES_BASE_PATH = path.resolve(__dirname, '../../content/examples');
|
||||
const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
|
||||
|
||||
class ExampleBoilerPlate {
|
||||
/**
|
||||
* Add boilerplate files to all the examples
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
// Add your e2e tests here
|
||||
|
||||
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));
|
||||
});
|
||||
});
|
@ -0,0 +1 @@
|
||||
<h1>Replace the src folder in this {{title}} with yours.</h1>
|
@ -0,0 +1,20 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
// Add your unit tests here
|
||||
});
|
@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css'],
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'example';
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
13
aio/tools/examples/shared/example-scaffold/src/index.html
Normal file
13
aio/tools/examples/shared/example-scaffold/src/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Ponyracer</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
12
aio/tools/examples/shared/example-scaffold/src/main.ts
Normal file
12
aio/tools/examples/shared/example-scaffold/src/main.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
155
aio/yarn.lock
155
aio/yarn.lock
@ -3075,11 +3075,6 @@ camelcase@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
|
||||
integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
|
||||
|
||||
camelcase@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
|
||||
integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo=
|
||||
|
||||
camelcase@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
|
||||
@ -3459,7 +3454,7 @@ cli-width@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
|
||||
integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
|
||||
|
||||
cliui@^3.0.3, cliui@^3.2.0:
|
||||
cliui@^3.0.3:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
|
||||
integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=
|
||||
@ -3495,6 +3490,15 @@ cliui@^6.0.0:
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
cliui@^7.0.2:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.2.tgz#e3a412e1d5ec0ccbe50d1b4120fc8164e97881f4"
|
||||
integrity sha512-lhpKkuUj67j5JgZIPZxLe7nSa4MQoojzRVWQyzMqBp2hBg6gwRjUDAwC1YDeBaC3APDBKNnjWbv2mlDF4XgOSA==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
clone@^1.0.2:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||
@ -4905,7 +4909,7 @@ errno@^0.1.1, errno@^0.1.3, errno@~0.1.7:
|
||||
dependencies:
|
||||
prr "~1.0.1"
|
||||
|
||||
error-ex@^1.2.0, error-ex@^1.3.1:
|
||||
error-ex@^1.3.1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
|
||||
integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
|
||||
@ -5017,6 +5021,11 @@ es6-weak-map@^2.0.1, es6-weak-map@^2.0.2:
|
||||
es6-iterator "^2.0.3"
|
||||
es6-symbol "^3.1.1"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
|
||||
|
||||
escape-html@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
@ -5583,14 +5592,6 @@ find-free-port@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/find-free-port/-/find-free-port-2.0.0.tgz#4b22e5f6579eb1a38c41ac6bcb3efed1b6da9b1b"
|
||||
integrity sha1-SyLl9leesaOMQaxryz7+0bbamxs=
|
||||
|
||||
find-up@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
|
||||
integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=
|
||||
dependencies:
|
||||
path-exists "^2.0.0"
|
||||
pinkie-promise "^2.0.0"
|
||||
|
||||
find-up@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
|
||||
@ -5925,7 +5926,7 @@ get-caller-file@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
|
||||
integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
|
||||
|
||||
get-caller-file@^2.0.1:
|
||||
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
@ -7374,11 +7375,6 @@ is-url@^1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
|
||||
integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
|
||||
|
||||
is-utf8@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
||||
integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
|
||||
|
||||
is-whitespace-character@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7"
|
||||
@ -8137,17 +8133,6 @@ listenercount@~1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937"
|
||||
integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=
|
||||
|
||||
load-json-file@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
|
||||
integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=
|
||||
dependencies:
|
||||
graceful-fs "^4.1.2"
|
||||
parse-json "^2.2.0"
|
||||
pify "^2.0.0"
|
||||
pinkie-promise "^2.0.0"
|
||||
strip-bom "^2.0.0"
|
||||
|
||||
load-json-file@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
|
||||
@ -9824,13 +9809,6 @@ parse-entities@^1.0.2, parse-entities@^1.1.0:
|
||||
is-decimal "^1.0.0"
|
||||
is-hexadecimal "^1.0.0"
|
||||
|
||||
parse-json@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
|
||||
integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
|
||||
dependencies:
|
||||
error-ex "^1.2.0"
|
||||
|
||||
parse-json@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
|
||||
@ -9910,13 +9888,6 @@ path-dirname@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
|
||||
integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
|
||||
|
||||
path-exists@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
|
||||
integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=
|
||||
dependencies:
|
||||
pinkie-promise "^2.0.0"
|
||||
|
||||
path-exists@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
|
||||
@ -9959,15 +9930,6 @@ path-to-regexp@^1.7.0:
|
||||
dependencies:
|
||||
isarray "0.0.1"
|
||||
|
||||
path-type@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
|
||||
integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=
|
||||
dependencies:
|
||||
graceful-fs "^4.1.2"
|
||||
pify "^2.0.0"
|
||||
pinkie-promise "^2.0.0"
|
||||
|
||||
path-type@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
|
||||
@ -10872,23 +10834,6 @@ read-package-tree@5.3.1:
|
||||
readdir-scoped-modules "^1.0.0"
|
||||
util-promisify "^2.1.0"
|
||||
|
||||
read-pkg-up@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
|
||||
integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=
|
||||
dependencies:
|
||||
find-up "^1.0.0"
|
||||
read-pkg "^1.0.0"
|
||||
|
||||
read-pkg@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
|
||||
integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=
|
||||
dependencies:
|
||||
load-json-file "^1.0.0"
|
||||
normalize-package-data "^2.3.2"
|
||||
path-type "^1.0.0"
|
||||
|
||||
read-pkg@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
|
||||
@ -12381,7 +12326,7 @@ string-length@^1.0.0:
|
||||
dependencies:
|
||||
strip-ansi "^3.0.0"
|
||||
|
||||
string-width@^1.0.1, string-width@^1.0.2:
|
||||
string-width@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
|
||||
integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
|
||||
@ -12513,13 +12458,6 @@ strip-ansi@^6.0.0:
|
||||
dependencies:
|
||||
ansi-regex "^5.0.0"
|
||||
|
||||
strip-bom@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
|
||||
integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=
|
||||
dependencies:
|
||||
is-utf8 "^0.2.0"
|
||||
|
||||
strip-bom@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
||||
@ -14010,11 +13948,6 @@ when@~3.6.x:
|
||||
resolved "https://registry.yarnpkg.com/when/-/when-3.6.4.tgz#473b517ec159e2b85005497a13983f095412e34e"
|
||||
integrity sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=
|
||||
|
||||
which-module@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
|
||||
integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=
|
||||
|
||||
which-module@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||
@ -14114,6 +14047,15 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
@ -14246,7 +14188,7 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
y18n@^3.2.0, y18n@^3.2.1:
|
||||
y18n@^3.2.0:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
|
||||
integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
|
||||
@ -14256,6 +14198,11 @@ y18n@^3.2.0, y18n@^3.2.1:
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
|
||||
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
|
||||
|
||||
y18n@^5.0.2:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.3.tgz#978115b82befe2b5c762bf55980b7b01a4a2d5d9"
|
||||
integrity sha512-JeFbcHQ/7hVmMBXW6UB6Tg7apStHd/ztGz1JN78y3pFi/q0Ht1eA6PVkvw56gm7UA8fcJR/ziRlYEDMGoju0yQ==
|
||||
|
||||
yallist@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
|
||||
@ -14295,12 +14242,10 @@ yargs-parser@^18.1.0, yargs-parser@^18.1.1, yargs-parser@^18.1.3:
|
||||
camelcase "^5.0.0"
|
||||
decamelize "^1.2.0"
|
||||
|
||||
yargs-parser@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
|
||||
integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=
|
||||
dependencies:
|
||||
camelcase "^3.0.0"
|
||||
yargs-parser@^20.2.2:
|
||||
version "20.2.2"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.2.tgz#84562c6b1c41ccec2f13d346c7dd83f8d1a0dc70"
|
||||
integrity sha512-XmrpXaTl6noDsf1dKpBuUNCOHqjs0g3jRMXf/ztRxdOmb+er8kE5z5b55Lz3p5u2T8KJ59ENBnASS8/iapVJ5g==
|
||||
|
||||
yargs@15.3.0:
|
||||
version "15.3.0"
|
||||
@ -14383,24 +14328,18 @@ yargs@^15.3.1:
|
||||
y18n "^4.0.0"
|
||||
yargs-parser "^18.1.1"
|
||||
|
||||
yargs@^7.0.2:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
|
||||
integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=
|
||||
yargs@^16.1.0:
|
||||
version "16.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.1.0.tgz#fc333fe4791660eace5a894b39d42f851cd48f2a"
|
||||
integrity sha512-upWFJOmDdHN0syLuESuvXDmrRcWd1QafJolHskzaw79uZa7/x53gxQKiR07W59GWY1tFhhU/Th9DrtSfpS782g==
|
||||
dependencies:
|
||||
camelcase "^3.0.0"
|
||||
cliui "^3.2.0"
|
||||
decamelize "^1.1.1"
|
||||
get-caller-file "^1.0.1"
|
||||
os-locale "^1.4.0"
|
||||
read-pkg-up "^1.0.1"
|
||||
cliui "^7.0.2"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
require-main-filename "^1.0.1"
|
||||
set-blocking "^2.0.0"
|
||||
string-width "^1.0.2"
|
||||
which-module "^1.0.0"
|
||||
y18n "^3.2.1"
|
||||
yargs-parser "^5.0.0"
|
||||
string-width "^4.2.0"
|
||||
y18n "^5.0.2"
|
||||
yargs-parser "^20.2.2"
|
||||
|
||||
yauzl@^2.10.0:
|
||||
version "2.10.0"
|
||||
|
@ -35,31 +35,17 @@ export async function printG3Comparison(git: GitClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** Random prefix to create unique branch names. */
|
||||
const randomPrefix = `prefix${Math.floor(Math.random() * 1000000)}`;
|
||||
/** Ref name of the temporary master branch. */
|
||||
const masterRef = `${randomPrefix}-master`;
|
||||
/** Ref name of the temporary g3 branch. */
|
||||
const g3Ref = `${randomPrefix}-g3`;
|
||||
/** Url of the ref for fetching master and g3 branches. */
|
||||
const refUrl = `https://github.com/${git.remoteConfig.owner}/${git.remoteConfig.name}.git`;
|
||||
/** The result fo the fetch command. */
|
||||
const fetchResult =
|
||||
git.runGraceful(['fetch', '-q', refUrl, `master:${masterRef}`, `g3:${g3Ref}`]);
|
||||
/** The latest sha for the g3 branch. */
|
||||
const g3Ref = getShaForBranchLatest('g3');
|
||||
/** The latest sha for the master branch. */
|
||||
const masterRef = getShaForBranchLatest('master');
|
||||
|
||||
// If the upstream repository does not have a g3 branch to compare to, skip the comparison.
|
||||
if (fetchResult.status !== 0) {
|
||||
if (fetchResult.stderr.includes(`couldn't find remote ref g3`)) {
|
||||
return debug('No g3 branch exists on upstream, skipping.');
|
||||
}
|
||||
throw Error('Fetch of master and g3 branches for comparison failed.');
|
||||
if (!g3Ref && !masterRef) {
|
||||
return debug('Exiting early as either the g3 or master was unable to be retrieved');
|
||||
}
|
||||
|
||||
/** The statistical information about the git diff between master and g3. */
|
||||
const stats = getDiffStats(git);
|
||||
|
||||
// Delete the temporarily created mater and g3 branches.
|
||||
git.runGraceful(['branch', '-D', masterRef, g3Ref]);
|
||||
const stats = getDiffStats();
|
||||
|
||||
info.group(bold('g3 branch check'));
|
||||
info(`${stats.commits} commits between g3 and master`);
|
||||
@ -73,11 +59,27 @@ export async function printG3Comparison(git: GitClient) {
|
||||
info();
|
||||
|
||||
|
||||
/** Fetch and retrieve the latest sha for a specific branch. */
|
||||
function getShaForBranchLatest(branch: string) {
|
||||
/** The result fo the fetch command. */
|
||||
const fetchResult = git.runGraceful([
|
||||
'fetch', '-q', `https://github.com/${git.remoteConfig.owner}/${git.remoteConfig.name}.git`,
|
||||
branch
|
||||
]);
|
||||
|
||||
if (fetchResult.status !== 0 &&
|
||||
fetchResult.stderr.includes(`couldn't find remote ref ${branch}`)) {
|
||||
debug(`No '${branch}' branch exists on upstream, skipping.`);
|
||||
return false;
|
||||
}
|
||||
return git.runGraceful(['rev-parse', 'FETCH_HEAD']).stdout.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get git diff stats between master and g3, for all files and filtered to only g3 affecting
|
||||
* files.
|
||||
*/
|
||||
function getDiffStats(git: GitClient) {
|
||||
function getDiffStats() {
|
||||
/** The diff stats to be returned. */
|
||||
const stats = {
|
||||
insertions: 0,
|
||||
@ -86,7 +88,6 @@ export async function printG3Comparison(git: GitClient) {
|
||||
commits: 0,
|
||||
};
|
||||
|
||||
|
||||
// Determine the number of commits between master and g3 refs. */
|
||||
stats.commits = parseInt(git.run(['rev-list', '--count', `${g3Ref}..${masterRef}`]).stdout, 10);
|
||||
|
||||
|
BIN
docs/images/angular-ecosystem-logos.png
Normal file
BIN
docs/images/angular-ecosystem-logos.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 196 KiB |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "11.0.0-next.6",
|
||||
"version": "11.1.0-next.0",
|
||||
"private": true,
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
"homepage": "https://github.com/angular/angular",
|
||||
|
@ -12,6 +12,7 @@ import {CompileReflector} from '../compile_reflector';
|
||||
import {EmitterVisitorContext} from './abstract_emitter';
|
||||
import {AbstractJsEmitterVisitor} from './abstract_js_emitter';
|
||||
import * as o from './output_ast';
|
||||
import {newTrustedFunctionForJIT} from './output_jit_trusted_types';
|
||||
|
||||
/**
|
||||
* A helper class to manage the evaluation of JIT generated code.
|
||||
@ -69,11 +70,11 @@ export class JitEvaluator {
|
||||
// function anonymous(a,b,c
|
||||
// /**/) { ... }```
|
||||
// We don't want to hard code this fact, so we auto detect it via an empty function first.
|
||||
const emptyFn = new Function(...fnArgNames.concat('return null;')).toString();
|
||||
const emptyFn = newTrustedFunctionForJIT(...fnArgNames.concat('return null;')).toString();
|
||||
const headerLines = emptyFn.slice(0, emptyFn.indexOf('return null;')).split('\n').length - 1;
|
||||
fnBody += `\n${ctx.toSourceMapGenerator(sourceUrl, headerLines).toJsComment()}`;
|
||||
}
|
||||
const fn = new Function(...fnArgNames.concat(fnBody));
|
||||
const fn = newTrustedFunctionForJIT(...fnArgNames.concat(fnBody));
|
||||
return this.executeFunction(fn, fnArgValues);
|
||||
}
|
||||
|
||||
|
135
packages/compiler/src/output/output_jit_trusted_types.ts
Normal file
135
packages/compiler/src/output/output_jit_trusted_types.ts
Normal 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.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview
|
||||
* A module to facilitate use of a Trusted Types policy within the JIT
|
||||
* compiler. It lazily constructs the Trusted Types policy, providing helper
|
||||
* utilities for promoting strings to Trusted Types. When Trusted Types are not
|
||||
* available, strings are used as a fallback.
|
||||
* @security All use of this module is security-sensitive and should go through
|
||||
* security review.
|
||||
*/
|
||||
|
||||
import {global} from '../util';
|
||||
|
||||
/**
|
||||
* While Angular only uses Trusted Types internally for the time being,
|
||||
* references to Trusted Types could leak into our core.d.ts, which would force
|
||||
* anyone compiling against @angular/core to provide the @types/trusted-types
|
||||
* package in their compilation unit.
|
||||
*
|
||||
* Until https://github.com/microsoft/TypeScript/issues/30024 is resolved, we
|
||||
* will keep Angular's public API surface free of references to Trusted Types.
|
||||
* For internal and semi-private APIs that need to reference Trusted Types, the
|
||||
* minimal type definitions for the Trusted Types API provided by this module
|
||||
* should be used instead.
|
||||
*
|
||||
* Adapted from
|
||||
* https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/trusted-types/index.d.ts
|
||||
* but restricted to the API surface used within Angular.
|
||||
*/
|
||||
|
||||
export type TrustedScript = {
|
||||
__brand__: 'TrustedScript'
|
||||
};
|
||||
|
||||
export interface TrustedTypePolicyFactory {
|
||||
createPolicy(policyName: string, policyOptions: {
|
||||
createScript?: (input: string) => string,
|
||||
}): TrustedTypePolicy;
|
||||
}
|
||||
|
||||
export interface TrustedTypePolicy {
|
||||
createScript(input: string): TrustedScript;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The Trusted Types policy, or null if Trusted Types are not
|
||||
* enabled/supported, or undefined if the policy has not been created yet.
|
||||
*/
|
||||
let policy: TrustedTypePolicy|null|undefined;
|
||||
|
||||
/**
|
||||
* Returns the Trusted Types policy, or null if Trusted Types are not
|
||||
* enabled/supported. The first call to this function will create the policy.
|
||||
*/
|
||||
function getPolicy(): TrustedTypePolicy|null {
|
||||
if (policy === undefined) {
|
||||
policy = null;
|
||||
if (global.trustedTypes) {
|
||||
try {
|
||||
policy =
|
||||
(global.trustedTypes as TrustedTypePolicyFactory).createPolicy('angular#unsafe-jit', {
|
||||
createScript: (s: string) => s,
|
||||
});
|
||||
} catch {
|
||||
// trustedTypes.createPolicy throws if called with a name that is
|
||||
// already registered, even in report-only mode. Until the API changes,
|
||||
// catch the error not to break the applications functionally. In such
|
||||
// cases, the code will fall back to using strings.
|
||||
}
|
||||
}
|
||||
}
|
||||
return policy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsafely promote a string to a TrustedScript, falling back to strings when
|
||||
* Trusted Types are not available.
|
||||
* @security In particular, it must be assured that the provided string will
|
||||
* never cause an XSS vulnerability if used in a context that will be
|
||||
* interpreted and executed as a script by a browser, e.g. when calling eval.
|
||||
*/
|
||||
function trustedScriptFromString(script: string): TrustedScript|string {
|
||||
return getPolicy()?.createScript(script) || script;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsafely call the Function constructor with the given string arguments. It
|
||||
* is only available in development mode, and should be stripped out of
|
||||
* production code.
|
||||
* @security This is a security-sensitive function; any use of this function
|
||||
* must go through security review. In particular, it must be assured that it
|
||||
* is only called from the JIT compiler, as use in other code can lead to XSS
|
||||
* vulnerabilities.
|
||||
*/
|
||||
export function newTrustedFunctionForJIT(...args: string[]): Function {
|
||||
if (!global.trustedTypes) {
|
||||
// In environments that don't support Trusted Types, fall back to the most
|
||||
// straightforward implementation:
|
||||
return new Function(...args);
|
||||
}
|
||||
|
||||
// Chrome currently does not support passing TrustedScript to the Function
|
||||
// constructor. The following implements the workaround proposed on the page
|
||||
// below, where the Chromium bug is also referenced:
|
||||
// https://github.com/w3c/webappsec-trusted-types/wiki/Trusted-Types-for-function-constructor
|
||||
const fnArgs = args.slice(0, -1).join(',');
|
||||
const fnBody = args.pop()!.toString();
|
||||
const body = `(function anonymous(${fnArgs}
|
||||
) { ${fnBody}
|
||||
})`;
|
||||
|
||||
// Using eval directly confuses the compiler and prevents this module from
|
||||
// being stripped out of JS binaries even if not used. The global['eval']
|
||||
// indirection fixes that.
|
||||
const fn = global['eval'](trustedScriptFromString(body) as string) as Function;
|
||||
|
||||
// To completely mimic the behavior of calling "new Function", two more
|
||||
// things need to happen:
|
||||
// 1. Stringifying the resulting function should return its source code
|
||||
fn.toString = () => body;
|
||||
// 2. When calling the resulting function, `this` should refer to `global`
|
||||
return fn.bind(global);
|
||||
|
||||
// When Trusted Types support in Function constructors is widely available,
|
||||
// the implementation of this function can be simplified to:
|
||||
// return new Function(...args.map(a => trustedScriptFromString(a)));
|
||||
}
|
@ -11,7 +11,7 @@ import {assertIndexInRange, assertLessThan, assertNotSame} from '../util/assert'
|
||||
|
||||
import {getExpressionChangedErrorDetails, throwErrorIfNoChangesMode} from './errors';
|
||||
import {LView} from './interfaces/view';
|
||||
import {getCheckNoChangesMode} from './state';
|
||||
import {isInCheckNoChangesMode} from './state';
|
||||
import {NO_CHANGE} from './tokens';
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@ export function bindingUpdated(lView: LView, bindingIndex: number, value: any):
|
||||
if (Object.is(oldValue, value)) {
|
||||
return false;
|
||||
} else {
|
||||
if (ngDevMode && getCheckNoChangesMode()) {
|
||||
if (ngDevMode && isInCheckNoChangesMode()) {
|
||||
// View engine didn't report undefined values as changed on the first checkNoChanges pass
|
||||
// (before the change detection was run).
|
||||
const oldValueToCompare = oldValue !== NO_CHANGE ? oldValue : undefined;
|
||||
|
@ -13,7 +13,7 @@ import {NgOnChangesFeatureImpl} from './features/ng_onchanges_feature';
|
||||
import {DirectiveDef} from './interfaces/definition';
|
||||
import {TNode} from './interfaces/node';
|
||||
import {FLAGS, HookData, InitPhaseState, LView, LViewFlags, PREORDER_HOOK_FLAGS, PreOrderHookFlags, TView} from './interfaces/view';
|
||||
import {getCheckNoChangesMode} from './state';
|
||||
import {isInCheckNoChangesMode} from './state';
|
||||
|
||||
|
||||
|
||||
@ -205,8 +205,8 @@ function callHooks(
|
||||
currentNodeIndex: number|null|undefined): void {
|
||||
ngDevMode &&
|
||||
assertEqual(
|
||||
getCheckNoChangesMode(), false,
|
||||
'Hooks should never be run in the check no changes mode.');
|
||||
isInCheckNoChangesMode(), false,
|
||||
'Hooks should never be run when in check no changes mode.');
|
||||
const startIndex = currentNodeIndex !== undefined ?
|
||||
(currentView[PREORDER_HOOK_FLAGS] & PreOrderHookFlags.IndexOfTheNextPreOrderHookMaskMask) :
|
||||
0;
|
||||
|
@ -8,7 +8,7 @@
|
||||
import {assertGreaterThan, assertIndexInRange} from '../../util/assert';
|
||||
import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks';
|
||||
import {FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, TView} from '../interfaces/view';
|
||||
import {getCheckNoChangesMode, getLView, getSelectedIndex, getTView, setSelectedIndex} from '../state';
|
||||
import {getLView, getSelectedIndex, getTView, isInCheckNoChangesMode, setSelectedIndex} from '../state';
|
||||
|
||||
|
||||
/**
|
||||
@ -36,7 +36,7 @@ import {getCheckNoChangesMode, getLView, getSelectedIndex, getTView, setSelected
|
||||
*/
|
||||
export function ɵɵadvance(delta: number): void {
|
||||
ngDevMode && assertGreaterThan(delta, 0, 'Can only advance forward');
|
||||
selectIndexInternal(getTView(), getLView(), getSelectedIndex() + delta, getCheckNoChangesMode());
|
||||
selectIndexInternal(getTView(), getLView(), getSelectedIndex() + delta, isInCheckNoChangesMode());
|
||||
}
|
||||
|
||||
export function selectIndexInternal(
|
||||
|
@ -33,7 +33,7 @@ import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRoo
|
||||
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view';
|
||||
import {assertNodeNotOfTypes, assertNodeOfPossibleTypes} from '../node_assert';
|
||||
import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher';
|
||||
import {enterView, getBindingsEnabled, getCheckNoChangesMode, getCurrentDirectiveIndex, getCurrentTNode, getSelectedIndex, isCurrentTNodeParent, leaveView, setBindingIndex, setBindingRootForHostBindings, setCheckNoChangesMode, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setSelectedIndex} from '../state';
|
||||
import {enterView, getBindingsEnabled, getCurrentDirectiveIndex, getCurrentTNode, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, leaveView, setBindingIndex, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setIsInCheckNoChangesMode, setSelectedIndex} from '../state';
|
||||
import {NO_CHANGE} from '../tokens';
|
||||
import {isAnimationProp, mergeHostAttrs} from '../util/attrs_utils';
|
||||
import {INTERPOLATION_DELIMITER, renderStringify, stringifyForError} from '../util/misc_utils';
|
||||
@ -383,7 +383,9 @@ export function refreshView<T>(
|
||||
const flags = lView[FLAGS];
|
||||
if ((flags & LViewFlags.Destroyed) === LViewFlags.Destroyed) return;
|
||||
enterView(lView);
|
||||
const checkNoChangesMode = getCheckNoChangesMode();
|
||||
// Check no changes mode is a dev only mode used to verify that bindings have not changed
|
||||
// since they were assigned. We do not want to execute lifecycle hooks in that mode.
|
||||
const isInCheckNoChangesPass = isInCheckNoChangesMode();
|
||||
try {
|
||||
resetPreOrderHookFlags(lView);
|
||||
|
||||
@ -397,7 +399,7 @@ export function refreshView<T>(
|
||||
|
||||
// execute pre-order hooks (OnInit, OnChanges, DoCheck)
|
||||
// PERF WARNING: do NOT extract this to a separate function without running benchmarks
|
||||
if (!checkNoChangesMode) {
|
||||
if (!isInCheckNoChangesPass) {
|
||||
if (hooksInitPhaseCompleted) {
|
||||
const preOrderCheckHooks = tView.preOrderCheckHooks;
|
||||
if (preOrderCheckHooks !== null) {
|
||||
@ -425,7 +427,7 @@ export function refreshView<T>(
|
||||
|
||||
// execute content hooks (AfterContentInit, AfterContentChecked)
|
||||
// PERF WARNING: do NOT extract this to a separate function without running benchmarks
|
||||
if (!checkNoChangesMode) {
|
||||
if (!isInCheckNoChangesPass) {
|
||||
if (hooksInitPhaseCompleted) {
|
||||
const contentCheckHooks = tView.contentCheckHooks;
|
||||
if (contentCheckHooks !== null) {
|
||||
@ -459,7 +461,7 @@ export function refreshView<T>(
|
||||
|
||||
// execute view hooks (AfterViewInit, AfterViewChecked)
|
||||
// PERF WARNING: do NOT extract this to a separate function without running benchmarks
|
||||
if (!checkNoChangesMode) {
|
||||
if (!isInCheckNoChangesPass) {
|
||||
if (hooksInitPhaseCompleted) {
|
||||
const viewCheckHooks = tView.viewCheckHooks;
|
||||
if (viewCheckHooks !== null) {
|
||||
@ -489,7 +491,7 @@ export function refreshView<T>(
|
||||
// refresh a `NgClass` binding should work. If we would reset the dirty state in the check
|
||||
// no changes cycle, the component would be not be dirty for the next update pass. This would
|
||||
// be different in production mode where the component dirty state is not reset.
|
||||
if (!checkNoChangesMode) {
|
||||
if (!isInCheckNoChangesPass) {
|
||||
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
|
||||
}
|
||||
if (lView[FLAGS] & LViewFlags.RefreshTransplantedView) {
|
||||
@ -504,7 +506,7 @@ export function refreshView<T>(
|
||||
export function renderComponentOrTemplate<T>(
|
||||
tView: TView, lView: LView, templateFn: ComponentTemplate<{}>|null, context: T) {
|
||||
const rendererFactory = lView[RENDERER_FACTORY];
|
||||
const normalExecutionPath = !getCheckNoChangesMode();
|
||||
const normalExecutionPath = !isInCheckNoChangesMode();
|
||||
const creationModeIsActive = isCreationMode(lView);
|
||||
try {
|
||||
if (normalExecutionPath && !creationModeIsActive && rendererFactory.begin) {
|
||||
@ -529,7 +531,7 @@ function executeTemplate<T>(
|
||||
if (rf & RenderFlags.Update && lView.length > HEADER_OFFSET) {
|
||||
// When we're updating, inherently select 0 so we don't
|
||||
// have to generate that instruction for most update blocks.
|
||||
selectIndexInternal(tView, lView, 0, getCheckNoChangesMode());
|
||||
selectIndexInternal(tView, lView, 0, isInCheckNoChangesMode());
|
||||
}
|
||||
templateFn(rf, context);
|
||||
} finally {
|
||||
@ -1918,11 +1920,11 @@ export function detectChangesInRootView(lView: LView): void {
|
||||
}
|
||||
|
||||
export function checkNoChangesInternal<T>(tView: TView, view: LView, context: T) {
|
||||
setCheckNoChangesMode(true);
|
||||
setIsInCheckNoChangesMode(true);
|
||||
try {
|
||||
detectChangesInternal(tView, view, context);
|
||||
} finally {
|
||||
setCheckNoChangesMode(false);
|
||||
setIsInCheckNoChangesMode(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1936,11 +1938,11 @@ export function checkNoChangesInternal<T>(tView: TView, view: LView, context: T)
|
||||
* @param lView The view which the change detection should be checked on.
|
||||
*/
|
||||
export function checkNoChangesInRootView(lView: LView): void {
|
||||
setCheckNoChangesMode(true);
|
||||
setIsInCheckNoChangesMode(true);
|
||||
try {
|
||||
detectChangesInRootView(lView);
|
||||
} finally {
|
||||
setCheckNoChangesMode(false);
|
||||
setIsInCheckNoChangesMode(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import {RendererStyleFlags2, RendererType2} from '../../render/api';
|
||||
import {TrustedHTML, TrustedScript, TrustedScriptURL} from '../../util/security/trusted_type_defs';
|
||||
import {getDocument} from './document';
|
||||
|
||||
// TODO: cleanup once the code is merged in angular/angular
|
||||
@ -80,7 +81,9 @@ export interface ProceduralRenderer3 {
|
||||
parentNode(node: RNode): RElement|null;
|
||||
nextSibling(node: RNode): RNode|null;
|
||||
|
||||
setAttribute(el: RElement, name: string, value: string, namespace?: string|null): void;
|
||||
setAttribute(
|
||||
el: RElement, name: string, value: string|TrustedHTML|TrustedScript|TrustedScriptURL,
|
||||
namespace?: string|null): void;
|
||||
removeAttribute(el: RElement, name: string, namespace?: string|null): void;
|
||||
addClass(el: RElement, name: string): void;
|
||||
removeClass(el: RElement, name: string): void;
|
||||
@ -157,9 +160,11 @@ export interface RElement extends RNode {
|
||||
classList: RDomTokenList;
|
||||
className: string;
|
||||
textContent: string|null;
|
||||
setAttribute(name: string, value: string): void;
|
||||
setAttribute(name: string, value: string|TrustedHTML|TrustedScript|TrustedScriptURL): void;
|
||||
removeAttribute(name: string): void;
|
||||
setAttributeNS(namespaceURI: string, qualifiedName: string, value: string): void;
|
||||
setAttributeNS(
|
||||
namespaceURI: string, qualifiedName: string,
|
||||
value: string|TrustedHTML|TrustedScript|TrustedScriptURL): void;
|
||||
addEventListener(type: string, listener: EventListener, useCapture?: boolean): void;
|
||||
removeEventListener(type: string, listener?: EventListener, options?: boolean): void;
|
||||
|
||||
|
@ -6,7 +6,10 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {TrustedHTML, TrustedScript, TrustedScriptURL} from '../../util/security/trusted_type_defs';
|
||||
|
||||
/**
|
||||
* Function used to sanitize the value before writing it into the renderer.
|
||||
*/
|
||||
export type SanitizerFn = (value: any, tagName?: string, propName?: string) => string;
|
||||
export type SanitizerFn = (value: any, tagName?: string, propName?: string) =>
|
||||
string|TrustedHTML|TrustedScript|TrustedScriptURL;
|
||||
|
@ -159,14 +159,17 @@ interface InstructionState {
|
||||
* In this mode, any changes in bindings will throw an ExpressionChangedAfterChecked error.
|
||||
*
|
||||
* Necessary to support ChangeDetectorRef.checkNoChanges().
|
||||
*
|
||||
* checkNoChanges Runs only in devmode=true and verifies that no unintended changes exist in
|
||||
* the change detector or its children.
|
||||
*/
|
||||
checkNoChangesMode: boolean;
|
||||
isInCheckNoChangesMode: boolean;
|
||||
}
|
||||
|
||||
export const instructionState: InstructionState = {
|
||||
lFrame: createLFrame(null),
|
||||
bindingsEnabled: true,
|
||||
checkNoChangesMode: false,
|
||||
isInCheckNoChangesMode: false,
|
||||
};
|
||||
|
||||
|
||||
@ -287,13 +290,13 @@ export function getContextLView(): LView {
|
||||
return instructionState.lFrame.contextLView;
|
||||
}
|
||||
|
||||
export function getCheckNoChangesMode(): boolean {
|
||||
export function isInCheckNoChangesMode(): boolean {
|
||||
// TODO(misko): remove this from the LView since it is ngDevMode=true mode only.
|
||||
return instructionState.checkNoChangesMode;
|
||||
return instructionState.isInCheckNoChangesMode;
|
||||
}
|
||||
|
||||
export function setCheckNoChangesMode(mode: boolean): void {
|
||||
instructionState.checkNoChangesMode = mode;
|
||||
export function setIsInCheckNoChangesMode(mode: boolean): void {
|
||||
instructionState.isInCheckNoChangesMode = mode;
|
||||
}
|
||||
|
||||
// top level variables should not be exported for performance reasons (PERF_NOTES.md)
|
||||
|
@ -7,6 +7,8 @@
|
||||
*/
|
||||
|
||||
import {isDevMode} from '../util/is_dev_mode';
|
||||
import {TrustedHTML} from '../util/security/trusted_type_defs';
|
||||
import {trustedHTMLFromString} from '../util/security/trusted_types';
|
||||
import {getInertBodyHelper, InertBodyHelper} from './inert_body';
|
||||
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
|
||||
|
||||
@ -242,7 +244,7 @@ let inertBodyHelper: InertBodyHelper;
|
||||
* Sanitizes the given unsafe, untrusted HTML fragment, and returns HTML text that is safe to add to
|
||||
* the DOM in a browser environment.
|
||||
*/
|
||||
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
|
||||
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): TrustedHTML|string {
|
||||
let inertBodyElement: HTMLElement|null = null;
|
||||
try {
|
||||
inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc);
|
||||
@ -274,7 +276,7 @@ export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string
|
||||
'WARNING: sanitizing HTML stripped some content, see http://g.co/ng/security#xss');
|
||||
}
|
||||
|
||||
return safeHtml;
|
||||
return trustedHTMLFromString(safeHtml);
|
||||
} finally {
|
||||
// In case anything goes wrong, clear out inertElement to reset the entire DOM structure.
|
||||
if (inertBodyElement) {
|
||||
|
@ -12,6 +12,7 @@ import {getLView} from '../render3/state';
|
||||
import {renderStringify} from '../render3/util/misc_utils';
|
||||
import {TrustedHTML, TrustedScript, TrustedScriptURL} from '../util/security/trusted_type_defs';
|
||||
import {trustedHTMLFromString, trustedScriptFromString, trustedScriptURLFromString} from '../util/security/trusted_types';
|
||||
import {trustedHTMLFromStringBypass, trustedScriptFromStringBypass, trustedScriptURLFromStringBypass} from '../util/security/trusted_types_bypass';
|
||||
|
||||
import {allowSanitizationBypassAndThrow, BypassType, unwrapSafeValue} from './bypass';
|
||||
import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer';
|
||||
@ -36,13 +37,13 @@ import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer';
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵsanitizeHtml(unsafeHtml: any): string {
|
||||
export function ɵɵsanitizeHtml(unsafeHtml: any): TrustedHTML|string {
|
||||
const sanitizer = getSanitizer();
|
||||
if (sanitizer) {
|
||||
return sanitizer.sanitize(SecurityContext.HTML, unsafeHtml) || '';
|
||||
return trustedHTMLFromStringBypass(sanitizer.sanitize(SecurityContext.HTML, unsafeHtml) || '');
|
||||
}
|
||||
if (allowSanitizationBypassAndThrow(unsafeHtml, BypassType.Html)) {
|
||||
return unwrapSafeValue(unsafeHtml);
|
||||
return trustedHTMLFromStringBypass(unwrapSafeValue(unsafeHtml));
|
||||
}
|
||||
return _sanitizeHtml(getDocument(), renderStringify(unsafeHtml));
|
||||
}
|
||||
@ -107,13 +108,14 @@ export function ɵɵsanitizeUrl(unsafeUrl: any): string {
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵsanitizeResourceUrl(unsafeResourceUrl: any): string {
|
||||
export function ɵɵsanitizeResourceUrl(unsafeResourceUrl: any): TrustedScriptURL|string {
|
||||
const sanitizer = getSanitizer();
|
||||
if (sanitizer) {
|
||||
return sanitizer.sanitize(SecurityContext.RESOURCE_URL, unsafeResourceUrl) || '';
|
||||
return trustedScriptURLFromStringBypass(
|
||||
sanitizer.sanitize(SecurityContext.RESOURCE_URL, unsafeResourceUrl) || '');
|
||||
}
|
||||
if (allowSanitizationBypassAndThrow(unsafeResourceUrl, BypassType.ResourceUrl)) {
|
||||
return unwrapSafeValue(unsafeResourceUrl);
|
||||
return trustedScriptURLFromStringBypass(unwrapSafeValue(unsafeResourceUrl));
|
||||
}
|
||||
throw new Error('unsafe value used in a resource URL context (see http://g.co/ng/security#xss)');
|
||||
}
|
||||
@ -130,13 +132,14 @@ export function ɵɵsanitizeResourceUrl(unsafeResourceUrl: any): string {
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵsanitizeScript(unsafeScript: any): string {
|
||||
export function ɵɵsanitizeScript(unsafeScript: any): TrustedScript|string {
|
||||
const sanitizer = getSanitizer();
|
||||
if (sanitizer) {
|
||||
return sanitizer.sanitize(SecurityContext.SCRIPT, unsafeScript) || '';
|
||||
return trustedScriptFromStringBypass(
|
||||
sanitizer.sanitize(SecurityContext.SCRIPT, unsafeScript) || '');
|
||||
}
|
||||
if (allowSanitizationBypassAndThrow(unsafeScript, BypassType.Script)) {
|
||||
return unwrapSafeValue(unsafeScript);
|
||||
return trustedScriptFromStringBypass(unwrapSafeValue(unsafeScript));
|
||||
}
|
||||
throw new Error('unsafe value used in a script context');
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ export function initNgDevMode(): boolean {
|
||||
if (typeof ngDevMode !== 'object') {
|
||||
ngDevModeResetPerfCounters();
|
||||
}
|
||||
return !!ngDevMode;
|
||||
return typeof ngDevMode !== 'undefined' && !!ngDevMode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
89
packages/core/src/util/security/trusted_types_bypass.ts
Normal file
89
packages/core/src/util/security/trusted_types_bypass.ts
Normal file
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview
|
||||
* A module to facilitate use of a Trusted Types policy internally within
|
||||
* Angular specifically for bypassSecurityTrust* and custom sanitizers. It
|
||||
* lazily constructs the Trusted Types policy, providing helper utilities for
|
||||
* promoting strings to Trusted Types. When Trusted Types are not available,
|
||||
* strings are used as a fallback.
|
||||
* @security All use of this module is security-sensitive and should go through
|
||||
* security review.
|
||||
*/
|
||||
|
||||
import {global} from '../global';
|
||||
import {TrustedHTML, TrustedScript, TrustedScriptURL, TrustedTypePolicy, TrustedTypePolicyFactory} from './trusted_type_defs';
|
||||
|
||||
/**
|
||||
* The Trusted Types policy, or null if Trusted Types are not
|
||||
* enabled/supported, or undefined if the policy has not been created yet.
|
||||
*/
|
||||
let policy: TrustedTypePolicy|null|undefined;
|
||||
|
||||
/**
|
||||
* Returns the Trusted Types policy, or null if Trusted Types are not
|
||||
* enabled/supported. The first call to this function will create the policy.
|
||||
*/
|
||||
function getPolicy(): TrustedTypePolicy|null {
|
||||
if (policy === undefined) {
|
||||
policy = null;
|
||||
if (global.trustedTypes) {
|
||||
try {
|
||||
policy = (global.trustedTypes as TrustedTypePolicyFactory)
|
||||
.createPolicy('angular#unsafe-bypass', {
|
||||
createHTML: (s: string) => s,
|
||||
createScript: (s: string) => s,
|
||||
createScriptURL: (s: string) => s,
|
||||
});
|
||||
} catch {
|
||||
// trustedTypes.createPolicy throws if called with a name that is
|
||||
// already registered, even in report-only mode. Until the API changes,
|
||||
// catch the error not to break the applications functionally. In such
|
||||
// cases, the code will fall back to using strings.
|
||||
}
|
||||
}
|
||||
}
|
||||
return policy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsafely promote a string to a TrustedHTML, falling back to strings when
|
||||
* Trusted Types are not available.
|
||||
* @security This is a security-sensitive function; any use of this function
|
||||
* must go through security review. In particular, it must be assured that it
|
||||
* is only passed strings that come directly from custom sanitizers or the
|
||||
* bypassSecurityTrust* functions.
|
||||
*/
|
||||
export function trustedHTMLFromStringBypass(html: string): TrustedHTML|string {
|
||||
return getPolicy()?.createHTML(html) || html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsafely promote a string to a TrustedScript, falling back to strings when
|
||||
* Trusted Types are not available.
|
||||
* @security This is a security-sensitive function; any use of this function
|
||||
* must go through security review. In particular, it must be assured that it
|
||||
* is only passed strings that come directly from custom sanitizers or the
|
||||
* bypassSecurityTrust* functions.
|
||||
*/
|
||||
export function trustedScriptFromStringBypass(script: string): TrustedScript|string {
|
||||
return getPolicy()?.createScript(script) || script;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsafely promote a string to a TrustedScriptURL, falling back to strings
|
||||
* when Trusted Types are not available.
|
||||
* @security This is a security-sensitive function; any use of this function
|
||||
* must go through security review. In particular, it must be assured that it
|
||||
* is only passed strings that come directly from custom sanitizers or the
|
||||
* bypassSecurityTrust* functions.
|
||||
*/
|
||||
export function trustedScriptURLFromStringBypass(url: string): TrustedScriptURL|string {
|
||||
return getPolicy()?.createScriptURL(url) || url;
|
||||
}
|
@ -156,7 +156,7 @@
|
||||
"name": "generatePropertyAliases"
|
||||
},
|
||||
{
|
||||
"name": "getCheckNoChangesMode"
|
||||
"name": "isInCheckNoChangesMode"
|
||||
},
|
||||
{
|
||||
"name": "getClosureSafeProperty"
|
||||
|
@ -948,7 +948,7 @@
|
||||
"name": "generatePropertyAliases"
|
||||
},
|
||||
{
|
||||
"name": "getCheckNoChangesMode"
|
||||
"name": "isInCheckNoChangesMode"
|
||||
},
|
||||
{
|
||||
"name": "getClosureSafeProperty"
|
||||
@ -1485,7 +1485,7 @@
|
||||
"name": "setBindingRootForHostBindings"
|
||||
},
|
||||
{
|
||||
"name": "setCheckNoChangesMode"
|
||||
"name": "setIsInCheckNoChangesMode"
|
||||
},
|
||||
{
|
||||
"name": "setCurrentDirectiveIndex"
|
||||
|
@ -108,7 +108,7 @@
|
||||
"name": "extractPipeDef"
|
||||
},
|
||||
{
|
||||
"name": "getCheckNoChangesMode"
|
||||
"name": "isInCheckNoChangesMode"
|
||||
},
|
||||
{
|
||||
"name": "getClosureSafeProperty"
|
||||
|
@ -1260,7 +1260,7 @@
|
||||
"name": "getBootstrapListener"
|
||||
},
|
||||
{
|
||||
"name": "getCheckNoChangesMode"
|
||||
"name": "isInCheckNoChangesMode"
|
||||
},
|
||||
{
|
||||
"name": "getClosureSafeProperty"
|
||||
@ -1818,7 +1818,7 @@
|
||||
"name": "setBindingRootForHostBindings"
|
||||
},
|
||||
{
|
||||
"name": "setCheckNoChangesMode"
|
||||
"name": "setIsInCheckNoChangesMode"
|
||||
},
|
||||
{
|
||||
"name": "setCurrentDirectiveIndex"
|
||||
|
@ -330,7 +330,7 @@
|
||||
"name": "generatePropertyAliases"
|
||||
},
|
||||
{
|
||||
"name": "getCheckNoChangesMode"
|
||||
"name": "isInCheckNoChangesMode"
|
||||
},
|
||||
{
|
||||
"name": "getClosureSafeProperty"
|
||||
@ -642,7 +642,7 @@
|
||||
"name": "setBindingRootForHostBindings"
|
||||
},
|
||||
{
|
||||
"name": "setCheckNoChangesMode"
|
||||
"name": "setIsInCheckNoChangesMode"
|
||||
},
|
||||
{
|
||||
"name": "setCurrentDirectiveIndex"
|
||||
|
@ -11,6 +11,10 @@ import {browserDetection} from '@angular/platform-browser/testing/src/browser_ut
|
||||
import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
|
||||
import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
|
||||
function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
|
||||
return _sanitizeHtml(defaultDoc, unsafeHtmlInput).toString();
|
||||
}
|
||||
|
||||
{
|
||||
describe('HTML sanitizer', () => {
|
||||
let defaultDoc: any;
|
||||
@ -29,73 +33,73 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
});
|
||||
|
||||
it('serializes nested structures', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>'))
|
||||
.toEqual('<div alt="x"><p>a</p>b<b>c<a alt="more">d</a></b>e</div>');
|
||||
expect(logMsgs).toEqual([]);
|
||||
});
|
||||
|
||||
it('serializes self closing elements', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<p>Hello <br> World</p>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<p>Hello <br> World</p>'))
|
||||
.toEqual('<p>Hello <br> World</p>');
|
||||
});
|
||||
|
||||
it('supports namespaced elements', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, 'a<my:hr/><my:div>b</my:div>c')).toEqual('abc');
|
||||
expect(sanitizeHtml(defaultDoc, 'a<my:hr/><my:div>b</my:div>c')).toEqual('abc');
|
||||
});
|
||||
|
||||
it('supports namespaced attributes', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<a xlink:href="something">t</a>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<a xlink:href="something">t</a>'))
|
||||
.toEqual('<a xlink:href="something">t</a>');
|
||||
expect(_sanitizeHtml(defaultDoc, '<a xlink:evil="something">t</a>')).toEqual('<a>t</a>');
|
||||
expect(_sanitizeHtml(defaultDoc, '<a xlink:href="javascript:foo()">t</a>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<a xlink:evil="something">t</a>')).toEqual('<a>t</a>');
|
||||
expect(sanitizeHtml(defaultDoc, '<a xlink:href="javascript:foo()">t</a>'))
|
||||
.toEqual('<a xlink:href="unsafe:javascript:foo()">t</a>');
|
||||
});
|
||||
|
||||
it('supports HTML5 elements', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<main><summary>Works</summary></main>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<main><summary>Works</summary></main>'))
|
||||
.toEqual('<main><summary>Works</summary></main>');
|
||||
});
|
||||
|
||||
it('supports ARIA attributes', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<h1 role="presentation" aria-haspopup="true">Test</h1>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<h1 role="presentation" aria-haspopup="true">Test</h1>'))
|
||||
.toEqual('<h1 role="presentation" aria-haspopup="true">Test</h1>');
|
||||
expect(_sanitizeHtml(defaultDoc, '<i aria-label="Info">Info</i>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<i aria-label="Info">Info</i>'))
|
||||
.toEqual('<i aria-label="Info">Info</i>');
|
||||
expect(_sanitizeHtml(defaultDoc, '<img src="pteranodon.jpg" aria-details="details">'))
|
||||
expect(sanitizeHtml(defaultDoc, '<img src="pteranodon.jpg" aria-details="details">'))
|
||||
.toEqual('<img src="pteranodon.jpg" aria-details="details">');
|
||||
});
|
||||
|
||||
it('sanitizes srcset attributes', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<img srcset="/foo.png 400px, javascript:evil() 23px">'))
|
||||
expect(sanitizeHtml(defaultDoc, '<img srcset="/foo.png 400px, javascript:evil() 23px">'))
|
||||
.toEqual('<img srcset="/foo.png 400px, unsafe:javascript:evil() 23px">');
|
||||
});
|
||||
|
||||
it('supports sanitizing plain text', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, 'Hello, World')).toEqual('Hello, World');
|
||||
expect(sanitizeHtml(defaultDoc, 'Hello, World')).toEqual('Hello, World');
|
||||
});
|
||||
|
||||
it('ignores non-element, non-attribute nodes', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<!-- comments? -->no.')).toEqual('no.');
|
||||
expect(_sanitizeHtml(defaultDoc, '<?pi nodes?>no.')).toEqual('no.');
|
||||
expect(sanitizeHtml(defaultDoc, '<!-- comments? -->no.')).toEqual('no.');
|
||||
expect(sanitizeHtml(defaultDoc, '<?pi nodes?>no.')).toEqual('no.');
|
||||
expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/);
|
||||
});
|
||||
|
||||
it('supports sanitizing escaped entities', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '🚀')).toEqual('🚀');
|
||||
expect(sanitizeHtml(defaultDoc, '🚀')).toEqual('🚀');
|
||||
expect(logMsgs).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not warn when just re-encoding text', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<p>Hellö Wörld</p>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<p>Hellö Wörld</p>'))
|
||||
.toEqual('<p>Hellö Wörld</p>');
|
||||
expect(logMsgs).toEqual([]);
|
||||
});
|
||||
|
||||
it('escapes entities', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<p>Hello < World</p>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<p>Hello < World</p>'))
|
||||
.toEqual('<p>Hello < World</p>');
|
||||
expect(_sanitizeHtml(defaultDoc, '<p>Hello < World</p>')).toEqual('<p>Hello < World</p>');
|
||||
expect(_sanitizeHtml(defaultDoc, '<p alt="% & " !">Hello</p>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<p>Hello < World</p>')).toEqual('<p>Hello < World</p>');
|
||||
expect(sanitizeHtml(defaultDoc, '<p alt="% & " !">Hello</p>'))
|
||||
.toEqual('<p alt="% & " !">Hello</p>'); // NB: quote encoded as ASCII ".
|
||||
});
|
||||
|
||||
@ -110,7 +114,7 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
];
|
||||
for (const tag of dangerousTags) {
|
||||
it(tag, () => {
|
||||
expect(_sanitizeHtml(defaultDoc, `<${tag}>evil!</${tag}>`)).toEqual('evil!');
|
||||
expect(sanitizeHtml(defaultDoc, `<${tag}>evil!</${tag}>`)).toEqual('evil!');
|
||||
});
|
||||
}
|
||||
|
||||
@ -125,7 +129,7 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
];
|
||||
for (const tag of dangerousSelfClosingTags) {
|
||||
it(tag, () => {
|
||||
expect(_sanitizeHtml(defaultDoc, `before<${tag}>After`)).toEqual('beforeAfter');
|
||||
expect(sanitizeHtml(defaultDoc, `before<${tag}>After`)).toEqual('beforeAfter');
|
||||
});
|
||||
}
|
||||
|
||||
@ -136,7 +140,7 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
];
|
||||
for (const tag of dangerousSkipContentTags) {
|
||||
it(tag, () => {
|
||||
expect(_sanitizeHtml(defaultDoc, `<${tag}>evil!</${tag}>`)).toEqual('');
|
||||
expect(sanitizeHtml(defaultDoc, `<${tag}>evil!</${tag}>`)).toEqual('');
|
||||
});
|
||||
}
|
||||
|
||||
@ -144,7 +148,7 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
// `<frame>` is special, because different browsers treat it differently (e.g. remove it
|
||||
// altogether). // We just verify that (one way or another), there is no `<frame>` element
|
||||
// after sanitization.
|
||||
expect(_sanitizeHtml(defaultDoc, `<frame>evil!</frame>`)).not.toContain('<frame>');
|
||||
expect(sanitizeHtml(defaultDoc, `<frame>evil!</frame>`)).not.toContain('<frame>');
|
||||
});
|
||||
});
|
||||
|
||||
@ -153,45 +157,45 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
|
||||
for (const attr of dangerousAttrs) {
|
||||
it(`${attr}`, () => {
|
||||
expect(_sanitizeHtml(defaultDoc, `<a ${attr}="x">evil!</a>`)).toEqual('<a>evil!</a>');
|
||||
expect(sanitizeHtml(defaultDoc, `<a ${attr}="x">evil!</a>`)).toEqual('<a>evil!</a>');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('ignores content of script elements', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<script>var foo="<p>bar</p>"</script>')).toEqual('');
|
||||
expect(_sanitizeHtml(defaultDoc, '<script>var foo="<p>bar</p>"</script><div>hi</div>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<script>var foo="<p>bar</p>"</script>')).toEqual('');
|
||||
expect(sanitizeHtml(defaultDoc, '<script>var foo="<p>bar</p>"</script><div>hi</div>'))
|
||||
.toEqual('<div>hi</div>');
|
||||
expect(_sanitizeHtml(defaultDoc, '<style>\<\!-- something--\>hi</style>')).toEqual('');
|
||||
expect(sanitizeHtml(defaultDoc, '<style>\<\!-- something--\>hi</style>')).toEqual('');
|
||||
});
|
||||
|
||||
it('ignores content of style elements', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<style><!-- foobar --></style><div>hi</div>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<style><!-- foobar --></style><div>hi</div>'))
|
||||
.toEqual('<div>hi</div>');
|
||||
expect(_sanitizeHtml(defaultDoc, '<style><!-- foobar --></style>')).toEqual('');
|
||||
expect(_sanitizeHtml(defaultDoc, '<style>\<\!-- something--\>hi</style>')).toEqual('');
|
||||
expect(sanitizeHtml(defaultDoc, '<style><!-- foobar --></style>')).toEqual('');
|
||||
expect(sanitizeHtml(defaultDoc, '<style>\<\!-- something--\>hi</style>')).toEqual('');
|
||||
expect(logMsgs.join('\n')).toMatch(/sanitizing HTML stripped some content/);
|
||||
});
|
||||
|
||||
it('should strip unclosed iframe tag', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '<iframe>')).toEqual('');
|
||||
expect(sanitizeHtml(defaultDoc, '<iframe>')).toEqual('');
|
||||
expect([
|
||||
'<iframe>',
|
||||
// Double-escaped on IE
|
||||
'&lt;iframe&gt;'
|
||||
]).toContain(_sanitizeHtml(defaultDoc, '<iframe><iframe>'));
|
||||
]).toContain(sanitizeHtml(defaultDoc, '<iframe><iframe>'));
|
||||
expect([
|
||||
'<script>evil();</script>',
|
||||
// Double-escaped on IE
|
||||
'&lt;script&gt;evil();&lt;/script&gt;'
|
||||
]).toContain(_sanitizeHtml(defaultDoc, '<iframe><script>evil();</script>'));
|
||||
]).toContain(sanitizeHtml(defaultDoc, '<iframe><script>evil();</script>'));
|
||||
});
|
||||
|
||||
it('should ignore extraneous body tags', () => {
|
||||
expect(_sanitizeHtml(defaultDoc, '</body>')).toEqual('');
|
||||
expect(_sanitizeHtml(defaultDoc, 'foo</body>bar')).toEqual('foobar');
|
||||
expect(_sanitizeHtml(defaultDoc, 'foo<body>bar')).toEqual('foobar');
|
||||
expect(_sanitizeHtml(defaultDoc, 'fo<body>ob</body>ar')).toEqual('foobar');
|
||||
expect(sanitizeHtml(defaultDoc, '</body>')).toEqual('');
|
||||
expect(sanitizeHtml(defaultDoc, 'foo</body>bar')).toEqual('foobar');
|
||||
expect(sanitizeHtml(defaultDoc, 'foo<body>bar')).toEqual('foobar');
|
||||
expect(sanitizeHtml(defaultDoc, 'fo<body>ob</body>ar')).toEqual('foobar');
|
||||
});
|
||||
|
||||
it('should not enter an infinite loop on clobbered elements', () => {
|
||||
@ -200,18 +204,17 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
// Anyway what we want to test is that browsers do not enter an infinite loop which would
|
||||
// result in a timeout error for the test.
|
||||
try {
|
||||
_sanitizeHtml(defaultDoc, '<form><input name="parentNode" /></form>');
|
||||
sanitizeHtml(defaultDoc, '<form><input name="parentNode" /></form>');
|
||||
} catch (e) {
|
||||
// depending on the browser, we might ge an exception
|
||||
}
|
||||
try {
|
||||
_sanitizeHtml(defaultDoc, '<form><input name="nextSibling" /></form>');
|
||||
sanitizeHtml(defaultDoc, '<form><input name="nextSibling" /></form>');
|
||||
} catch (e) {
|
||||
// depending on the browser, we might ge an exception
|
||||
}
|
||||
try {
|
||||
_sanitizeHtml(
|
||||
defaultDoc, '<form><div><div><input name="nextSibling" /></div></div></form>');
|
||||
sanitizeHtml(defaultDoc, '<form><div><div><input name="nextSibling" /></div></div></form>');
|
||||
} catch (e) {
|
||||
// depending on the browser, we might ge an exception
|
||||
}
|
||||
@ -220,7 +223,7 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
// See
|
||||
// https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
|
||||
it('should not allow JavaScript execution when creating inert document', () => {
|
||||
const output = _sanitizeHtml(defaultDoc, '<svg><g onload="window.xxx = 100"></g></svg>');
|
||||
const output = sanitizeHtml(defaultDoc, '<svg><g onload="window.xxx = 100"></g></svg>');
|
||||
const window = defaultDoc.defaultView;
|
||||
if (window) {
|
||||
expect(window.xxx).toBe(undefined);
|
||||
@ -232,7 +235,7 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
// See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
|
||||
it('should not allow JavaScript hidden in badly formed HTML to get through sanitization (Firefox bug)',
|
||||
() => {
|
||||
expect(_sanitizeHtml(
|
||||
expect(sanitizeHtml(
|
||||
defaultDoc, '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">'))
|
||||
.toEqual(
|
||||
isDOMParserAvailable() ?
|
||||
@ -245,7 +248,7 @@ import {isDOMParserAvailable} from '../../src/sanitization/inert_body';
|
||||
if (browserDetection.isWebkit) {
|
||||
it('should prevent mXSS attacks', function() {
|
||||
// In Chrome Canary 62, the ideographic space character is kept as a stringified HTML entity
|
||||
expect(_sanitizeHtml(defaultDoc, '<a href=" javascript:alert(1)">CLICKME</a>'))
|
||||
expect(sanitizeHtml(defaultDoc, '<a href=" javascript:alert(1)">CLICKME</a>'))
|
||||
.toMatch(/<a href="unsafe:( )?javascript:alert\(1\)">CLICKME<\/a>/);
|
||||
});
|
||||
}
|
||||
|
@ -29,15 +29,15 @@ describe('sanitization', () => {
|
||||
}
|
||||
}
|
||||
it('should sanitize html', () => {
|
||||
expect(ɵɵsanitizeHtml('<div></div>')).toEqual('<div></div>');
|
||||
expect(ɵɵsanitizeHtml(new Wrap('<div></div>'))).toEqual('<div></div>');
|
||||
expect(ɵɵsanitizeHtml('<img src="javascript:true">'))
|
||||
expect(ɵɵsanitizeHtml('<div></div>').toString()).toEqual('<div></div>');
|
||||
expect(ɵɵsanitizeHtml(new Wrap('<div></div>')).toString()).toEqual('<div></div>');
|
||||
expect(ɵɵsanitizeHtml('<img src="javascript:true">').toString())
|
||||
.toEqual('<img src="unsafe:javascript:true">');
|
||||
expect(ɵɵsanitizeHtml(new Wrap('<img src="javascript:true">')))
|
||||
expect(ɵɵsanitizeHtml(new Wrap('<img src="javascript:true">')).toString())
|
||||
.toEqual('<img src="unsafe:javascript:true">');
|
||||
expect(() => ɵɵsanitizeHtml(bypassSanitizationTrustUrl('<img src="javascript:true">')))
|
||||
.toThrowError(/Required a safe HTML, got a URL/);
|
||||
expect(ɵɵsanitizeHtml(bypassSanitizationTrustHtml('<img src="javascript:true">')))
|
||||
expect(ɵɵsanitizeHtml(bypassSanitizationTrustHtml('<img src="javascript:true">')).toString())
|
||||
.toEqual('<img src="javascript:true">');
|
||||
});
|
||||
|
||||
@ -57,7 +57,7 @@ describe('sanitization', () => {
|
||||
expect(() => ɵɵsanitizeResourceUrl('javascript:true')).toThrowError(ERROR);
|
||||
expect(() => ɵɵsanitizeResourceUrl(bypassSanitizationTrustHtml('javascript:true')))
|
||||
.toThrowError(/Required a safe ResourceURL, got a HTML/);
|
||||
expect(ɵɵsanitizeResourceUrl(bypassSanitizationTrustResourceUrl('javascript:true')))
|
||||
expect(ɵɵsanitizeResourceUrl(bypassSanitizationTrustResourceUrl('javascript:true')).toString())
|
||||
.toEqual('javascript:true');
|
||||
});
|
||||
|
||||
@ -78,7 +78,7 @@ describe('sanitization', () => {
|
||||
expect(() => ɵɵsanitizeScript('true')).toThrowError(ERROR);
|
||||
expect(() => ɵɵsanitizeScript(bypassSanitizationTrustHtml('true')))
|
||||
.toThrowError(/Required a safe Script, got a HTML/);
|
||||
expect(ɵɵsanitizeScript(bypassSanitizationTrustScript('true'))).toEqual('true');
|
||||
expect(ɵɵsanitizeScript(bypassSanitizationTrustScript('true')).toString()).toEqual('true');
|
||||
});
|
||||
|
||||
it('should select correct sanitizer for URL props', () => {
|
||||
@ -114,7 +114,8 @@ describe('sanitization', () => {
|
||||
bypassSanitizationTrustHtml('javascript:true'), 'iframe', 'src'))
|
||||
.toThrowError(/Required a safe ResourceURL, got a HTML/);
|
||||
expect(ɵɵsanitizeUrlOrResourceUrl(
|
||||
bypassSanitizationTrustResourceUrl('javascript:true'), 'iframe', 'src'))
|
||||
bypassSanitizationTrustResourceUrl('javascript:true'), 'iframe', 'src')
|
||||
.toString())
|
||||
.toEqual('javascript:true');
|
||||
});
|
||||
|
||||
|
@ -6,16 +6,17 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, TmplAstNode, TmplAstTextAttribute} from '@angular/compiler';
|
||||
import {AST, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstTextAttribute} from '@angular/compiler';
|
||||
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
||||
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ShimLocation, Symbol, SymbolKind, TemplateSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {findNodeAtPosition} from './hybrid_visitor';
|
||||
import {getPathToNodeAtPosition} from './hybrid_visitor';
|
||||
import {flatMap, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTextSpanOfNode, isDollarEvent, TemplateInfo, toTextSpan} from './utils';
|
||||
|
||||
interface DefinitionMeta {
|
||||
node: AST|TmplAstNode;
|
||||
path: Array<AST|TmplAstNode>;
|
||||
symbol: Symbol;
|
||||
}
|
||||
|
||||
@ -41,12 +42,12 @@ export class DefinitionBuilder {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const definitions = this.getDefinitionsForSymbol(definitionMeta.symbol, definitionMeta.node);
|
||||
const definitions = this.getDefinitionsForSymbol({...definitionMeta, ...templateInfo});
|
||||
return {definitions, textSpan: getTextSpanOfNode(definitionMeta.node)};
|
||||
}
|
||||
|
||||
private getDefinitionsForSymbol(symbol: Symbol, node: TmplAstNode|AST):
|
||||
readonly ts.DefinitionInfo[]|undefined {
|
||||
private getDefinitionsForSymbol({symbol, node, path, component}: DefinitionMeta&
|
||||
TemplateInfo): readonly ts.DefinitionInfo[]|undefined {
|
||||
switch (symbol.kind) {
|
||||
case SymbolKind.Directive:
|
||||
case SymbolKind.Element:
|
||||
@ -58,9 +59,14 @@ export class DefinitionBuilder {
|
||||
// LS users to "go to definition" on an item in the template that maps to a class and be
|
||||
// taken to the directive or HTML class.
|
||||
return this.getTypeDefinitionsForTemplateInstance(symbol, node);
|
||||
case SymbolKind.Input:
|
||||
case SymbolKind.Output:
|
||||
return this.getDefinitionsForSymbols(...symbol.bindings);
|
||||
case SymbolKind.Input: {
|
||||
const bindingDefs = this.getDefinitionsForSymbols(...symbol.bindings);
|
||||
// Also attempt to get directive matches for the input name. If there is a directive that
|
||||
// has the input name as part of the selector, we want to return that as well.
|
||||
const directiveDefs = this.getDirectiveTypeDefsForBindingNode(node, path, component);
|
||||
return [...bindingDefs, ...directiveDefs];
|
||||
}
|
||||
case SymbolKind.Variable:
|
||||
case SymbolKind.Reference: {
|
||||
const definitions: ts.DefinitionInfo[] = [];
|
||||
@ -112,8 +118,14 @@ export class DefinitionBuilder {
|
||||
case SymbolKind.Template:
|
||||
return this.getTypeDefinitionsForTemplateInstance(symbol, node);
|
||||
case SymbolKind.Output:
|
||||
case SymbolKind.Input:
|
||||
return this.getTypeDefinitionsForSymbols(...symbol.bindings);
|
||||
case SymbolKind.Input: {
|
||||
const bindingDefs = this.getTypeDefinitionsForSymbols(...symbol.bindings);
|
||||
// Also attempt to get directive matches for the input name. If there is a directive that
|
||||
// has the input name as part of the selector, we want to return that as well.
|
||||
const directiveDefs = this.getDirectiveTypeDefsForBindingNode(
|
||||
node, definitionMeta.path, templateInfo.component);
|
||||
return [...bindingDefs, ...directiveDefs];
|
||||
}
|
||||
case SymbolKind.Reference:
|
||||
case SymbolKind.Expression:
|
||||
case SymbolKind.Variable:
|
||||
@ -150,6 +162,28 @@ export class DefinitionBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
private getDirectiveTypeDefsForBindingNode(
|
||||
node: TmplAstNode|AST, pathToNode: Array<TmplAstNode|AST>, component: ts.ClassDeclaration) {
|
||||
if (!(node instanceof TmplAstBoundAttribute) && !(node instanceof TmplAstTextAttribute) &&
|
||||
!(node instanceof TmplAstBoundEvent)) {
|
||||
return [];
|
||||
}
|
||||
const parent = pathToNode[pathToNode.length - 2];
|
||||
if (!(parent instanceof TmplAstTemplate || parent instanceof TmplAstElement)) {
|
||||
return [];
|
||||
}
|
||||
const templateOrElementSymbol =
|
||||
this.compiler.getTemplateTypeChecker().getSymbolOfNode(parent, component);
|
||||
if (templateOrElementSymbol === null ||
|
||||
(templateOrElementSymbol.kind !== SymbolKind.Template &&
|
||||
templateOrElementSymbol.kind !== SymbolKind.Element)) {
|
||||
return [];
|
||||
}
|
||||
const dirs =
|
||||
getDirectiveMatchesForAttribute(node.name, parent, templateOrElementSymbol.directives);
|
||||
return this.getTypeDefinitionsForSymbols(...dirs);
|
||||
}
|
||||
|
||||
private getTypeDefinitionsForSymbols(...symbols: HasShimLocation[]): ts.DefinitionInfo[] {
|
||||
return flatMap(symbols, ({shimLocation}) => {
|
||||
const {shimPath, positionInShimFile} = shimLocation;
|
||||
@ -159,15 +193,16 @@ export class DefinitionBuilder {
|
||||
|
||||
private getDefinitionMetaAtPosition({template, component}: TemplateInfo, position: number):
|
||||
DefinitionMeta|undefined {
|
||||
const node = findNodeAtPosition(template, position);
|
||||
if (node === undefined) {
|
||||
const path = getPathToNodeAtPosition(template, position);
|
||||
if (path === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = path[path.length - 1];
|
||||
const symbol = this.compiler.getTemplateTypeChecker().getSymbolOfNode(node, component);
|
||||
if (symbol === null) {
|
||||
return;
|
||||
}
|
||||
return {node, symbol};
|
||||
return {node, path, symbol};
|
||||
}
|
||||
}
|
||||
|
@ -13,12 +13,14 @@ import * as t from '@angular/compiler/src/render3/r3_ast'; // t for temp
|
||||
import {isTemplateNode, isTemplateNodeWithKeyAndValue} from './utils';
|
||||
|
||||
/**
|
||||
* Return the template AST node or expression AST node that most accurately
|
||||
* Return the path to the template AST node or expression AST node that most accurately
|
||||
* represents the node at the specified cursor `position`.
|
||||
*
|
||||
* @param ast AST tree
|
||||
* @param position cursor position
|
||||
*/
|
||||
export function findNodeAtPosition(ast: t.Node[], position: number): t.Node|e.AST|undefined {
|
||||
export function getPathToNodeAtPosition(ast: t.Node[], position: number): Array<t.Node|e.AST>|
|
||||
undefined {
|
||||
const visitor = new R3Visitor(position);
|
||||
visitor.visitAll(ast);
|
||||
const candidate = visitor.path[visitor.path.length - 1];
|
||||
@ -35,7 +37,22 @@ export function findNodeAtPosition(ast: t.Node[], position: number): t.Node|e.AS
|
||||
return;
|
||||
}
|
||||
}
|
||||
return candidate;
|
||||
return visitor.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the template AST node or expression AST node that most accurately
|
||||
* represents the node at the specified cursor `position`.
|
||||
*
|
||||
* @param ast AST tree
|
||||
* @param position cursor position
|
||||
*/
|
||||
export function findNodeAtPosition(ast: t.Node[], position: number): t.Node|e.AST|undefined {
|
||||
const path = getPathToNodeAtPosition(ast, position);
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
return path[path.length - 1];
|
||||
}
|
||||
|
||||
class R3Visitor implements t.Visitor {
|
||||
|
@ -88,11 +88,14 @@ describe('definitions', () => {
|
||||
templateOverride: `<div *ng¦If="anyValue"></div>`,
|
||||
expectedSpanText: 'ngIf',
|
||||
});
|
||||
expect(definitions!.length).toEqual(1);
|
||||
// Because the input is also part of the selector, the directive is also returned.
|
||||
expect(definitions!.length).toEqual(2);
|
||||
const [inputDef, directiveDef] = definitions;
|
||||
|
||||
const [def] = definitions;
|
||||
expect(def.textSpan).toEqual('ngIf');
|
||||
expect(def.contextSpan).toEqual('set ngIf(condition: T);');
|
||||
expect(inputDef.textSpan).toEqual('ngIf');
|
||||
expect(inputDef.contextSpan).toEqual('set ngIf(condition: T);');
|
||||
expect(directiveDef.textSpan).toEqual('NgIf');
|
||||
expect(directiveDef.contextSpan).toContain('export declare class NgIf');
|
||||
});
|
||||
|
||||
it('should work for directives with compound selectors', () => {
|
||||
@ -125,6 +128,18 @@ describe('definitions', () => {
|
||||
expect(def.contextSpan).toEqual(`@Input('tcName') name = 'test';`);
|
||||
});
|
||||
|
||||
it('should work for text inputs', () => {
|
||||
const definitions = getDefinitionsAndAssertBoundSpan({
|
||||
templateOverride: `<test-comp tcN¦ame="name"></test-comp>`,
|
||||
expectedSpanText: 'tcName="name"',
|
||||
});
|
||||
expect(definitions!.length).toEqual(1);
|
||||
|
||||
const [def] = definitions;
|
||||
expect(def.textSpan).toEqual('name');
|
||||
expect(def.contextSpan).toEqual(`@Input('tcName') name = 'test';`);
|
||||
});
|
||||
|
||||
it('should work for structural directive inputs ngForTrackBy', () => {
|
||||
const definitions = getDefinitionsAndAssertBoundSpan({
|
||||
templateOverride: `<div *ngFor="let item of heroes; tr¦ackBy: test;"></div>`,
|
||||
@ -145,12 +160,16 @@ describe('definitions', () => {
|
||||
templateOverride: `<div *ngFor="let item o¦f heroes"></div>`,
|
||||
expectedSpanText: 'of',
|
||||
});
|
||||
expect(definitions!.length).toEqual(1);
|
||||
// Because the input is also part of the selector ([ngFor][ngForOf]), the directive is also
|
||||
// returned.
|
||||
expect(definitions!.length).toEqual(2);
|
||||
const [inputDef, directiveDef] = definitions;
|
||||
|
||||
const [def] = definitions;
|
||||
expect(def.textSpan).toEqual('ngForOf');
|
||||
expect(def.contextSpan)
|
||||
expect(inputDef.textSpan).toEqual('ngForOf');
|
||||
expect(inputDef.contextSpan)
|
||||
.toEqual('set ngForOf(ngForOf: U & NgIterable<T> | undefined | null);');
|
||||
expect(directiveDef.textSpan).toEqual('NgForOf');
|
||||
expect(directiveDef.contextSpan).toContain('export declare class NgForOf');
|
||||
});
|
||||
|
||||
it('should work for two-way binding providers', () => {
|
||||
@ -191,6 +210,20 @@ describe('definitions', () => {
|
||||
const definitionAndBoundSpan = ngLS.getDefinitionAndBoundSpan(APP_COMPONENT, position);
|
||||
expect(definitionAndBoundSpan).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the directive when the event is part of the selector', () => {
|
||||
const definitions = getDefinitionsAndAssertBoundSpan({
|
||||
templateOverride: `<div (eventSelect¦or)="title = ''"></div>`,
|
||||
expectedSpanText: `(eventSelector)="title = ''"`,
|
||||
});
|
||||
expect(definitions!.length).toEqual(2);
|
||||
|
||||
const [inputDef, directiveDef] = definitions;
|
||||
expect(inputDef.textSpan).toEqual('eventSelector');
|
||||
expect(inputDef.contextSpan).toEqual('@Output() eventSelector = new EventEmitter<void>();');
|
||||
expect(directiveDef.textSpan).toEqual('EventSelectorDirective');
|
||||
expect(directiveDef.contextSpan).toContain('export class EventSelectorDirective');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -14,6 +14,10 @@ import {HumanizedDefinitionInfo, humanizeDefinitionInfo} from './test_utils';
|
||||
describe('type definitions', () => {
|
||||
const {project, service, tsLS} = setup();
|
||||
const ngLS = new LanguageService(project, tsLS);
|
||||
const possibleArrayDefFiles = new Set([
|
||||
'lib.es5.d.ts', 'lib.es2015.core.d.ts', 'lib.es2015.iterable.d.ts',
|
||||
'lib.es2015.symbol.wellknown.d.ts', 'lib.es2016.array.include.d.ts'
|
||||
]);
|
||||
|
||||
beforeEach(() => {
|
||||
service.reset();
|
||||
@ -121,7 +125,11 @@ describe('type definitions', () => {
|
||||
const definitions = getTypeDefinitionsAndAssertBoundSpan({
|
||||
templateOverride: `<div *ngFor="let item o¦f heroes"></div>`,
|
||||
});
|
||||
expectAllArrayDefinitions(definitions);
|
||||
// In addition to all the array defs, this will also return the NgForOf def because the
|
||||
// input is part of the selector ([ngFor][ngForOf]).
|
||||
expectAllDefinitions(
|
||||
definitions, new Set(['Array', 'NgForOf']),
|
||||
new Set([...possibleArrayDefFiles, 'ng_for_of.d.ts']));
|
||||
});
|
||||
|
||||
it('should return nothing for two-way binding providers', () => {
|
||||
@ -141,10 +149,25 @@ describe('type definitions', () => {
|
||||
});
|
||||
expect(definitions!.length).toEqual(2);
|
||||
|
||||
const [def, xyz] = definitions;
|
||||
expect(def.textSpan).toEqual('EventEmitter');
|
||||
expect(def.contextSpan).toContain('export interface EventEmitter<T> extends Subject<T>');
|
||||
expect(xyz.textSpan).toEqual('EventEmitter');
|
||||
const [emitterInterface, emitterConst] = definitions;
|
||||
expect(emitterInterface.textSpan).toEqual('EventEmitter');
|
||||
expect(emitterInterface.contextSpan)
|
||||
.toContain('export interface EventEmitter<T> extends Subject<T>');
|
||||
expect(emitterInterface.fileName).toContain('event_emitter.d.ts');
|
||||
expect(emitterConst.textSpan).toEqual('EventEmitter');
|
||||
expect(emitterConst.contextSpan).toContain('export declare const EventEmitter');
|
||||
expect(emitterConst.fileName).toContain('event_emitter.d.ts');
|
||||
});
|
||||
|
||||
it('should return the directive when the event is part of the selector', () => {
|
||||
const definitions = getTypeDefinitionsAndAssertBoundSpan({
|
||||
templateOverride: `<div (eventSelect¦or)="title = ''"></div>`,
|
||||
});
|
||||
expect(definitions!.length).toEqual(3);
|
||||
|
||||
// EventEmitter tested in previous test
|
||||
const directiveDef = definitions[2];
|
||||
expect(directiveDef.contextSpan).toContain('export class EventSelectorDirective');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -264,21 +287,21 @@ describe('type definitions', () => {
|
||||
const definitions = getTypeDefinitionsAndAssertBoundSpan({
|
||||
templateOverride: `<div *ngFor="let item of heroes as her¦oes2; trackBy: test;"></div>`,
|
||||
});
|
||||
expectAllArrayDefinitions(definitions);
|
||||
expectAllDefinitions(definitions, new Set(['Array']), possibleArrayDefFiles);
|
||||
});
|
||||
|
||||
it('should work for uses of members in structural directives', () => {
|
||||
const definitions = getTypeDefinitionsAndAssertBoundSpan({
|
||||
templateOverride: `<div *ngFor="let item of heroes as heroes2">{{her¦oes2}}</div>`,
|
||||
});
|
||||
expectAllArrayDefinitions(definitions);
|
||||
expectAllDefinitions(definitions, new Set(['Array']), possibleArrayDefFiles);
|
||||
});
|
||||
|
||||
it('should work for members in structural directives', () => {
|
||||
const definitions = getTypeDefinitionsAndAssertBoundSpan({
|
||||
templateOverride: `<div *ngFor="let item of her¦oes; trackBy: test;"></div>`,
|
||||
});
|
||||
expectAllArrayDefinitions(definitions);
|
||||
expectAllDefinitions(definitions, new Set(['Array']), possibleArrayDefFiles);
|
||||
});
|
||||
|
||||
it('should return nothing for the $any() cast function', () => {
|
||||
@ -297,14 +320,12 @@ describe('type definitions', () => {
|
||||
return defs!.map(d => humanizeDefinitionInfo(d, service));
|
||||
}
|
||||
|
||||
function expectAllArrayDefinitions(definitions: HumanizedDefinitionInfo[]) {
|
||||
function expectAllDefinitions(
|
||||
definitions: HumanizedDefinitionInfo[], textSpans: Set<string>,
|
||||
possibleFileNames: Set<string>) {
|
||||
expect(definitions!.length).toBeGreaterThan(0);
|
||||
const actualTextSpans = new Set(definitions.map(d => d.textSpan));
|
||||
expect(actualTextSpans).toEqual(new Set(['Array']));
|
||||
const possibleFileNames = [
|
||||
'lib.es5.d.ts', 'lib.es2015.core.d.ts', 'lib.es2015.iterable.d.ts',
|
||||
'lib.es2015.symbol.wellknown.d.ts', 'lib.es2016.array.include.d.ts'
|
||||
];
|
||||
expect(actualTextSpans).toEqual(textSpans);
|
||||
for (const def of definitions) {
|
||||
const fileName = def.fileName.split('/').slice(-1)[0];
|
||||
expect(possibleFileNames)
|
||||
|
@ -143,9 +143,13 @@ function getInlineTemplateInfoAtPosition(
|
||||
/**
|
||||
* Given an attribute node, converts it to string form.
|
||||
*/
|
||||
function toAttributeString(attribute: t.TextAttribute|t.BoundAttribute): string {
|
||||
function toAttributeString(attribute: t.TextAttribute|t.BoundAttribute|t.BoundEvent): string {
|
||||
if (attribute instanceof t.BoundEvent) {
|
||||
return `[${attribute.name}]`;
|
||||
} else {
|
||||
return `[${attribute.name}=${attribute.valueSpan?.toString() ?? ''}]`;
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeName(node: t.Template|t.Element): string {
|
||||
return node instanceof t.Template ? node.tagName : node.name;
|
||||
@ -154,8 +158,10 @@ function getNodeName(node: t.Template|t.Element): string {
|
||||
/**
|
||||
* Given a template or element node, returns all attributes on the node.
|
||||
*/
|
||||
function getAttributes(node: t.Template|t.Element): Array<t.TextAttribute|t.BoundAttribute> {
|
||||
const attributes: Array<t.TextAttribute|t.BoundAttribute> = [...node.attributes, ...node.inputs];
|
||||
function getAttributes(node: t.Template|
|
||||
t.Element): Array<t.TextAttribute|t.BoundAttribute|t.BoundEvent> {
|
||||
const attributes: Array<t.TextAttribute|t.BoundAttribute|t.BoundEvent> =
|
||||
[...node.attributes, ...node.inputs, ...node.outputs];
|
||||
if (node instanceof t.Template) {
|
||||
attributes.push(...node.templateAttrs);
|
||||
}
|
||||
@ -216,8 +222,8 @@ export function getDirectiveMatchesForAttribute(
|
||||
const allDirectiveMatches =
|
||||
getDirectiveMatchesForSelector(directives, getNodeName(hostNode) + allAttrs.join(''));
|
||||
const attrsExcludingName = attributes.filter(a => a.name !== name).map(toAttributeString);
|
||||
const matchesWithoutAttr =
|
||||
getDirectiveMatchesForSelector(directives, attrsExcludingName.join(''));
|
||||
const matchesWithoutAttr = getDirectiveMatchesForSelector(
|
||||
directives, getNodeName(hostNode) + attrsExcludingName.join(''));
|
||||
return difference(allDirectiveMatches, matchesWithoutAttr);
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ import * as ParsingCases from './parsing-cases';
|
||||
ParsingCases.TestPipe,
|
||||
ParsingCases.WithContextDirective,
|
||||
ParsingCases.CompoundCustomButtonDirective,
|
||||
ParsingCases.EventSelectorDirective,
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
|
@ -75,6 +75,11 @@ export class CompoundCustomButtonDirective {
|
||||
@Input() config?: {color?: string};
|
||||
}
|
||||
|
||||
@Directive({selector: '[eventSelector]'})
|
||||
export class EventSelectorDirective {
|
||||
@Output() eventSelector = new EventEmitter<void>();
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'prefixPipe',
|
||||
})
|
||||
|
@ -162,7 +162,7 @@ export class DomSanitizerImpl extends DomSanitizer {
|
||||
if (allowSanitizationBypassOrThrow(value, BypassType.Html)) {
|
||||
return unwrapSafeValue(value);
|
||||
}
|
||||
return _sanitizeHtml(this._doc, String(value));
|
||||
return _sanitizeHtml(this._doc, String(value)).toString();
|
||||
case SecurityContext.STYLE:
|
||||
if (allowSanitizationBypassOrThrow(value, BypassType.Style)) {
|
||||
return unwrapSafeValue(value);
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **zone.js:** jest getRealSystemTime should return native time ([#39127](https://github.com/angular/angular/issues/39127)) ([ffc3332](https://github.com/angular/angular/commit/ffc3332))
|
||||
* **zone.js:** add missing types field in package.json ([#38585](https://github.com/angular/angular/issues/38585)) ([27cc56b](https://github.com/angular/angular/commit/27cc56b)), closes [#38584](https://github.com/angular/angular/issues/38584)
|
||||
* **zone.js:** defineProperty patch should not swallow error ([#37582](https://github.com/angular/angular/issues/37582)) ([45a73dd](https://github.com/angular/angular/commit/45a73dd)), closes [#37432](https://github.com/angular/angular/issues/37432)
|
||||
* **zone.js:** run tests in umd format ([#37582](https://github.com/angular/angular/issues/37582)) ([40096be](https://github.com/angular/angular/commit/40096be))
|
||||
@ -17,6 +18,16 @@
|
||||
* **zone.js:** add jest fakeTimers support ([#39016](https://github.com/angular/angular/issues/39016)) ([82d54fe](https://github.com/angular/angular/commit/82d54fe)), closes [#38851](https://github.com/angular/angular/issues/38851)
|
||||
|
||||
|
||||
### Refactor
|
||||
|
||||
* **zone.js:** refactor(zone.js): rename several internal apis in fake async zone spec ([#39127](https://github.com/angular/angular/issues/39127)) ([8a68669](https://github.com/angular/angular/commit/8a68669))
|
||||
|
||||
|
||||
### Build
|
||||
|
||||
* **zone.js:** build(zone.js): zone.js should output esm format for fesm2015 bundles ([#39203](https://github.com/angular/angular/issues/39203)) ([822b838](https://github.com/angular/angular/commit/822b838))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **zone.js:** ZoneJS no longer swallows errors produced by `Object.defineProperty` calls.
|
||||
|
Reference in New Issue
Block a user