Skip to content

[Live] Add data-error attribute + fix loading behaviour on error #1916

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

Open
wants to merge 13 commits into
base: 2.x
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion src/Dropzone/assets/dist/style.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/Dropzone/assets/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
}

.dropzone-preview-button::before {
content: '×';
content: "×";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this :)

We'll make the change in another small PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never made that change, I think that's just the linter ?

padding: 3px 7px;
cursor: pointer;
}
Expand Down
27 changes: 27 additions & 0 deletions src/LiveComponent/assets/dist/Component/plugins/ErrorPlugin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Component } from '../../live_controller';
import type { PluginInterface } from './PluginInterface';
export default class ErrorPlugin implements PluginInterface {
static errorAttribute: string;
static isErrorAttribute: string;
static supportedActions: {
show: string;
hide: string;
addClass: string;
removeClass: string;
addAttribute: string;
removeAttribute: string;
};
attachToComponent(component: Component): void;
showErrors(component: Component): void;
hideErrors(component: Component): void;
private handleErrorToggle;
private handleErrorDirective;
private getErrorDirectives;
private parseErrorAction;
private showElement;
private hideElement;
private addClass;
private removeClass;
private addAttributes;
private removeAttributes;
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { type Directive } from '../../Directive/directives_parser';
import { type ElementDirectives } from '../../Directive/directives_parser';
import type BackendRequest from '../../Backend/BackendRequest';
import type Component from '../../Component';
import type { PluginInterface } from './PluginInterface';
interface ElementLoadingDirectives {
element: HTMLElement | SVGElement;
directives: Directive[];
}
export default class implements PluginInterface {
attachToComponent(component: Component): void;
startLoading(component: Component, targetElement: HTMLElement | SVGElement, backendRequest: BackendRequest): void;
finishLoading(component: Component, targetElement: HTMLElement | SVGElement): void;
private handleLoadingToggle;
private handleLoadingDirective;
getLoadingDirectives(component: Component, element: HTMLElement | SVGElement): ElementLoadingDirectives[];
getLoadingDirectives(component: Component, element: HTMLElement | SVGElement): ElementDirectives[];
private showElement;
private hideElement;
private addClass;
private removeClass;
private addAttributes;
private removeAttributes;
}
export {};
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ export interface Directive {
modifiers: DirectiveModifier[];
getString: () => string;
}
export interface ElementDirectives {
element: HTMLElement | SVGElement;
directives: Directive[];
}
export declare function parseDirectives(content: string | null): Directive[];
2 changes: 1 addition & 1 deletion src/LiveComponent/assets/dist/HookManager.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ComponentHookName, ComponentHookCallback } from './Component';
import type { ComponentHookCallback, ComponentHookName } from './Component';
export default class {
private hooks;
register<T extends string | ComponentHookName = ComponentHookName>(hookName: T, callback: ComponentHookCallback<T>): void;
Expand Down
2 changes: 1 addition & 1 deletion src/LiveComponent/assets/dist/live.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

139 changes: 139 additions & 0 deletions src/LiveComponent/assets/dist/live_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2043,12 +2043,17 @@ class Component {
!headers.get('X-Live-Redirect')) {
const controls = { displayError: true };
this.valueStore.pushPendingPropsBackToDirty();
this.hooks.triggerHook('loading.state:finished', this.element);
this.hooks.triggerHook('response:error', backendResponse, controls);
if (controls.displayError) {
this.renderError(html);
}
this.backendRequest = null;
thisPromiseResolve(backendResponse);
if (this.isRequestPending) {
this.isRequestPending = false;
this.performRequest();
}
return response;
}
this.processRerender(html, backendResponse);
Expand Down Expand Up @@ -2946,6 +2951,139 @@ class LazyPlugin {
}
}

