From 3de36e3a2b7bda9185458a8fe12ac5b6bb6b4433 Mon Sep 17 00:00:00 2001 From: Ika Date: Sun, 30 Dec 2018 23:03:42 +0800 Subject: [PATCH] fix(ng,vue): add parens to avoid unexpected `}}` in interpolations (#5657) --- src/common/fast-path.js | 23 +- src/language-html/printer-html.js | 20 +- src/language-js/needs-parens.js | 61 + .../__snapshots__/jsfmt.spec.js.snap | 2229 +++++++++++++++++ .../html_angular/interpolation.component.html | 10 + tests/html_angular/jsfmt.spec.js | 1 + 6 files changed, 2331 insertions(+), 13 deletions(-) diff --git a/src/common/fast-path.js b/src/common/fast-path.js index eb678a9a..4c152b2f 100644 --- a/src/common/fast-path.js +++ b/src/common/fast-path.js @@ -29,17 +29,18 @@ FastPath.prototype.getValue = function getValue() { }; function getNodeHelper(path, count) { - const s = path.stack; - - for (let i = s.length - 1; i >= 0; i -= 2) { - const value = s[i]; + const stackIndex = getNodeStackIndexHelper(path.stack, count); + return stackIndex === -1 ? null : path.stack[stackIndex]; +} +function getNodeStackIndexHelper(stack, count) { + for (let i = stack.length - 1; i >= 0; i -= 2) { + const value = stack[i]; if (value && !Array.isArray(value) && --count < 0) { - return value; + return i; } } - - return null; + return -1; } FastPath.prototype.getNode = function getNode(count) { @@ -70,6 +71,14 @@ FastPath.prototype.call = function call(callback /*, name1, name2, ... */) { return result; }; +FastPath.prototype.callParent = function callParent(callback, count) { + const stackIndex = getNodeStackIndexHelper(this.stack, ~~count + 1); + const parentValues = this.stack.splice(stackIndex + 1); + const result = callback(this); + Array.prototype.push.apply(this.stack, parentValues); + return result; +}; + // Similar to FastPath.prototype.call, except that the value obtained by // accessing this.getValue()[name1][name2]... should be array-like. The // callback will be called with a reference to this path object for each diff --git a/src/language-html/printer-html.js b/src/language-html/printer-html.js index c9bcada1..896b54a3 100644 --- a/src/language-html/printer-html.js +++ b/src/language-html/printer-html.js @@ -85,11 +85,16 @@ function embed(path, print, textToDoc, options) { line, textToDoc( node.value, - options.parser === "angular" - ? { parser: "__ng_interpolation", trailingComma: "none" } - : options.parser === "vue" - ? { parser: "__vue_expression" } - : { parser: "__js_expression" } + Object.assign( + { + __isInHtmlInterpolation: true // to avoid unexpected `}}` + }, + options.parser === "angular" + ? { parser: "__ng_interpolation", trailingComma: "none" } + : options.parser === "vue" + ? { parser: "__vue_expression" } + : { parser: "__js_expression" } + ) ) ]) ), @@ -1043,7 +1048,10 @@ function printEmbeddedAttributeValue(node, originalTextToDoc, options) { indent( concat([ line, - ngTextToDoc(part, { parser: "__ng_interpolation" }) + ngTextToDoc(part, { + parser: "__ng_interpolation", + __isInHtmlInterpolation: true // to avoid unexpected `}}` + }) ]) ), line, diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js index 68a92cce..402fee36 100644 --- a/src/language-js/needs-parens.js +++ b/src/language-js/needs-parens.js @@ -57,6 +57,16 @@ function needsParens(path, options) { return false; } + // to avoid unexpected `}}` in HTML interpolations + if ( + options.__isInHtmlInterpolation && + !options.bracketSpacing && + endsWithRightBracket(node) && + isFollowedByRightBracket(path) + ) { + return true; + } + // Only statements don't need parentheses. if (isStatement(node)) { return false; @@ -688,4 +698,55 @@ function isStatement(node) { ); } +function endsWithRightBracket(node) { + switch (node.type) { + case "ObjectExpression": + return true; + default: + return false; + } +} + +function isFollowedByRightBracket(path) { + const node = path.getValue(); + const parent = path.getParentNode(); + const name = path.getName(); + switch (parent.type) { + case "NGPipeExpression": + if ( + typeof name === "number" && + parent.arguments[name] === node && + parent.arguments.length - 1 === name + ) { + return path.callParent(isFollowedByRightBracket); + } + break; + case "ObjectProperty": + if (name === "value") { + const parentParent = path.getParentNode(1); + return ( + parentParent.properties[parentParent.properties.length - 1] === parent + ); + } + break; + case "BinaryExpression": + case "LogicalExpression": + if (name === "right") { + return path.callParent(isFollowedByRightBracket); + } + break; + case "ConditionalExpression": + if (name === "alternate") { + return path.callParent(isFollowedByRightBracket); + } + break; + case "UnaryExpression": + if (parent.prefix) { + return path.callParent(isFollowedByRightBracket); + } + break; + } + return false; +} + module.exports = needsParens; diff --git a/tests/html_angular/__snapshots__/jsfmt.spec.js.snap b/tests/html_angular/__snapshots__/jsfmt.spec.js.snap index 2bfac28a..ec5d2dd2 100644 --- a/tests/html_angular/__snapshots__/jsfmt.spec.js.snap +++ b/tests/html_angular/__snapshots__/jsfmt.spec.js.snap @@ -61,6 +61,21 @@ printWidth: 80 ================================================================================ `; +exports[`attr-name.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== +
+ +=====================================output===================================== +
+ +================================================================================ +`; + exports[`attributes.component.html 1`] = ` ====================================options===================================== parsers: ["angular"] @@ -1229,6 +1244,240 @@ printWidth: 80 ================================================================================ `; +exports[`attributes.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== +
+ +=====================================output===================================== +
+ +================================================================================ +`; + exports[`first-lf.component.html 1`] = ` ====================================options===================================== parsers: ["angular"] @@ -1679,6 +1928,99 @@ printWidth: 80 ================================================================================ `; +exports[`first-lf.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== + + + + + + + + + + + + + + + + + + + + + +=====================================output===================================== + + + + + + + + + + + + + + + + + + + + + +================================================================================ +`; + exports[`ignore-attribute.component.html 1`] = ` ====================================options===================================== parsers: ["angular"] @@ -1937,6 +2279,65 @@ printWidth: 80 ================================================================================ `; +exports[`ignore-attribute.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== +
+ +
+ +
+ +
+ +=====================================output===================================== +
+ +
+ +
+ +
+ +================================================================================ +`; + exports[`interpolation.component.html 1`] = ` ====================================options===================================== parsers: ["angular"] @@ -1999,6 +2400,16 @@ printWidth: 80 {{ aNormalValue | aPipe }}: {{ aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} +

+ {{ + 'delete' + | translate: {what: ('entities' | translate: {count: array.length})} + }} +

+

{{ {a:1+{} } }}

+

{{ {a:a==={} } }}

+

{{ {a:!{} } }}

+

{{ {a:a?b:{} } }}

=====================================output=====================================
{{ a | b: c }}
@@ -2077,6 +2488,16 @@ printWidth: 80 aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} +

+ {{ + "delete" + | translate: { what: "entities" | translate: { count: array.length } } + }} +

+

{{ { a: 1 + {} } }}

+

{{ { a: a === {} } }}

+

{{ { a: !{} } }}

+

{{ { a: a ? b : {} } }}

================================================================================ `; @@ -2144,6 +2565,16 @@ trailingComma: "es5" {{ aNormalValue | aPipe }}: {{ aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} +

+ {{ + 'delete' + | translate: {what: ('entities' | translate: {count: array.length})} + }} +

+

{{ {a:1+{} } }}

+

{{ {a:a==={} } }}

+

{{ {a:!{} } }}

+

{{ {a:a?b:{} } }}

=====================================output=====================================
{{ a | b: c }}
@@ -2222,6 +2653,16 @@ trailingComma: "es5" aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} +

+ {{ + "delete" + | translate: { what: "entities" | translate: { count: array.length } } + }} +

+

{{ { a: 1 + {} } }}

+

{{ { a: a === {} } }}

+

{{ { a: !{} } }}

+

{{ { a: a ? b : {} } }}

================================================================================ `; @@ -2288,6 +2729,16 @@ printWidth: 1 {{ aNormalValue | aPipe }}: {{ aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} +

+ {{ + 'delete' + | translate: {what: ('entities' | translate: {count: array.length})} + }} +

+

{{ {a:1+{} } }}

+

{{ {a:a==={} } }}

+

{{ {a:!{} } }}

+

{{ {a:a?b:{} } }}

=====================================output=====================================
@@ -2528,6 +2979,55 @@ printWidth: 1 }} +

