Skip to content

Commit 79e34fd

Browse files
Merge pull request #2481 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-mut-helper
Extract rule: template-no-mut-helper
2 parents 64efd92 + 2d8cca9 commit 79e34fd

4 files changed

Lines changed: 519 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ rules in templates can be disabled with eslint directives with mustache or html
210210
| [template-no-input-placeholder](docs/rules/template-no-input-placeholder.md) | disallow placeholder attribute on input elements | | | |
211211
| [template-no-input-tagname](docs/rules/template-no-input-tagname.md) | disallow tagName attribute on {{input}} helper | | | |
212212
| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
213+
| [template-no-mut-helper](docs/rules/template-no-mut-helper.md) | disallow usage of (mut) helper | | | |
213214
| [template-no-negated-comparison](docs/rules/template-no-negated-comparison.md) | disallow negated comparisons in templates | | | |
214215
| [template-no-negated-condition](docs/rules/template-no-negated-condition.md) | disallow negated conditions in if/unless | | | |
215216
| [template-no-nested-splattributes](docs/rules/template-no-nested-splattributes.md) | disallow nested ...attributes usage | | | |
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# ember/template-no-mut-helper
2+
3+
> **HBS Only**: This rule applies to classic `.hbs` template files only (loose mode). It is not relevant for `gjs`/`gts` files (strict mode), where these patterns cannot occur.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Disallow usage of the `(mut)` helper.
8+
9+
The `(mut)` helper was used in classic Ember to create two-way bindings. In modern Ember (Octane and beyond), this pattern is discouraged in favor of explicit one-way data flow with actions or setters.
10+
11+
## Rule Details
12+
13+
This rule disallows using the `(mut)` helper in templates.
14+
15+
## Reasons to not use [the `mut` helper](https://api.emberjs.com/ember/release/classes/Ember.Templates.helpers/methods/each?anchor=mut)
16+
17+
1. General problems in the programming model:
18+
- The mut helper is non-intuitive to use, see, teach, and learn since it can either be a getter or a setter based on the context in which it’s used.
19+
20+
Example:
21+
22+
```hbs
23+
{{#let (mut this.foo) as |foo|}}
24+
<!-- When used like this, it's a getter -->
25+
{{foo}}
26+
27+
<!-- When used like this, it's a setter -->
28+
<button {{action foo 123}}>Update Foo</button>
29+
{{/let}}
30+
```
31+
32+
- The need for the [no-extra-mut-helper-argument](template-no-extra-mut-helper-argument.md) rule is further evidence that `mut` has a non-intuitive signature and frequently gets misused.
33+
- The mut helper is usually only used as a pure setter, in which case there are other template helpers that are pure setters that could be used instead of mut (e.g. [ember-set-helper](https://github.com/pzuraq/ember-set-helper)).
34+
35+
2. Incompatibility with Glimmer Component intentions:
36+
- The mut helper can re-introduce 2 way data binding into Glimmer Components on named arguments where a child can change a parent’s data, which goes against the Data Down Actions Up principle, goes against Glimmer Components’ intention to have immutable arguments, and is [discouraged by the Ember Core team](https://www.pzuraq.com/blog/on-mut-and-2-way-binding/).
37+
38+
Example:
39+
40+
```hbs
41+
<input type='checkbox' checked={{@checked}} {{on 'change' (fn (mut @checked) (not @checked))}} />
42+
```
43+
44+
## What this rule does
45+
46+
This rule forbids any use of the `mut` helper, both as a getter and a setter, in any context. It also
47+
surfaces possible alternatives in the lint violation message to help guide engineers to resolving
48+
the lint violations.
49+
50+
## Examples
51+
52+
### Incorrect ❌
53+
54+
```hbs
55+
<Input @value={{this.name}} @onChange={{mut this.name}} />
56+
```
57+
58+
```hbs
59+
{{input value=(mut this.name)}}
60+
```
61+
62+
```hbs
63+
<CustomComponent @onChange={{mut this.value}} />
64+
```
65+
66+
### Correct ✅
67+
68+
```hbs
69+
<Input @value={{this.name}} @onChange={{this.updateName}} />
70+
```
71+
72+
```hbs
73+
<Input @value={{this.name}} @onChange={{fn this.updateName}} />
74+
```
75+
76+
```hbs
77+
<CustomComponent @onChange={{this.handleChange}} />
78+
```
79+
80+
## Migration
81+
82+
1. When used as a pure setter only, `mut` could be replaced by a JS action ("Option 1" below) or [ember-set-helper](https://github.com/pzuraq/ember-set-helper) ("Option 2" below):
83+
84+
Before:
85+
86+
```hbs
87+
<MyComponent @closeDropdown={{action (mut this.setIsDropdownOpen) false}} />
88+
```
89+
90+
After (Option 1 HBS):
91+
92+
```hbs
93+
<MyComponent @closeDropdown={{action this.setIsDropdownOpen false}} />
94+
```
95+
96+
After (Option 1 JS):
97+
98+
```js
99+
// in your component class
100+
class MyComponent extends Component {
101+
@action
102+
setIsDropdownOpen(isDropdownOpen) {
103+
set(this, 'isDropdownOpen', isDropdownOpen);
104+
}
105+
}
106+
```
107+
108+
After (Option 2):
109+
110+
```hbs
111+
<MyComponent @closeDropdown={{set this 'isDropdownOpen' false}} />
112+
```
113+
114+
\
115+
2. When used as a pure getter only, `mut` could be removed:
116+
117+
Before:
118+
119+
```hbs
120+
<MyComponent @isDropdownOpen={{mut this.isDropdownOpen}} />
121+
```
122+
123+
After:
124+
125+
```hbs
126+
<MyComponent @isDropdownOpen={{this.isDropdownOpen}} />
127+
```
128+
129+
\
130+
3. When `mut` is used as a getter and setter, `mut` could be replaced with a different namespace for the property and a dedicated action function to set the property: (Note: another other option could be to pull in the pick helper from [ember-composable-helpers](https://github.com/DockYard/ember-composable-helpers) and use it like [this](https://github.com/pzuraq/ember-set-helper#picking-values-with-ember-composable-helpers).) (Note: Another option could be to use [ember-box](https://github.com/pzuraq/ember-box)).
131+
132+
Before:
133+
134+
```hbs
135+
{{#let (mut this.foo) as |foo|}}
136+
{{foo}}
137+
<input onchange={{action foo value=”target.value”}} />
138+
{{/let}}
139+
```
140+
141+
After HBS:
142+
143+
```hbs
144+
{{this.foo}}
145+
<input {{on “change” this.updateFoo}} />
146+
```
147+
148+
After JS:
149+
150+
```js
151+
// in your component class
152+
class MyComponent extends Component {
153+
@tracked
154+
foo;
155+
156+
@action
157+
updateFoo(evt) {
158+
this.foo = evt.target.value;
159+
// or set(this, 'foo', evt.target.value); for legacy Ember code
160+
}
161+
}
162+
```
163+
164+
\
165+
4. When `mut` is being passed into a built-in classic component that uses 2 way data binding, `mut` could be removed:
166+
167+
Before:
168+
169+
```hbs
170+
<Input @value={{mut this.profile.description}} />
171+
```
172+
173+
After:
174+
175+
```hbs
176+
<Input @value={{this.profile.description}} />
177+
```
178+
179+
## Options
180+
181+
| Name | Type | Default | Description |
182+
| ------------------- | -------- | ------- | ----------------------------------------------------------------------------- |
183+
| `setterAlternative` | `string` | | If provided, the error message suggests using this helper instead of `(mut)`. |
184+
185+
## Related Rules
186+
187+
- [no-mut-helper](./no-mut-helper.md)
188+
189+
## References
190+
191+
- [Ember Octane Guide - Two-way bindings](https://guides.emberjs.com/release/upgrading/current-edition/)
192+
- [eslint-plugin-ember template-no-mut-helper](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-mut-helper.md)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'suggestion',
5+
docs: {
6+
description: 'disallow usage of (mut) helper',
7+
category: 'Best Practices',
8+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-mut-helper.md',
9+
templateMode: 'loose',
10+
},
11+
fixable: null,
12+
schema: [
13+
{
14+
type: 'object',
15+
properties: {
16+
setterAlternative: { type: 'string' },
17+
},
18+
additionalProperties: false,
19+
},
20+
],
21+
messages: {
22+
noMutHelper: '{{message}}',
23+
},
24+
originallyFrom: {
25+
name: 'ember-template-lint',
26+
rule: 'lib/rules/no-mut-helper.js',
27+
docs: 'docs/rule/no-mut-helper.md',
28+
tests: 'test/unit/rules/no-mut-helper-test.js',
29+
},
30+
},
31+
32+
create(context) {
33+
const options = context.options[0] || {};
34+
const setterAlternative = options.setterAlternative;
35+
const message = setterAlternative
36+
? `Do not use the (mut) helper. Consider using a JS action or {{${setterAlternative}}} instead.`
37+
: 'Do not use the (mut) helper. Use regular setters or actions instead.';
38+
39+
function checkNode(node) {
40+
if (node.path?.type === 'GlimmerPathExpression' && node.path.original === 'mut') {
41+
context.report({
42+
node,
43+
messageId: 'noMutHelper',
44+
data: { message },
45+
});
46+
}
47+
}
48+
49+
return {
50+
GlimmerSubExpression: checkNode,
51+
GlimmerMustacheStatement: checkNode,
52+
};
53+
},
54+
};

0 commit comments

Comments
 (0)