Skip to content

Commit 883ee2f

Browse files
committed
Extract rule: template-no-only-default-slot
1 parent 3f6a7c8 commit 883ee2f

4 files changed

Lines changed: 267 additions & 7 deletions

File tree

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -352,13 +352,14 @@ rules in templates can be disabled with eslint directives with mustache or html
352352

353353
### Stylistic Issues
354354

355-
| Name                     | Description | 💼 | 🔧 | 💡 |
356-
| :----------------------------------------------------------------- | :------------------------------------------------------------- | :- | :- | :- |
357-
| [order-in-components](docs/rules/order-in-components.md) | enforce proper order of properties in components | | 🔧 | |
358-
| [order-in-controllers](docs/rules/order-in-controllers.md) | enforce proper order of properties in controllers | | 🔧 | |
359-
| [order-in-models](docs/rules/order-in-models.md) | enforce proper order of properties in models | | 🔧 | |
360-
| [order-in-routes](docs/rules/order-in-routes.md) | enforce proper order of properties in routes | | 🔧 | |
361-
| [template-attribute-order](docs/rules/template-attribute-order.md) | enforce consistent ordering of attributes in template elements | | | |
355+
| Name                          | Description | 💼 | 🔧 | 💡 |
356+
| :--------------------------------------------------------------------------- | :------------------------------------------------------------- | :- | :- | :- |
357+
| [order-in-components](docs/rules/order-in-components.md) | enforce proper order of properties in components | | 🔧 | |
358+
| [order-in-controllers](docs/rules/order-in-controllers.md) | enforce proper order of properties in controllers | | 🔧 | |
359+
| [order-in-models](docs/rules/order-in-models.md) | enforce proper order of properties in models | | 🔧 | |
360+
| [order-in-routes](docs/rules/order-in-routes.md) | enforce proper order of properties in routes | | 🔧 | |
361+
| [template-attribute-order](docs/rules/template-attribute-order.md) | enforce consistent ordering of attributes in template elements | | | |
362+
| [template-no-only-default-slot](docs/rules/template-no-only-default-slot.md) | disallow using only the default slot | | 🔧 | |
362363

