Skip to content

Commit 230dd70

Browse files
committed
feature #1418 [Live] Rename data-action-name to use standard Stimulus features (weaverryan)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Live] Rename data-action-name to use standard Stimulus features | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Issues | Fix #1283 Fix #1065 | License | MIT Hi! The `data-action-name` was flawed because the parser inside couldn't handle things like `,` or `)` - e.g. `data-action-name="save(I am a string with ) inside of it)"` One solution was to write a giant parser to parse these arguments correctly. Instead, in a live stream, some of us decided to "just use Stimulus": ```diff <button data-action="live#action" - data-action-name="debounce(300)|save" + data-live-action-param="debounce(300)|save" >Save</button> ``` This uses [Stimulus action parameters](https://stimulus.hotwired.dev/reference/actions#action-parameters). The new syntax is basically the same length as the previous one, but it's pure Stimulus. Passing arguments is also slightly different, as they are a query string (this allows us to have special characters in the argument values without inventing our own syntax). For single arguments, things look like before (other than the new attribute name): ```diff <button data-action="live#action" - data-action-name="addItem(id={{ item.id }}, itemName=CustomItem)" + data-live-action-param="addItem" + data-live-id-param="{{ item.id }}" >Add Item</button> ``` This also removes the `|prevent` modifier... because... again ❗ Stimulus already does this: ```diff <button - data-action="live#action + data-action="live#action:prevent" - data-action-name="prevent|save" + data-live-action-param="save" >Save</button> ``` Cheers! Commits ------- 98b0ea4 [Live] Rename data-action-name to use standard Stimulus features
2 parents 43fa51a + 98b0ea4 commit 230dd70

File tree

16 files changed

+199
-264
lines changed

16 files changed

+199
-264
lines changed

src/LiveComponent/CHANGELOG.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,51 @@
11
# CHANGELOG
22

3+
## 2.16.0
4+
5+
- [BC BREAK] The `data-action-name` attribute behavior was removed in favor of
6+
using Stimulus "action parameters" and `data-live-action-param`. This is a
7+
breaking change if you were using the `data-action-name` attribute directly
8+
in your templates.
9+
10+
To upgrade your application, follow these changes:
11+
12+
```diff
13+
<button
14+
data-action="live#action"
15+
- data-action-name="debounce(300)|save"
16+
+ data-live-action-param="debounce(300)|save"
17+
>Save</button>
18+
```
19+
20+
To pass arguments to an action, also use the Stimulus "action parameters" syntax:
21+
22+
```diff
23+
<button
24+
data-action="live#action"
25+
- data-action-name="addItem(id={{ item.id }}, itemName=CustomItem)"
26+
+ data-live-action-param="addItem"
27+
+ data-live-id-param="{{ item.id }}"
28+
+ data-live-item-name-param="CustomItem"
29+
>Add Item</button>
30+
```
31+
32+
Additionally, the `prevent` modifier (e.g. `prevent|save`) was removed. Replace
33+
this with the standard Stimulus `:prevent` action option:
34+
35+
```diff
36+
<button
37+
- data-action="live#action
38+
+ data-action="live#action:prevent"
39+
- data-action-name="prevent|save"
40+
+ data-live-action-param="save"
41+
>Save</button>
42+
```
43+
44+
- [BC BREAK] The `data-event` attribute was removed in favor of using Stimulus
45+
"action parameters": rename `data-event` to `data-live-event-param`. Additionally,
46+
if you were passing arguments to the event name, use action parameter attributes
47+
for those as well - e.g. `data-live-foo-param="bar"`.
48+
349
## 2.15.0
450

551
- [BC BREAK] The `data-live-id` attribute was changed to `id` #1484

