Skip to content
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

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

Open
wants to merge 9 commits into
base: 2.x
Choose a base branch
from
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 { Directive } from '../../Directive/directives_parser';
import { ElementDirectives } from '../../Directive/directives_parser';
import BackendRequest from '../../Backend/BackendRequest';
import Component from '../../Component';
import { 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 @@ -10,4 +10,8 @@ export interface Directive {
(): 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/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 @@ -2059,12 +2059,17 @@ class Component {
if (!((_a = headers.get('Content-Type')) === null || _a === void 0 ? void 0 : _a.includes('application/vnd.live-component+html')) && !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 @@ -2949,6 +2954,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 @@ -3098,6 +3236,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 @@ -299,6 +299,7 @@ export default class Component {
if (!headers.get('Content-Type')?.includes('application/vnd.live-component+html') && !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) {
Expand All @@ -308,6 +309,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