363364
### Testing
364365

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# ember/template-no-only-default-slot
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Disallows using only the `default` slot when rendering content into a component.
8+
9+
The default slot (`<:default>`) is used to explicitly target the main content block of a component. However, when _only_ the default slot is used — with no named slots — the extra syntax is redundant and unnecessary.
10+
11+
This rule disallows using only the `default` slot block when rendering content into a component. The preferred form is to pass the content directly, without the default slot wrapper.
12+
13+
## Motivation
14+
15+
When a component has a single default block like this:
16+
17+
```gjs
18+
<template>
19+
<MyComponent>
20+
<:default>
21+
Hello!
22+
</:default>
23+
</MyComponent>
24+
</template>
25+
```
26+
27+
The `<:default>` adds no semantic value. It's simpler and clearer to write:
28+
29+
```gjs
30+
<template>
31+
<MyComponent>
32+
Hello!
33+
</MyComponent>
34+
</template>
35+
```
36+
37+
Explicit slot naming should only be used when multiple slots are present, and disambiguation is needed.
38+
39+
## Examples
40+
41+
This rule **forbids** the following:
42+
43+
```gjs
44+
<template>
45+
<MyComponent>
46+
<:default>
47+
What?
48+
</:default>
49+
</MyComponent>
50+
</template>
51+
```
52+
53+
```gjs
54+
<template>
55+
<MyComponent>
56+
<:default>
57+
<p>Hello world</p>
58+
</:default>
59+
</MyComponent>
60+
</template>
61+
```
62+
63+
This rule **allows** the following:
64+
65+
```gjs
66+
<template>
67+
<MyComponent>
68+
Hello!
69+
</MyComponent>
70+
</template>
71+
```
72+
73+
```gjs
74+
<template>
75+
<MyComponent>
76+
<:header>Header</:header>
77+
<:default>Content</:default>
78+
</MyComponent>
79+
</template>
80+
```
81+
82+
## Migration
83+
84+
If you see this pattern:
85+
86+
```hbs
87+
<SomeCard>
88+
<:default>
89+
Card Content
90+
</:default>
91+
</SomeCard>
92+
```
93+
94+
Just remove the `<:default>` wrapper:
95+
96+
```hbs
97+
<SomeCard>
98+
Card Content
99+
</SomeCard>
100+
```
101+
102+
## References
103+
104+
- [Ember Guides - Named Blocks](https://guides.emberjs.com/release/components/block-content/#toc_named-blocks)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const ERROR_MESSAGE =
2+
'Only default slot used — prefer direct block content without <:default> for clarity and simplicity.';
3+
4+
/** @type {import('eslint').Rule.RuleModule} */
5+
module.exports = {
6+
meta: {
7+
type: 'suggestion',
8+
docs: {
9+
description: 'disallow using only the default slot',
10+
category: 'Stylistic Issues',
11+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-only-default-slot.md',
12+
templateMode: 'both',
13+
},
14+
fixable: 'code',
15+
schema: [],
16+
messages: {},
17+
originallyFrom: {
18+
name: 'ember-template-lint',
19+
rule: 'lib/rules/no-only-default-slot.js',
20+
docs: 'docs/rule/no-only-default-slot.md',
21+
tests: 'test/unit/rules/no-only-default-slot-test.js',
22+
},
23+
},
24+
25+
create(context) {
26+
return {
27+
GlimmerElementNode(node) {
28+
if (node.tag === ':default') {
29+
// Find the parent element node
30+
const parent = node.parent;
31+
32+
if (parent && parent.type === 'GlimmerElementNode') {
33+
// Check if parent has only one child (the :default node)
34+
if (parent.children && parent.children.length === 1) {
35+
context.report({
36+
node,
37+
message: ERROR_MESSAGE,
38+
fix(fixer) {
39+
const sourceCode = context.getSourceCode();
40+
// Replace the :default node with its children
41+
if (node.children && node.children.length > 0) {
42+
const childrenText = node.children
43+
.map((child) => sourceCode.getText(child))
44+
.join('');
45+
return fixer.replaceText(node, childrenText);
46+
} else {
47+
// If no children, just remove the :default node
48+
return fixer.remove(node);
49+
}
50+
},
51+
});
52+
}
53+
}
54+
}
55+
},
56+
};
57+
},
58+
};
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
const rule = require('../../../lib/rules/template-no-only-default-slot');
2+
const RuleTester = require('eslint').RuleTester;
3+
4+
const ruleTester = new RuleTester({
5+
parser: require.resolve('ember-eslint-parser'),
6+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
7+
});
8+
9+
ruleTester.run('template-no-only-default-slot', rule, {
10+
valid: [
11+
`<template><MyComponent>
12+
Hello!
13+
</MyComponent></template>`,
14+
`<template><MyComponent>
15+
<:header>header</:header>
16+
<:footer>footer</:footer>
17+
</MyComponent></template>`,
18+
`<template><MyComponent>
19+
<:default>header</:default>
20+
<:footer>footer</:footer>
21+
</MyComponent></template>`,
22+
`<template><MyComponent>
23+
<:footer>footer</:footer>
24+
</MyComponent></template>`,
25+
],
26+
invalid: [
27+
{
28+
code: '<template><MyComponent><:default>what</:default></MyComponent></template>',
29+
output: '<template><MyComponent>what</MyComponent></template>',
30+
errors: [
31+
{
32+
message:
33+
'Only default slot used — prefer direct block content without <:default> for clarity and simplicity.',
34+
},
35+
],
36+
},
37+
{
38+
code: '<template><MyComponent><:default></:default></MyComponent></template>',
39+
output: '<template><MyComponent></MyComponent></template>',
40+
errors: [
41+
{
42+
message:
43+
'Only default slot used — prefer direct block content without <:default> for clarity and simplicity.',
44+
},
45+
],
46+
},
47+
],
48+
});
49+
50+
const hbsRuleTester = new RuleTester({
51+
parser: require.resolve('ember-eslint-parser/hbs'),
52+
parserOptions: {
53+
ecmaVersion: 2022,
54+
sourceType: 'module',
55+
},
56+
});
57+
58+
hbsRuleTester.run('template-no-only-default-slot', rule, {
59+
valid: [
60+
`<MyComponent>
61+
Hello!
62+
</MyComponent>`,
63+
`<MyComponent>
64+
<:header>header</:header>
65+
<:footer>footer</:footer>
66+
</MyComponent>`,
67+
`<MyComponent>
68+
<:default>header</:default>
69+
<:footer>footer</:footer>
70+
</MyComponent>`,
71+
`<MyComponent>
72+
<:footer>footer</:footer>
73+
</MyComponent>`,
74+
],
75+
invalid: [
76+
{
77+
code: '<MyComponent><:default>what</:default></MyComponent>',
78+
output: '<MyComponent>what</MyComponent>',
79+
errors: [
80+
{
81+
message:
82+
'Only default slot used — prefer direct block content without <:default> for clarity and simplicity.',
83+
},
84+
],
85+
},
86+
{
87+
code: '<MyComponent><:default></:default></MyComponent>',
88+
output: '<MyComponent></MyComponent>',
89+
errors: [
90+
{
91+
message:
92+
'Only default slot used — prefer direct block content without <:default> for clarity and simplicity.',
93+
},
94+
],
95+
},
96+
],
97+
});

0 commit comments

Comments
 (0)