class ErrorPlugin {
attachToComponent(component) {
component.on('response:error', () => {
this.showErrors(component);
});
component.on('request:started', () => {
this.hideErrors(component);
});
this.hideErrors(component);
}
showErrors(component) {
this.handleErrorToggle(component, true, component.element);
}
hideErrors(component) {
this.handleErrorToggle(component, false, component.element);
}
handleErrorToggle(component, isError, targetElement) {
this.getErrorDirectives(component, targetElement).forEach(({ element, directives }) => {
if (isError) {
this.addAttributes(element, [ErrorPlugin.isErrorAttribute]);
}
else {
this.removeAttributes(element, [ErrorPlugin.isErrorAttribute]);
}
directives.forEach((directive) => {
this.handleErrorDirective(element, isError, directive);
});
});
}
handleErrorDirective(element, isError, directive) {
const finalAction = this.parseErrorAction(directive.action, isError);
switch (finalAction) {
case ErrorPlugin.supportedActions.show:
this.showElement(element);
break;
case ErrorPlugin.supportedActions.hide:
this.hideElement(element);
break;
case ErrorPlugin.supportedActions.addClass:
this.addClass(element, directive.args);
break;
case ErrorPlugin.supportedActions.removeClass:
this.removeClass(element, directive.args);
break;
case ErrorPlugin.supportedActions.addAttribute:
this.addAttributes(element, directive.args);
break;
case ErrorPlugin.supportedActions.removeAttribute:
this.removeAttributes(element, directive.args);
break;
default:
throw new Error(`Unknown ${ErrorPlugin.errorAttribute} action "${finalAction}". Supported actions are: ${Object.values(`"${ErrorPlugin.supportedActions}"`).join(', ')}.`);
}
}
getErrorDirectives(component, element) {
const errorDirectives = [];
let matchingElements = [...Array.from(element.querySelectorAll(`[${ErrorPlugin.errorAttribute}]`))];
matchingElements = matchingElements.filter((elt) => elementBelongsToThisComponent(elt, component));
if (element.hasAttribute(ErrorPlugin.errorAttribute)) {
matchingElements = [element, ...matchingElements];
}
matchingElements.forEach((element) => {
if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) {
throw new Error(`Element "${element.nodeName}" is not supported for ${ErrorPlugin.errorAttribute} directives. Only HTMLElement and SVGElement are supported.`);
}
const directives = parseDirectives(element.getAttribute(ErrorPlugin.errorAttribute) || 'show');
directives.forEach((directive) => {
if (directive.modifiers.length > 0) {
throw new Error(`Modifiers are not supported for ${ErrorPlugin.errorAttribute} directives, but the following were found: "{${directive.modifiers.map((modifier) => `${modifier.name}: ${modifier.value}}`).join(', ')}" for element with tag "${element.nodeName}".`);
}
});
errorDirectives.push({
element,
directives,
});
});
return errorDirectives;
}
parseErrorAction(action, isError) {
switch (action) {
case ErrorPlugin.supportedActions.show:
return isError ? 'show' : 'hide';
case ErrorPlugin.supportedActions.hide:
return isError ? 'hide' : 'show';
case ErrorPlugin.supportedActions.addClass:
return isError ? 'addClass' : 'removeClass';
case ErrorPlugin.supportedActions.removeClass:
return isError ? 'removeClass' : 'addClass';
case ErrorPlugin.supportedActions.addAttribute:
return isError ? 'addAttribute' : 'removeAttribute';
case ErrorPlugin.supportedActions.removeAttribute:
return isError ? 'removeAttribute' : 'addAttribute';
default:
throw new Error(`Unknown ${ErrorPlugin.errorAttribute} action "${action}". Supported actions are: ${Object.values(`"${ErrorPlugin.supportedActions}"`).join(', ')}.`);
}
}
showElement(element) {
element.style.display = 'revert';
}
hideElement(element) {
element.style.display = 'none';
}
addClass(element, classes) {
element.classList.add(...combineSpacedArray(classes));
}
removeClass(element, classes) {
element.classList.remove(...combineSpacedArray(classes));
if (element.classList.length === 0) {
element.removeAttribute('class');
}
}
addAttributes(element, attributes) {
attributes.forEach((attribute) => {
element.setAttribute(attribute, '');
});
}
removeAttributes(element, attributes) {
attributes.forEach((attribute) => {
element.removeAttribute(attribute);
});
}
}
ErrorPlugin.errorAttribute = 'data-live-error';
ErrorPlugin.isErrorAttribute = 'data-live-is-error';
ErrorPlugin.supportedActions = {
show: 'show',
hide: 'hide',
addClass: 'addClass',
removeClass: 'removeClass',
addAttribute: 'addAttribute',
removeAttribute: 'removeAttribute',
};

class LiveControllerDefault extends Controller {
constructor() {
super(...arguments);
Expand Down Expand Up @@ -3094,6 +3232,7 @@ class LiveControllerDefault extends Controller {
}
const plugins = [
new LoadingPlugin(),
new ErrorPlugin(),
new LazyPlugin(),
new ValidatedFieldsPlugin(),
new PageUnloadingPlugin(),
Expand Down
8 changes: 8 additions & 0 deletions src/LiveComponent/assets/src/Component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ export default class Component {
) {
const controls = { displayError: true };
this.valueStore.pushPendingPropsBackToDirty();
this.hooks.triggerHook('loading.state:finished', this.element);
this.hooks.triggerHook('response:error', backendResponse, controls);

if (controls.displayError) {
Expand All @@ -324,6 +325,13 @@ export default class Component {
this.backendRequest = null;
thisPromiseResolve(backendResponse);

// If there's another request pending, perform it now
// This will also ensure that the error state is cleared
if (this.isRequestPending) {
this.isRequestPending = false;
this.performRequest();
}

return response;
}

Expand Down
Loading