Skip to content

[Live] Rename data-action-name to use standard Stimulus features #1418

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/LiveComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
# CHANGELOG

## 2.16.0

- [BC BREAK] The `data-action-name` attribute behavior was removed in favor of
using Stimulus "action parameters" and `data-live-action-param`. This is a
breaking change if you were using the `data-action-name` attribute directly
in your templates.

To upgrade your application, follow these changes:

```diff
<button
data-action="live#action"
- data-action-name="debounce(300)|save"
+ data-live-action-param="debounce(300)|save"
>Save</button>
```

To pass arguments to an action, also use the Stimulus "action parameters" syntax:

```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 }}"
+ data-live-item-name-param="CustomItem"
>Add Item</button>
```

Additionally, the `prevent` modifier (e.g. `prevent|save`) was removed. Replace
this with the standard Stimulus `:prevent` action option:

```diff
<button
- data-action="live#action
+ data-action="live#action:prevent"
- data-action-name="prevent|save"
+ data-live-action-param="save"
>Save</button>
```

- [BC BREAK] The `data-event` attribute was removed in favor of using Stimulus
"action parameters": rename `data-event` to `data-live-event-param`. Additionally,
if you were passing arguments to the event name, use action parameter attributes
for those as well - e.g. `data-live-foo-param="bar"`.

## 2.15.0

