Skip to content

Commit 3b5e6b9

Browse files
Merge pull request #2622 from NullVoxPopuli/nvp/template-lint-extract-rule-template-style-concatenation
Extract rule: template-style-concatenation
2 parents c9d988f + e74eb91 commit 3b5e6b9

4 files changed

Lines changed: 301 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ rules in templates can be disabled with eslint directives with mustache or html
245245
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
246246
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
247247
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
248+
| [template-style-concatenation](docs/rules/template-style-concatenation.md) | disallow string concatenation in inline styles | | | |
248249

249250
### Components
250251

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# ember/template-style-concatenation
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallows string concatenation in inline styles.
6+
7+
String concatenation in style attributes can be error-prone and hard to maintain. Use the `{{html-safe}}` helper or a computed property instead.
8+
9+
## Rule Details
10+
11+
This rule disallows string concatenation in `style` attributes.
12+
13+
## Examples
14+
15+
Examples of **incorrect** code for this rule:
16+
17+
```gjs
18+
<template>
19+
<div style="color: {{this.color}};">Content</div>
20+
</template>
21+
```
22+
23+
```gjs
24+
<template>
25+
<div style={{concat "width: " this.width "px;"}}>Content</div>
26+
</template>
27+
```
28+
29+
Examples of **correct** code for this rule:
30+
31+
```gjs
32+
<template>
33+
<div style="color: red;">Content</div>
34+
</template>
35+
```
36+
37+
```gjs
38+
<template>
39+
<div style={{this.computedStyle}}>Content</div>
40+
</template>
41+
```
42+
43+
```gjs
44+
<template>
45+
<div style={{html-safe this.styleString}}>Content</div>
46+
</template>
47+
```
48+
49+
## Migration
50+
51+
In your component:
52+
53+
```js
54+
import { htmlSafe } from '@ember/template';
55+
56+
export default class MyComponent extends Component {
57+
get computedStyle() {
58+
return htmlSafe(`width: ${this.width}px; color: ${this.color};`);
59+
}
60+
}
61+
```
62+
63+
Then in template:
64+
65+
```gjs
66+
<template>
67+
<div style={{this.computedStyle}}>Content</div>
68+
</template>
69+
```
70+
71+
## Related Rules
72+
73+
- [no-inline-styles](template-no-inline-styles.md)
74+
75+
## References
76+
77+
- [eslint-plugin-ember template-style-concatenation](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-style-concatenation.md)
78+
- [Ember.js Guides - htmlSafe](https://guides.emberjs.com/release/templates/writing-helpers/#toc_marking-strings-as-html-safe)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'suggestion',
5+
docs: {
6+
description: 'disallow string concatenation in inline styles',
7+
category: 'Best Practices',
8+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-style-concatenation.md',
9+
templateMode: 'both',
10+
},
11+
fixable: null,
12+
schema: [],
13+
messages: {
14+
unexpected:
15+
'Avoid string concatenation in style attributes. Use a computed property with htmlSafe instead.',
16+
},
17+
originallyFrom: {
18+
name: 'ember-template-lint',
19+
rule: 'lib/rules/style-concatenation.js',
20+
docs: 'docs/rule/style-concatenation.md',
21+
tests: 'test/unit/rules/style-concatenation-test.js',
22+
},
23+
},
24+
25+
create(context) {
26+
return {
27+
GlimmerElementNode(node) {
28+
const styleAttr = node.attributes?.find((a) => a.name === 'style');
29+
30+
if (!styleAttr || !styleAttr.value) {
31+
return;
32+
}
33+
34+
// Check if style attribute uses concatenation
35+
if (styleAttr.value.type === 'GlimmerConcatStatement') {
36+
context.report({
37+
node: styleAttr,
38+
messageId: 'unexpected',
39+
});
40+
}
41+
42+
// Check for mustache containing concat helper
43+
if (styleAttr.value.type === 'GlimmerMustacheStatement') {
44+
const path = styleAttr.value.path;
45+
if (path && path.type === 'GlimmerPathExpression' && path.original === 'concat') {
46+
context.report({
47+
node: styleAttr,
48+
messageId: 'unexpected',
49+
});
50+
}
51+
}
52+
},
53+
};
54+
},
55+
};
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const rule = require('../../../lib/rules/template-style-concatenation');
6+
const RuleTester = require('eslint').RuleTester;
7+
8+
//------------------------------------------------------------------------------
9+
// Tests
10+
//------------------------------------------------------------------------------
11+
12+
const ruleTester = new RuleTester({
13+
parser: require.resolve('ember-eslint-parser'),
14+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
15+
});
16+
17+
ruleTester.run('template-style-concatenation', rule, {
18+
valid: [
19+
`<template>
20+
<div style="color: red;">Content</div>
21+
</template>`,
22+
`<template>
23+
<div style={{this.computedStyle}}>Content</div>
24+
</template>`,
25+
`<template>
26+
<div style={{html-safe this.styleString}}>Content</div>
27+
</template>`,
28+
29+
'<template><img></template>',
30+
'<template><img style={{myStyle}}></template>',
31+
'<template><img style={{background-image url}}></template>',
32+
'<template><img style="background-image: url(/foo.png)"}}></template>',
33+
'<template><img style={{html-safe (concat "background-image: url(" url ")")}}></template>',
34+
'<template><img style={{html-safe (concat knownSafeStyle1 ";" knownSafeStyle2)}}></template>',
35+
],
36+
37+
invalid: [
38+
{
39+
code: `<template>
40+
<div style="color: {{this.color}};">Content</div>
41+
</template>`,
42+
output: null,
43+
errors: [
44+
{
45+
message:
46+
'Avoid string concatenation in style attributes. Use a computed property with htmlSafe instead.',
47+
type: 'GlimmerAttrNode',
48+
},
49+
],
50+
},
51+
{
52+
code: `<template>
53+
<div style={{concat "width: " this.width "px;"}}>Content</div>
54+
</template>`,
55+
output: null,
56+
errors: [
57+
{
58+
message:
59+
'Avoid string concatenation in style attributes. Use a computed property with htmlSafe instead.',
60+
type: 'GlimmerAttrNode',
61+
},
62+
],
63+
},
64+
65+
{
66+
code: '<template><img style="{{myStyle}}"></template>',
67+
output: null,
68+
errors: [
69+
{
70+
message:
71+
'Avoid string concatenation in style attributes. Use a computed property with htmlSafe instead.',
72+
},
73+
],
74+
},
75+
{
76+
code: '<template><img style="background-image: {{url}}"></template>',
77+
output: null,
78+
errors: [
79+
{
80+
message:
81+
'Avoid string concatenation in style attributes. Use a computed property with htmlSafe instead.',
82+
},
83+
],
84+
},
85+
{
86+
code: '<template><img style="{{background-image url}}"></template>',
87+
output: null,
88+
errors: [
89+
{
90+
message:
91+
'Avoid string concatenation in style attributes. Use a computed property with htmlSafe instead.',
92+
},
93+
],
94+
},
95+
{
96+
code: '<template><img style={{concat knownSafeStyle1 ";" knownSafeStyle2}}></template>',
97+
output: null,
98+
errors: [
99+
{
100+
message:
101+
'Avoid string concatenation in style attributes. Use a computed property with htmlSafe instead.',
102+
},
103+
],
104+
},
105+
],
106+
});
107+
108+
const hbsRuleTester = new RuleTester({
109+
parser: require.resolve('ember-eslint-parser/hbs'),
110+
parserOptions: {
111+
ecmaVersion: 2022,
112+
sourceType: 'module',
113+
},
114+
});
115+
116+
hbsRuleTester.run('template-style-concatenation', rule, {
117+
valid: [
118+
'<img>',
119+
'<img style={{myStyle}}>',
120+
'<img style={{background-image url}}>',
121+
'<img style="background-image: url(/foo.png)"}}>',
122+
'<img style={{html-safe (concat "background-image: url(" url ")")}}>',
123+
'<img style={{html-safe (concat knownSafeStyle1 ";" knownSafeStyle2)}}>',
124+
],
125+
invalid: [
126+
{
127+
code: '<img style="{{myStyle}}">',
128+
output: null,
129+
errors: [
130+
{
131+
message:
132+
'Avoid string concatenation in style attributes. Use a computed property with htmlSafe instead.',
133+
},
134+
],
135+
},
136+
{
137+
code: '<img style="background-image: {{url}}">',
138+
output: null,
139+
errors: [
140+
{
141+
message:
142+
'Avoid string concatenation in style attributes. Use a computed property with htmlSafe instead.',
143+
},
144+
],
145+
},
146+
{
147+
code: '<img style="{{background-image url}}">',
148+
output: null,
149+
errors: [
150+
{
151+
message:
152+
'Avoid string concatenation in style attributes. Use a computed property with htmlSafe instead.',
153+
},
154+
],
155+
},
156+
{
157+
code: '<img style={{concat knownSafeStyle1 ";" knownSafeStyle2}}>',
158+
output: null,
159+
errors: [
160+
{
161+
message:
162+
'Avoid string concatenation in style attributes. Use a computed property with htmlSafe instead.',
163+
},
164+
],
165+
},
166+
],
167+
});

0 commit comments

Comments
 (0)