+ {{ + "delete" + | translate + : { + what: + "entities" + | translate + : { + count: + array.length + } + } + }} +

+

+ {{ + { + a: + 1 + + {} + } + }} +

+

+ {{ + { + a: + a === + {} + } + }} +

+

+ {{ + { + a: !{} + } + }} +

+

+ {{ + { + a: a + ? b + : {} + } + }} +

================================================================================ `; @@ -2595,6 +3095,16 @@ printWidth: 80 {{ aNormalValue | aPipe }}: {{ aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} +

+ {{ + 'delete' + | translate: {what: ('entities' | translate: {count: array.length})} + }} +

+

{{ {a:1+{} } }}

+

{{ {a:a==={} } }}

+

{{ {a:!{} } }}

+

{{ {a:a?b:{} } }}

=====================================output=====================================
{{ a | b: c }}
@@ -2678,6 +3188,181 @@ printWidth: 80 {{ aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} +

+ {{ + "delete" + | translate: { what: "entities" | translate: { count: array.length } } + }} +

+

{{ { a: 1 + {} } }}

+

{{ { a: a === {} } }}

+

{{ { a: !{} } }}

+

{{ { a: a ? b : {} } }}

+ +================================================================================ +`; + +exports[`interpolation.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== +
{{ a | b : c }}
+
{{ 0 - 1 }}
+
{{ - 1 }}
+
{{ a ? 1 : 2 }}
+
{{ a ( 1 ) ( 2 ) }}
+
{{ a [ b ] }}
+
{{ [ 1 ] }}
+
{{ { 'a' : 1 } }}
+
{{ { a : 1 } }}
+
{{ true }}
+
{{ undefined }}
+
{{ null }}
+
{{ ( 1 ) }}
+
{{ 1 }}
+
{{ 'hello' }}
+
{{ a ( 1 , 2 ) }}
+
{{ a . b ( 1 , 2 ) }}
+
{{ x ! }}
+
{{ ! x }}
+
{{ ( ( a ) ) }}
+
{{ a }}
+
{{ a // hello }}
+
{{ a . b }}
+
{{ a ?. b ( ) }}
+
{{ a ?. b }}
+
{{ a // hello }}
+ + + + + +
{{copyTypes[options.copyType]}}
+{{listRow.NextScheduledSendStatus == 1 || listRow.NextScheduledSendStatus == 2 || listRow.NextScheduledSendStatus == 3}} +{{a}}{{b}} + + {{ aNormalValue | aPipe }}: + {{ aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} + +

+ {{ + 'delete' + | translate: {what: ('entities' | translate: {count: array.length})} + }} +

+

{{ {a:1+{} } }}

+

{{ {a:a==={} } }}

+

{{ {a:!{} } }}

+

{{ {a:a?b:{} } }}

+ +=====================================output===================================== +
{{ a | b: c }}
+
{{ 0 - 1 }}
+
{{ -1 }}
+
{{ a ? 1 : 2 }}
+
{{ a(1)(2) }}
+
{{ a[b] }}
+
{{ [1] }}
+
{{ {a: 1} }}
+
{{ {a: 1} }}
+
{{ true }}
+
{{ undefined }}
+
{{ null }}
+
{{ 1 }}
+
{{ 1 }}
+
{{ "hello" }}
+
{{ a(1, 2) }}
+
{{ a.b(1, 2) }}
+
{{ x! }}
+
{{ !x }}
+
{{ a }}
+
{{ a }}
+
{{ a // hello }}
+
{{ a.b }}
+
{{ a?.b() }}
+
{{ a?.b }}
+
{{ a // hello }}
+ + + + + +
+ {{ copyTypes[options.copyType] }} +
+{{ + listRow.NextScheduledSendStatus == 1 || + listRow.NextScheduledSendStatus == 2 || + listRow.NextScheduledSendStatus == 3 +}} +{{ a }}{{ b }} + + {{ aNormalValue | aPipe }}: + {{ + aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis + }} + +

+ {{ + "delete" + | translate: {what: "entities" | translate: ({count: array.length})} + }} +

+

{{ {a: 1 + ({})} }}

+

{{ {a: a === ({})} }}

+

{{ {a: !({})} }}

+

{{ {a: a ? b : ({})} }}

================================================================================ `; @@ -10809,6 +11494,1535 @@ can be found in the LICENSE file at http://angular.io/license ================================================================================ `; +exports[`real-world.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== + + + +

Template Syntax

+Interpolation
+Expression context
+Statement context
+Mental Model
+Buttons
+Properties vs. Attributes
+
+Property Binding
+
+ Attribute Binding
+ Class Binding
+ Style Binding
+
+
+Event Binding
+Two-way Binding
+
+
Directives
+
+ NgModel (two-way) Binding
+ NgClass Binding
+ NgStyle Binding
+ NgIf
+ NgFor
+
+ NgFor with index
+ NgFor with trackBy
+
+ NgSwitch
+
+
+Template reference variables
+Inputs and outputs
+Pipes
+Safe navigation operator ?.
+Non-null assertion operator !.
+Enums
+ + +

Interpolation

+ +

My current hero is {{currentHero.name}}

+ +

+ {{title}} + +

+ + +

The sum of 1 + 1 is {{1 + 1}}

+ + +

The sum of 1 + 1 is not {{1 + 1 + getVal()}}

+ +top + +

Expression context

+ +

Component expression context ({{title}}, [hidden]="isUnchanged")

+
+ {{title}} + changed +
+ + +

Template input variable expression context (let hero)

+ + +
{{hero.name}}
+
+ +

Template reference variable expression context (#heroInput)

+
+ Type something: + {{heroInput.value}} +
+ +top + +

Statement context

+ +

Component statement context ( (click)="onSave() ) +

+ +
+ +

Template $event statement context

+
+ +
+ +

Template input variable statement context (let hero)

+ +
+ +
+ +

Template reference variable statement context (#heroForm)

+
+
...
+
+ +top + + +

New Mental Model

+ + + +
Mental Model
+ + +

+ +
+ +
Mental Model
+ + +
+

+ +
+ + +
+

+ +
+ + +
+
+

+ + + +
click me
+{{clicked}} +

+ +
+ Hero Name: + + {{name}} +
+

+ + +

+ +
Special
+

+ + + +top + + +

Property vs. Attribute (img examples)

+ + + +

+ + + + + +top + + +

Buttons

+ + + + +

+ + +

+ + + +top + + +

Property Binding

+ + + +
[ngClass] binding to the classes property
+ + + + +
+ +
+ + +

is the interpolated image.

+

is the property bound image.

+ +

"{{title}}" is the interpolated title.

+

"" is the property bound title.

+ + +

"{{evilTitle}}" is the interpolated evil title.

+

"" is the property bound evil title.

+ +top + + +

Attribute Binding

+ + + + + + + + + +
One-Two
FiveSix
+ +
+ + +

+ + +
+ + + + + + + +
+ +top + + +

Class Binding

+ + +
Bad curly special
+ + +
Bad curly
+ + +
The class binding is special
+ + +
This one is not so special
+ +
This class binding is special too
+ +top + + +

Style Binding

+ + + + + + + +top + + +

Event Binding

+ + + + + +
+ +
click with myClick
+{{clickMessage}} +
+ + + + +
+ + + + +
Click me +
Click me too!
+
+ + +
+ +
+ + +
+ +
+ +top + +

Two-way Binding

+
+ +
Resizable Text
+ +
+
+
+

De-sugared two-way binding

+ +
+ +top + + +

NgModel (two-way) Binding

+ +

Result: {{currentHero.name}}

+ + +without NgModel +
+ +[(ngModel)] +
+ +bindon-ngModel +
+ +(ngModelChange)="...name=$event" +
+ +(ngModelChange)="setUppercaseName($event)" + +top + + +

NgClass Binding

+ +

currentClasses is {{currentClasses | json}}

+
This div is initially saveable, unchanged, and special
+ + +
+ | + | + + +

+
+ This div should be {{ canSave ? "": "not"}} saveable, + {{ isUnchanged ? "unchanged" : "modified" }} and, + {{ isSpecial ? "": "not"}} special after clicking "Refresh".
+

+ +
This div is special
+ +
Bad curly special
+
Curly special
+ +top + + +

NgStyle Binding

+ +
+ This div is x-large or smaller. +
+ +

[ngStyle] binding to currentStyles - CSS property names

+

currentStyles is {{currentStyles | json}}

+
+ This div is initially italic, normal weight, and extra large (24px). +
+ + +
+ | + | + + +

+
+ This div should be {{ canSave ? "italic": "plain"}}, + {{ isUnchanged ? "normal weight" : "bold" }} and, + {{ isSpecial ? "extra large": "normal size"}} after clicking "Refresh".
+ +top + + +

NgIf Binding

+ + + +
Hello, {{currentHero.name}}
+
Hello, {{nullHero.name}}
+ + + +Add {{currentHero.name}} with template + + +
Hero Detail removed from DOM (via template) because isActive is false
+ + + + + +
Show with class
+
Hide with class
+ + + + +
Show with style
+
Hide with style
+ +top + + +

NgFor Binding

+ +
+
{{hero.name}}
+
+
+ +
+ + +
+ +top + +

*ngFor with index

+

with semi-colon separator

+
+
{{i + 1}} - {{hero.name}}
+
+ +

with comma separator

+
+ +
{{i + 1}} - {{hero.name}}
+
+ +top + +

*ngFor trackBy

+ + + + +

without trackBy

+
+
({{hero.id}}) {{hero.name}}
+ +
+ Hero DOM elements change #{{heroesNoTrackByCount}} without trackBy +
+
+ +

with trackBy

+
+
({{hero.id}}) {{hero.name}}
+ +
+ Hero DOM elements change #{{heroesWithTrackByCount}} with trackBy +
+
+ +


+ +

with trackBy and semi-colon separator

+
+
+ ({{hero.id}}) {{hero.name}} +
+
+ +

with trackBy and comma separator

+
+
({{hero.id}}) {{hero.name}}
+
+ +

with trackBy and space separator

+
+
({{hero.id}}) {{hero.name}}
+
+ +

with generic trackById function

+
+
({{hero.id}}) {{hero.name}}
+
+ +top + + +

NgSwitch Binding

+ +

Pick your favorite hero

+
+ +
+ +
+ + + +
Are you as confused as {{currentHero.name}}?
+ +
+ +top + + +

Template reference variables

+ + + + + + + + + + + + + + +

Example Form

+ + +top + + +

Inputs and Outputs

+ + + + + + + +
myClick2
+{{clickMessage2}} + +top + + +

Pipes

+ +
Title through uppercase pipe: {{title | uppercase}}
+ + +
+ Title through a pipe chain: + {{title | uppercase | lowercase}} +
+ + +
Birthdate: {{currentHero?.birthdate | date:'longDate'}}
+ +
{{currentHero | json}}
+ +
Birthdate: {{(currentHero?.birthdate | date:'longDate') | uppercase}}
+ +
+ + {{product.price | currency:'USD':true}} +
+ +top + + +

Safe navigation operator ?.

+ +
+ The title is {{title}} +
+ +
+ The current hero's name is {{currentHero?.name}} +
+ +
+ The current hero's name is {{currentHero.name}} +
+ + + + + +
The null hero's name is {{nullHero.name}}
+ +
+The null hero's name is {{nullHero && nullHero.name}} +
+ +
+ + The null hero's name is {{nullHero?.name}} +
+ + +top + + +

Non-null assertion operator !.

+ +
+ +
+ The hero's name is {{hero!.name}} +
+
+ +top + + +

$any type cast function $any( ).

+ +
+ +
+ The hero's marker is {{$any(hero).marker}} +
+
+ +
+ +
+ Undeclared members is {{$any(this).member}} +
+
+ +top + + + +

Enums in binding

+ +

+ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ The current color is {{Color[color]}} and its number is {{color}}.
+ +

+ +top + + + + +
+
+
+ +
+ +
+
+ {{submitMessage}} +
+
+ + + + +=====================================output===================================== + + + +

Template Syntax

+Interpolation
+Expression context
+Statement context
+Mental Model
+Buttons
+Properties vs. Attributes
+
+Property Binding
+
+ Attribute Binding
+ Class Binding
+ Style Binding
+
+
+Event Binding
+Two-way Binding
+
+
Directives
+
+ NgModel (two-way) Binding
+ NgClass Binding
+ NgStyle Binding
+ NgIf
+ NgFor
+
+ NgFor with index
+ NgFor with trackBy
+
+ NgSwitch
+
+
+Template reference variables
+Inputs and outputs
+Pipes
+Safe navigation operator ?.
+Non-null assertion operator !.
+Enums
+ + +
+

Interpolation

+ +

My current hero is {{ currentHero.name }}

+ +

+ {{ title }} + +

+ + +

The sum of 1 + 1 is {{ 1 + 1 }}

+ + +

The sum of 1 + 1 is not {{ 1 + 1 + getVal() }}

+ +top + +
+

Expression context

+ +

+ Component expression context ({{title}}, + [hidden]="isUnchanged") +

+
+ {{ title }} + changed +
+ +

Template input variable expression context (let hero)

+ + +
{{ hero.name }}
+
+ +

Template reference variable expression context (#heroInput)

+
+ Type something: + {{ heroInput.value }} +
+ +top + +
+

Statement context

+ +

Component statement context ( (click)="onSave() )

+
+ +
+ +

Template $event statement context

+
+ +
+ +

Template input variable statement context (let hero)

+ +
+ +
+ +

Template reference variable statement context (#heroForm)

+
+
...
+
+ +top + + +
+

New Mental Model

+ + + +
Mental Model
+ + +

+ +
+ +
Mental Model
+ + +
+

+ +
+ + +
+

+ +
+ + +
+
+

+ + + +
click me
+{{ clicked }} +

+ +
+ Hero Name: + + {{ name }} +
+

+ + +

+ +
Special
+

+ + + +top + + +
+

Property vs. Attribute (img examples)

+ + + +

+ + + + + +top + + +
+

Buttons

+ + + + +

+ + +

+ + + +top + + +
+

Property Binding

+ + + +
[ngClass] binding to the classes property
+ + + + +
+ +
+ + +

is the interpolated image.

+

is the property bound image.

+ +

+ "{{ title }}" is the interpolated title. +

+

"" is the property bound title.

+ + +

+ "{{ evilTitle }}" is the interpolated evil title. +

+

+ "" is the property bound evil + title. +

+ +top + + +
+

Attribute Binding

+ + + + + + + + + + + + + + +
One-Two
FiveSix
+ +
+ + +

+ + +
+ + + + + + + +
+ +top + + +
+

Class Binding

+ + +
Bad curly special
+ + +
Bad curly
+ + +
The class binding is special
+ + +
+ This one is not so special +
+ +
This class binding is special too
+ +top + + +
+

Style Binding

+ + + + + + + +top + + +
+

Event Binding

+ + + + + +
+ +
click with myClick
+ {{ clickMessage }} +
+ + + +
+ + + + +
+ Click me +
Click me too!
+
+ + +
+ +
+ + +
+ +
+ +top + +
+

Two-way Binding

+
+ +
Resizable Text
+ +
+
+
+

De-sugared two-way binding

+ +
+ +top + + +
+

NgModel (two-way) Binding

+ +

Result: {{ currentHero.name }}

+ + +without NgModel +
+ +[(ngModel)] +
+ +bindon-ngModel +
+ +(ngModelChange)="...name=$event" +
+ +(ngModelChange)="setUppercaseName($event)" + +top + + +
+

NgClass Binding

+ +

currentClasses is {{ currentClasses | json }}

+
+ This div is initially saveable, unchanged, and special +
+ + +
+ | + +| + + +

+
+ This div should be {{ canSave ? "" : "not" }} saveable, + {{ isUnchanged ? "unchanged" : "modified" }} and, + {{ isSpecial ? "" : "not" }} special after clicking "Refresh". +
+

+ +
This div is special
+ +
Bad curly special
+
Curly special
+ +top + + +
+

NgStyle Binding

+ +
+ This div is x-large or smaller. +
+ +

[ngStyle] binding to currentStyles - CSS property names

+

currentStyles is {{ currentStyles | json }}

+
+ This div is initially italic, normal weight, and extra large (24px). +
+ + +
+ | + | + + +

+
+ This div should be {{ canSave ? "italic" : "plain" }}, + {{ isUnchanged ? "normal weight" : "bold" }} and, + {{ isSpecial ? "extra large" : "normal size" }} after clicking "Refresh". +
+ +top + + +
+

NgIf Binding

+ + + +
Hello, {{ currentHero.name }}
+
Hello, {{ nullHero.name }}
+ + + +Add {{ currentHero.name }} with template + + +
Hero Detail removed from DOM (via template) because isActive is false
+ + + + + +
Show with class
+
Hide with class
+ + + + +
Show with style
+
Hide with style
+ +top + + +
+

NgFor Binding

+ +
+
{{ hero.name }}
+
+
+ +
+ + +
+ +top + +

*ngFor with index

+

with semi-colon separator

+
+
+ {{ i + 1 }} - {{ hero.name }} +
+
+ +

with comma separator

+
+ +
+ {{ i + 1 }} - {{ hero.name }} +
+
+ +top + +

*ngFor trackBy

+ + + + +

without trackBy

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+ +
+ Hero DOM elements change #{{ heroesNoTrackByCount }} without trackBy +
+
+ +

with trackBy

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+ +
+ Hero DOM elements change #{{ heroesWithTrackByCount }} with trackBy +
+
+ +


+ +

with trackBy and semi-colon separator

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+
+ +

with trackBy and comma separator

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+
+ +

with trackBy and space separator

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+
+ +

with generic trackById function

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+
+ +top + + +
+

NgSwitch Binding

+ +

Pick your favorite hero

+
+ +
+ +
+ + + +
+ Are you as confused as {{ currentHero.name }}? +
+ +
+ +top + + +
+

Template reference variables

+ + + + + + + + + + + + + + +

Example Form

+ + +top + + +
+

Inputs and Outputs

+ + + + + + + +
myClick2
+{{ clickMessage2 }} + +top + + +
+

Pipes

+ +
Title through uppercase pipe: {{ title | uppercase }}
+ + +
+ Title through a pipe chain: + {{ title | uppercase | lowercase }} +
+ + +
Birthdate: {{ currentHero?.birthdate | date: "longDate" }}
+ +
{{ currentHero | json }}
+ +
+ Birthdate: {{ currentHero?.birthdate | date: "longDate" | uppercase }} +
+ +
+ + {{ product.price | currency: "USD":true }} +
+ +top + + +
+

Safe navigation operator ?.

+ +
The title is {{ title }}
+ +
The current hero's name is {{ currentHero?.name }}
+ +
The current hero's name is {{ currentHero.name }}
+ + + + +
The null hero's name is {{ nullHero.name }}
+ +
The null hero's name is {{ nullHero && nullHero.name }}
+ +
+ + The null hero's name is {{ nullHero?.name }} +
+ +top + + +
+

Non-null assertion operator !.

+ +
+ +
The hero's name is {{ hero!.name }}
+
+ +top + + +
+

$any type cast function $any( ).

+ +
+ +
The hero's marker is {{ $any(hero).marker }}
+
+ +
+ +
Undeclared members is {{$any(this).member}}
+
+ +top + + + +
+

Enums in binding

+ +

+ The name of the Color.Red enum is {{ Color[Color.Red] }}.
+ The current color is {{ Color[color] }} and its number is {{ color }}.
+ +

+ +top + + + +
+
+
+ +
+ +
+
+ {{ submitMessage }} +
+
+ + + +================================================================================ +`; + exports[`tag-name.component.html 1`] = ` ====================================options===================================== parsers: ["angular"] @@ -10866,3 +13080,18 @@ printWidth: 80 ================================================================================ `; + +exports[`tag-name.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== +
+ +=====================================output===================================== +
+ +================================================================================ +`; diff --git a/tests/html_angular/interpolation.component.html b/tests/html_angular/interpolation.component.html index 134c8f93..67a053c5 100644 --- a/tests/html_angular/interpolation.component.html +++ b/tests/html_angular/interpolation.component.html @@ -54,3 +54,13 @@ {{ aNormalValue | aPipe }}: {{ aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} +

+ {{ + 'delete' + | translate: {what: ('entities' | translate: {count: array.length})} + }} +

+

{{ {a:1+{} } }}

+

{{ {a:a==={} } }}

+

{{ {a:!{} } }}

+

{{ {a:a?b:{} } }}

diff --git a/tests/html_angular/jsfmt.spec.js b/tests/html_angular/jsfmt.spec.js index b3183c49..a9de4214 100644 --- a/tests/html_angular/jsfmt.spec.js +++ b/tests/html_angular/jsfmt.spec.js @@ -2,3 +2,4 @@ run_spec(__dirname, ["angular"]); run_spec(__dirname, ["angular"], { trailingComma: "es5" }); run_spec(__dirname, ["angular"], { printWidth: 1 }); run_spec(__dirname, ["angular"], { htmlWhitespaceSensitivity: "ignore" }); +run_spec(__dirname, ["angular"], { bracketSpacing: false });