- [BC BREAK] The `data-live-id` attribute was changed to `id` #1484
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export interface DirectiveModifier {
export interface Directive {
action: string;
args: string[];
named: any;
modifiers: DirectiveModifier[];
getString: {
(): string;
Expand Down
6 changes: 3 additions & 3 deletions src/LiveComponent/assets/dist/live_controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
disconnect(): void;
update(event: any): void;
action(event: any): void;
emit(event: Event): void;
emitUp(event: Event): void;
emitSelf(event: Event): void;
$render(): Promise<import("./Backend/BackendResponse").default>;
emit(event: any): void;
emitUp(event: any): void;
emitSelf(event: any): void;
$updateModel(model: string, value: any, shouldRender?: boolean, debounce?: number | boolean): Promise<import("./Backend/BackendResponse").default>;
propsUpdatedFromParentValueChanged(): void;
fingerprintValueChanged(): void;
Expand Down
66 changes: 21 additions & 45 deletions src/LiveComponent/assets/dist/live_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ function parseDirectives(content) {
return directives;
}
let currentActionName = '';
let currentArgumentName = '';
let currentArgumentValue = '';
let currentArguments = [];
let currentNamedArguments = {};
let currentModifiers = [];
let state = 'action';
const getLastActionName = function () {
Expand All @@ -25,52 +23,30 @@ function parseDirectives(content) {
directives.push({
action: currentActionName,
args: currentArguments,
named: currentNamedArguments,
modifiers: currentModifiers,
getString: () => {
return content;
}
});
currentActionName = '';
currentArgumentName = '';
currentArgumentValue = '';
currentArguments = [];
currentNamedArguments = {};
currentModifiers = [];
state = 'action';
};
const pushArgument = function () {
const mixedArgTypesError = () => {
throw new Error(`Normal and named arguments cannot be mixed inside "${currentActionName}()"`);
};
if (currentArgumentName) {
if (currentArguments.length > 0) {
mixedArgTypesError();
}
currentNamedArguments[currentArgumentName.trim()] = currentArgumentValue;
}
else {
if (Object.keys(currentNamedArguments).length > 0) {
mixedArgTypesError();
}
currentArguments.push(currentArgumentValue.trim());
}
currentArgumentName = '';
currentArguments.push(currentArgumentValue.trim());
currentArgumentValue = '';
};
const pushModifier = function () {
if (currentArguments.length > 1) {
throw new Error(`The modifier "${currentActionName}()" does not support multiple arguments.`);
}
if (Object.keys(currentNamedArguments).length > 0) {
throw new Error(`The modifier "${currentActionName}()" does not support named arguments.`);
}
currentModifiers.push({
name: currentActionName,
value: currentArguments.length > 0 ? currentArguments[0] : null,
});
currentActionName = '';
currentArgumentName = '';
currentArguments = [];
state = 'action';
};
Expand Down Expand Up @@ -104,11 +80,6 @@ function parseDirectives(content) {
pushArgument();
break;
}
if (char === '=') {
currentArgumentName = currentArgumentValue;
currentArgumentValue = '';
break;
}
currentArgumentValue += char;
break;
case 'after_arguments':
Expand Down Expand Up @@ -325,7 +296,7 @@ function getAllModelDirectiveFromElements(element) {
}
const directives = parseDirectives(element.dataset.model);
directives.forEach((directive) => {
if (directive.args.length > 0 || directive.named.length > 0) {
if (directive.args.length > 0) {
throw new Error(`The data-model="${element.dataset.model}" format is invalid: it does not support passing arguments to the model.`);
}
directive.action = normalizeModelName(directive.action);
Expand All @@ -342,7 +313,7 @@ function getModelDirectiveFromElement(element, throwOnMissing = true) {
if (formElement && 'model' in formElement.dataset) {
const directives = parseDirectives(formElement.dataset.model || '*');
const directive = directives[0];
if (directive.args.length > 0 || directive.named.length > 0) {
if (directive.args.length > 0) {
throw new Error(`The data-model="${formElement.dataset.model}" format is invalid: it does not support passing arguments to the model.`);
}
directive.action = normalizeModelName(element.getAttribute('name'));
Expand Down Expand Up @@ -2942,15 +2913,18 @@ class LiveControllerDefault extends Controller {
this.updateModelFromElementEvent(event.currentTarget, null);
}
action(event) {
const rawAction = event.currentTarget.dataset.actionName;
const params = event.params;
if (!params.action) {
throw new Error(`No action name provided on element: ${getElementAsTagText(event.currentTarget)}. Did you forget to add the "data-live-action-param" attribute?`);
}
const rawAction = params.action;
const actionArgs = Object.assign({}, params);
delete actionArgs.action;
const directives = parseDirectives(rawAction);
let debounce = false;
directives.forEach((directive) => {
let pendingFiles = {};
const validModifiers = new Map();
validModifiers.set('prevent', () => {
event.preventDefault();
});
validModifiers.set('stop', () => {
event.stopPropagation();
});
Expand Down Expand Up @@ -2985,12 +2959,15 @@ class LiveControllerDefault extends Controller {
}
delete this.pendingFiles[key];
}
this.component.action(directive.action, directive.named, debounce);
this.component.action(directive.action, actionArgs, debounce);
if (getModelDirectiveFromElement(event.currentTarget, false)) {
this.pendingActionTriggerModelElement = event.currentTarget;
}
});
}
$render() {
return this.component.render();
}
emit(event) {
this.getEmitDirectives(event).forEach(({ name, data, nameMatch }) => {
this.component.emit(name, data, nameMatch);
Expand All @@ -3006,9 +2983,6 @@ class LiveControllerDefault extends Controller {
this.component.emitSelf(name, data);
});
}
$render() {
return this.component.render();
}
$updateModel(model, value, shouldRender = true, debounce = true) {
return this.component.set(model, value, shouldRender, debounce);
}
Expand All @@ -3019,11 +2993,13 @@ class LiveControllerDefault extends Controller {
this.component.fingerprint = this.fingerprintValue;
}
getEmitDirectives(event) {
const element = event.currentTarget;
if (!element.dataset.event) {
throw new Error(`No data-event attribute found on element: ${getElementAsTagText(element)}`);
const params = event.params;
if (!params.event) {
throw new Error(`No event name provided on element: ${getElementAsTagText(event.currentTarget)}. Did you forget to add the "data-live-event-param" attribute?`);
}
const eventInfo = element.dataset.event;
const eventInfo = params.event;
const eventArgs = Object.assign({}, params);
delete eventArgs.event;
const directives = parseDirectives(eventInfo);
const emits = [];
directives.forEach((directive) => {
Expand All @@ -3039,7 +3015,7 @@ class LiveControllerDefault extends Controller {
});
emits.push({
name: directive.action,
data: directive.named,
data: eventArgs,
nameMatch,
});
});
Expand Down
60 changes: 5 additions & 55 deletions src/LiveComponent/assets/src/Directive/directives_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ export interface Directive {
* An array of unnamed arguments passed to the action
*/
args: string[];
/**
* An object of named arguments
*/
named: any;
/**
* Any modifiers applied to the action
*/
Expand All @@ -41,17 +37,8 @@ export interface Directive {
* into an array of directives, with this format:
*
* [
* { action: 'addClass', args: ['foo'], named: {}, modifiers: [] },
* { action: 'removeAttribute', args: ['bar'], named: {}, modifiers: [] }
* ]
*
* This also handles named arguments
*
* save(foo=bar, baz=bazzles)
*
* Which would return:
* [
* { action: 'save', args: [], named: { foo: 'bar', baz: 'bazzles }, modifiers: [] }
* { action: 'addClass', args: ['foo'], modifiers: [] },
* { action: 'removeAttribute', args: ['bar'], modifiers: [] }
* ]
*
* @param {string} content The value of the attribute
Expand All @@ -64,10 +51,8 @@ export function parseDirectives(content: string|null): Directive[] {
}

let currentActionName = '';
let currentArgumentName = '';
let currentArgumentValue = '';
let currentArguments: string[] = [];
let currentNamedArguments: any = {};
let currentModifiers: { name: string, value: string | null }[] = [];
let state = 'action';

Expand All @@ -86,7 +71,6 @@ export function parseDirectives(content: string|null): Directive[] {
directives.push({
action: currentActionName,
args: currentArguments,
named: currentNamedArguments,
modifiers: currentModifiers,
getString: () => {
// TODO - make a string representation of JUST this directive
Expand All @@ -95,36 +79,15 @@ export function parseDirectives(content: string|null): Directive[] {
}
});
currentActionName = '';
currentArgumentName = '';
currentArgumentValue = '';
currentArguments = [];
currentNamedArguments = {};
currentModifiers = [];
state = 'action';
}
const pushArgument = function() {
const mixedArgTypesError = () => {
throw new Error(`Normal and named arguments cannot be mixed inside "${currentActionName}()"`)
}

if (currentArgumentName) {
if (currentArguments.length > 0) {
mixedArgTypesError();
}

// argument names are also trimmed to avoid space after ","
// "foo=bar, baz=bazzles"
currentNamedArguments[currentArgumentName.trim()] = currentArgumentValue;
} else {
if (Object.keys(currentNamedArguments).length > 0) {
mixedArgTypesError();
}

// value is trimmed to avoid space after ","
// "foo, bar"
currentArguments.push(currentArgumentValue.trim());
}
currentArgumentName = '';
// value is trimmed to avoid space after ","
// "foo, bar"
currentArguments.push(currentArgumentValue.trim());
currentArgumentValue = '';
}

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

if (Object.keys(currentNamedArguments).length > 0) {
throw new Error(`The modifier "${currentActionName}()" does not support named arguments.`)
}

currentModifiers.push({
name: currentActionName,
value: currentArguments.length > 0 ? currentArguments[0] : null,
});
currentActionName = '';
currentArgumentName = '';
currentArguments = [];
state = 'action';
}
Expand Down Expand Up @@ -196,14 +154,6 @@ export function parseDirectives(content: string|null): Directive[] {
break;
}

if (char === '=') {
// this is a named argument!
currentArgumentName = currentArgumentValue;
currentArgumentValue = '';

break;
}

// add next character to argument
currentArgumentValue += char;

Expand Down
4 changes: 2 additions & 2 deletions src/LiveComponent/assets/src/dom_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export function getAllModelDirectiveFromElements(element: HTMLElement): Directiv
const directives = parseDirectives(element.dataset.model);

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

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