src/LiveComponent/assets/dist/Directive/directives_parser.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export interface DirectiveModifier {
55
export interface Directive {
66
action: string;
77
args: string[];
8-
named: any;
98
modifiers: DirectiveModifier[];
109
getString: {
1110
(): string;

src/LiveComponent/assets/dist/live_controller.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
9595
disconnect(): void;
9696
update(event: any): void;
9797
action(event: any): void;
98-
emit(event: Event): void;
99-
emitUp(event: Event): void;
100-
emitSelf(event: Event): void;
10198
$render(): Promise<import("./Backend/BackendResponse").default>;
99+
emit(event: any): void;
100+
emitUp(event: any): void;
101+
emitSelf(event: any): void;
102102
$updateModel(model: string, value: any, shouldRender?: boolean, debounce?: number | boolean): Promise<import("./Backend/BackendResponse").default>;
103103
propsUpdatedFromParentValueChanged(): void;
104104
fingerprintValueChanged(): void;

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ function parseDirectives(content) {
66
return directives;
77
}
88
let currentActionName = '';
9-
let currentArgumentName = '';
109
let currentArgumentValue = '';
1110
let currentArguments = [];
12-
let currentNamedArguments = {};
1311
let currentModifiers = [];
1412
let state = 'action';
1513
const getLastActionName = function () {
@@ -25,52 +23,30 @@ function parseDirectives(content) {
2523
directives.push({
2624
action: currentActionName,
2725
args: currentArguments,
28-
named: currentNamedArguments,
2926
modifiers: currentModifiers,
3027
getString: () => {
3128
return content;
3229
}
3330
});
3431
currentActionName = '';
35-
currentArgumentName = '';
3632
currentArgumentValue = '';
3733
currentArguments = [];
38-
currentNamedArguments = {};
3934
currentModifiers = [];
4035
state = 'action';
4136
};
4237
const pushArgument = function () {
43-
const mixedArgTypesError = () => {
44-
throw new Error(`Normal and named arguments cannot be mixed inside "${currentActionName}()"`);
45-
};
46-
if (currentArgumentName) {
47-
if (currentArguments.length > 0) {
48-
mixedArgTypesError();
49-
}
50-
currentNamedArguments[currentArgumentName.trim()] = currentArgumentValue;
51-
}
52-
else {
53-
if (Object.keys(currentNamedArguments).length > 0) {
54-
mixedArgTypesError();
55-
}
56-
currentArguments.push(currentArgumentValue.trim());
57-
}
58-
currentArgumentName = '';
38+
currentArguments.push(currentArgumentValue.trim());
5939
currentArgumentValue = '';
6040
};
6141
const pushModifier = function () {
6242
if (currentArguments.length > 1) {
6343
throw new Error(`The modifier "${currentActionName}()" does not support multiple arguments.`);
6444
}
65-
if (Object.keys(currentNamedArguments).length > 0) {
66-
throw new Error(`The modifier "${currentActionName}()" does not support named arguments.`);
67-
}
6845
currentModifiers.push({
6946
name: currentActionName,
7047
value: currentArguments.length > 0 ? currentArguments[0] : null,
7148
});
7249
currentActionName = '';
73-
currentArgumentName = '';
7450
currentArguments = [];
7551
state = 'action';
7652
};
@@ -104,11 +80,6 @@ function parseDirectives(content) {
10480
pushArgument();
10581
break;
10682
}
107-
if (char === '=') {
108-
currentArgumentName = currentArgumentValue;
109-
currentArgumentValue = '';
110-
break;
111-
}
11283
currentArgumentValue += char;
11384
break;
11485
case 'after_arguments':
@@ -325,7 +296,7 @@ function getAllModelDirectiveFromElements(element) {
325296
}
326297
const directives = parseDirectives(element.dataset.model);
327298
directives.forEach((directive) => {
328-
if (directive.args.length > 0 || directive.named.length > 0) {
299+
if (directive.args.length > 0) {
329300
throw new Error(`The data-model="${element.dataset.model}" format is invalid: it does not support passing arguments to the model.`);
330301
}
331302
directive.action = normalizeModelName(directive.action);
@@ -342,7 +313,7 @@ function getModelDirectiveFromElement(element, throwOnMissing = true) {
342313
if (formElement && 'model' in formElement.dataset) {
343314
const directives = parseDirectives(formElement.dataset.model || '*');
344315
const directive = directives[0];
345-
if (directive.args.length > 0 || directive.named.length > 0) {
316+
if (directive.args.length > 0) {
346317
throw new Error(`The data-model="${formElement.dataset.model}" format is invalid: it does not support passing arguments to the model.`);
347318
}
348319
directive.action = normalizeModelName(element.getAttribute('name'));
@@ -2942,15 +2913,18 @@ class LiveControllerDefault extends Controller {
29422913
this.updateModelFromElementEvent(event.currentTarget, null);
29432914
}
29442915
action(event) {
2945-
const rawAction = event.currentTarget.dataset.actionName;
2916+
const params = event.params;
2917+
if (!params.action) {
2918+
throw new Error(`No action name provided on element: ${getElementAsTagText(event.currentTarget)}. Did you forget to add the "data-live-action-param" attribute?`);
2919+
}
2920+
const rawAction = params.action;
2921+
const actionArgs = Object.assign({}, params);
2922+
delete actionArgs.action;
29462923
const directives = parseDirectives(rawAction);
29472924
let debounce = false;
29482925
directives.forEach((directive) => {
29492926
let pendingFiles = {};
29502927
const validModifiers = new Map();
2951-
validModifiers.set('prevent', () => {
2952-
event.preventDefault();
2953-
});
29542928
validModifiers.set('stop', () => {
29552929
event.stopPropagation();
29562930
});
@@ -2985,12 +2959,15 @@ class LiveControllerDefault extends Controller {
29852959
}
29862960
delete this.pendingFiles[key];
29872961
}
2988-
this.component.action(directive.action, directive.named, debounce);
2962+
this.component.action(directive.action, actionArgs, debounce);
29892963
if (getModelDirectiveFromElement(event.currentTarget, false)) {
29902964
this.pendingActionTriggerModelElement = event.currentTarget;
29912965
}
29922966
});
29932967
}
2968+
$render() {
2969+
return this.component.render();
2970+
}
29942971
emit(event) {
29952972
this.getEmitDirectives(event).forEach(({ name, data, nameMatch }) => {
29962973
this.component.emit(name, data, nameMatch);
@@ -3006,9 +2983,6 @@ class LiveControllerDefault extends Controller {
30062983
this.component.emitSelf(name, data);
30072984
});
30082985
}
3009-
$render() {
3010-
return this.component.render();
3011-
}
30122986
$updateModel(model, value, shouldRender = true, debounce = true) {
30132987
return this.component.set(model, value, shouldRender, debounce);
30142988
}
@@ -3019,11 +2993,13 @@ class LiveControllerDefault extends Controller {
30192993
this.component.fingerprint = this.fingerprintValue;
30202994
}
30212995
getEmitDirectives(event) {
3022-
const element = event.currentTarget;
3023-
if (!element.dataset.event) {
3024-
throw new Error(`No data-event attribute found on element: ${getElementAsTagText(element)}`);
2996+
const params = event.params;
2997+
if (!params.event) {
2998+
throw new Error(`No event name provided on element: ${getElementAsTagText(event.currentTarget)}. Did you forget to add the "data-live-event-param" attribute?`);
30252999
}
3026-
const eventInfo = element.dataset.event;
3000+
const eventInfo = params.event;
3001+
const eventArgs = Object.assign({}, params);
3002+
delete eventArgs.event;
30273003
const directives = parseDirectives(eventInfo);
30283004
const emits = [];
30293005
directives.forEach((directive) => {
@@ -3039,7 +3015,7 @@ class LiveControllerDefault extends Controller {
30393015
});
30403016
emits.push({
30413017
name: directive.action,
3042-
data: directive.named,
3018+
data: eventArgs,
30433019
nameMatch,
30443020
});
30453021
});

src/LiveComponent/assets/src/Directive/directives_parser.ts

Lines changed: 5 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ export interface Directive {
2525
* An array of unnamed arguments passed to the action
2626
*/
2727
args: string[];
28-
/**
29-
* An object of named arguments
30-
*/
31-
named: any;
3228
/**
3329
* Any modifiers applied to the action
3430
*/
@@ -41,17 +37,8 @@ export interface Directive {
4137
* into an array of directives, with this format:
4238
*
4339
* [
44-
* { action: 'addClass', args: ['foo'], named: {}, modifiers: [] },
45-
* { action: 'removeAttribute', args: ['bar'], named: {}, modifiers: [] }
46-
* ]
47-
*
48-
* This also handles named arguments
49-
*
50-
* save(foo=bar, baz=bazzles)
51-
*
52-
* Which would return:
53-
* [
54-
* { action: 'save', args: [], named: { foo: 'bar', baz: 'bazzles }, modifiers: [] }
40+
* { action: 'addClass', args: ['foo'], modifiers: [] },
41+
* { action: 'removeAttribute', args: ['bar'], modifiers: [] }
5542
* ]
5643
*
5744
* @param {string} content The value of the attribute
@@ -64,10 +51,8 @@ export function parseDirectives(content: string|null): Directive[] {
6451
}
6552

6653
let currentActionName = '';
67-
let currentArgumentName = '';
6854
let currentArgumentValue = '';
6955
let currentArguments: string[] = [];
70-
let currentNamedArguments: any = {};
7156
let currentModifiers: { name: string, value: string | null }[] = [];
7257
let state = 'action';
7358

@@ -86,7 +71,6 @@ export function parseDirectives(content: string|null): Directive[] {
8671
directives.push({
8772
action: currentActionName,
8873
args: currentArguments,
89-
named: currentNamedArguments,
9074
modifiers: currentModifiers,
9175
getString: () => {
9276
// TODO - make a string representation of JUST this directive
@@ -95,36 +79,15 @@ export function parseDirectives(content: string|null): Directive[] {
9579
}
9680
});
9781
currentActionName = '';
98-
currentArgumentName = '';
9982
currentArgumentValue = '';
10083
currentArguments = [];
101-
currentNamedArguments = {};
10284
currentModifiers = [];
10385
state = 'action';
10486
}
10587
const pushArgument = function() {
106-
const mixedArgTypesError = () => {
107-
throw new Error(`Normal and named arguments cannot be mixed inside "${currentActionName}()"`)
108-
}
109-
110-
if (currentArgumentName) {
111-
if (currentArguments.length > 0) {
112-
mixedArgTypesError();
113-
}
114-
115-
// argument names are also trimmed to avoid space after ","
116-
// "foo=bar, baz=bazzles"
117-
currentNamedArguments[currentArgumentName.trim()] = currentArgumentValue;
118-
} else {
119-
if (Object.keys(currentNamedArguments).length > 0) {
120-
mixedArgTypesError();
121-
}
122-
123-
// value is trimmed to avoid space after ","
124-
// "foo, bar"
125-
currentArguments.push(currentArgumentValue.trim());
126-
}
127-
currentArgumentName = '';
88+
// value is trimmed to avoid space after ","
89+
// "foo, bar"
90+
currentArguments.push(currentArgumentValue.trim());
12891
currentArgumentValue = '';
12992
}
13093

@@ -133,16 +96,11 @@ export function parseDirectives(content: string|null): Directive[] {
13396
throw new Error(`The modifier "${currentActionName}()" does not support multiple arguments.`)
13497
}
13598

136-
if (Object.keys(currentNamedArguments).length > 0) {
137-
throw new Error(`The modifier "${currentActionName}()" does not support named arguments.`)
138-
}
139-
14099
currentModifiers.push({
141100
name: currentActionName,
142101
value: currentArguments.length > 0 ? currentArguments[0] : null,
143102
});
144103
currentActionName = '';
145-
currentArgumentName = '';
146104
currentArguments = [];
147105
state = 'action';
148106
}
@@ -196,14 +154,6 @@ export function parseDirectives(content: string|null): Directive[] {
196154
break;
197155
}
198156

199-
if (char === '=') {
200-
// this is a named argument!
201-
currentArgumentName = currentArgumentValue;
202-
currentArgumentValue = '';
203-
204-
break;
205-
}
206-
207157
// add next character to argument
208158
currentArgumentValue += char;
209159

src/LiveComponent/assets/src/dom_utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export function getAllModelDirectiveFromElements(element: HTMLElement): Directiv
141141
const directives = parseDirectives(element.dataset.model);
142142

143143
directives.forEach((directive) => {
144-
if (directive.args.length > 0 || directive.named.length > 0) {
144+
if (directive.args.length > 0) {
145145
throw new Error(
146146
`The data-model="${element.dataset.model}" format is invalid: it does not support passing arguments to the model.`
147147
);
@@ -167,7 +167,7 @@ export function getModelDirectiveFromElement(element: HTMLElement, throwOnMissin
167167
const directives = parseDirectives(formElement.dataset.model || '*');
168168
const directive = directives[0];
169169

170-
if (directive.args.length > 0 || directive.named.length > 0) {
170+
if (directive.args.length > 0) {
171171
throw new Error(
172172
`The data-model="${formElement.dataset.model}" format is invalid: it does not support passing arguments to the model.`
173173
);

0 commit comments

Comments
 (0)