Skip to content

Commit 719764b

Browse files
committed
Update jsx-sort-props to allow sorting callbacks last
1 parent a7d75cf commit 719764b

File tree

3 files changed

+56
-7
lines changed

3 files changed

+56
-7
lines changed

docs/rules/jsx-sort-props.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ The following patterns are considered okay and do not cause warnings:
3838
<Hello name="John" Number="2" />;
3939
```
4040

41+
### `callbacksLast`
42+
43+
When `true`, callbacks must be listed after all other props:
44+
45+
```js
46+
<Hello tel={5555555} onClick={this._handleClick} />
47+
```
48+
4149
## When not to use
4250

4351
This rule is a formatting preference and not following it won't negatively affect the quality of your code. If alphabetizing props isn't a part of your coding standards, then you can leave this rule off.

lib/rules/jsx-sort-props.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@
88
// Rule Definition
99
// ------------------------------------------------------------------------------
1010

11+
function isCallbackPropName(propName) {
12+
return /^on[A-Z]/.test(propName);
13+
}
14+
1115
module.exports = function(context) {
1216

1317
var configuration = context.options[0] || {};
1418
var ignoreCase = configuration.ignoreCase || false;
19+
var callbacksLast = configuration.callbacksLast || false;
1520

1621
return {
1722
JSXOpeningElement: function(node) {
@@ -20,15 +25,29 @@ module.exports = function(context) {
2025
return attrs[idx + 1];
2126
}
2227

23-
var lastPropName = memo.name.name;
24-
var currenPropName = decl.name.name;
28+
var previousPropName = memo.name.name;
29+
var currentPropName = decl.name.name;
30+
var previousIsCallback = isCallbackPropName(previousPropName);
31+
var currentIsCallback = isCallbackPropName(currentPropName);
2532

2633
if (ignoreCase) {
27-
lastPropName = lastPropName.toLowerCase();
28-
currenPropName = currenPropName.toLowerCase();
34+
previousPropName = previousPropName.toLowerCase();
35+
currentPropName = currentPropName.toLowerCase();
36+
}
37+
38+
if (callbacksLast) {
39+
if (!previousIsCallback && currentIsCallback) {
40+
// Entering the callback prop section
41+
return decl;
42+
}
43+
if (previousIsCallback && !currentIsCallback) {
44+
// Encountered a non-callback prop after a callback prop
45+
context.report(decl, 'Callbacks must be listed after all other props');
46+
return memo;
47+
}
2948
}
3049

31-
if (currenPropName < lastPropName) {
50+
if (currentPropName < previousPropName) {
3251
context.report(decl, 'Props should be sorted alphabetically');
3352
return memo;
3453
}
@@ -42,6 +61,11 @@ module.exports = function(context) {
4261
module.exports.schema = [{
4362
type: 'object',
4463
properties: {
64+
// Whether callbacks (prefixed with "on") should be listed at the very end,
65+
// after all other props.
66+
callbacksLast: {
67+
type: 'boolean'
68+
},
4569
ignoreCase: {
4670
type: 'boolean'
4771
}

tests/lib/rules/jsx-sort-props.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ var expectedError = {
2222
message: 'Props should be sorted alphabetically',
2323
type: 'JSXAttribute'
2424
};
25+
var expectedCallbackError = {
26+
message: 'Callbacks must be listed after all other props',
27+
type: 'JSXAttribute'
28+
};
29+
var callbacksLastArgs = [{
30+
callbacksLast: true
31+
}];
2532
var ignoreCaseArgs = [{
2633
ignoreCase: true
2734
}];
@@ -40,9 +47,12 @@ ruleTester.run('jsx-sort-props', rule, {
4047
{code: '<App {...this.props} a="c" b="b" c="a" />;', ecmaFeatures: features},
4148
{code: '<App c="a" {...this.props} a="c" b="b" />;', ecmaFeatures: features},
4249
{code: '<App A a />;', ecmaFeatures: features},
50+
// Ignoring case
4351
{code: '<App a A />;', options: ignoreCaseArgs, ecmaFeatures: features},
4452
{code: '<App a B c />;', options: ignoreCaseArgs, ecmaFeatures: features},
45-
{code: '<App A b C />;', options: ignoreCaseArgs, ecmaFeatures: features}
53+
{code: '<App A b C />;', options: ignoreCaseArgs, ecmaFeatures: features},
54+
// Sorting callbacks below all other props
55+
{code: '<App a z onBar onFoo />;', options: callbacksLastArgs, ecmaFeatures: features}
4656
],
4757
invalid: [
4858
{code: '<App b a />;', errors: [expectedError], ecmaFeatures: features},
@@ -53,6 +63,13 @@ ruleTester.run('jsx-sort-props', rule, {
5363
{code: '<App B A c />;', options: ignoreCaseArgs, errors: [expectedError], ecmaFeatures: features},
5464
{code: '<App c="a" a="c" b="b" />;', errors: 2, ecmaFeatures: features},
5565
{code: '<App {...this.props} c="a" a="c" b="b" />;', errors: 2, ecmaFeatures: features},
56-
{code: '<App d="d" b="b" {...this.props} c="a" a="c" />;', errors: 2, ecmaFeatures: features}
66+
{code: '<App d="d" b="b" {...this.props} c="a" a="c" />;', errors: 2, ecmaFeatures: features},
67+
{code: '<App a z onFoo onBar />;', errors: [expectedError], options: callbacksLastArgs, ecmaFeatures: features},
68+
{
69+
code: '<App a onBar onFoo z />;',
70+
errors: [expectedCallbackError],
71+
options: callbacksLastArgs,
72+
ecmaFeatures: features
73+
}
5774
]
5875
});

0 commit comments

Comments
 (0)