Skip to content

[WIP] Simplifying and clarifying the Component constructor and responsibilities #1426

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

Closed
wants to merge 1 commit into from
Closed
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
14 changes: 5 additions & 9 deletions src/LiveComponent/assets/src/Component/ElementDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,23 @@ import {getModelDirectiveFromElement} from '../dom_utils';
export interface ElementDriver {
getModelName(element: HTMLElement): string|null;

getComponentProps(rootElement: HTMLElement): any;
getComponentProps(): any;

/**
* Given an HtmlElement and a child id, find the root element for that child.
* TODO: make this part of an options array to Component
*/
findChildComponentElement(id: string, element: HTMLElement): HTMLElement|null;

/**
* Given an element, find the "key" that should be used to identify it;
*/
getKeyFromElement(element: HTMLElement): string|null;

/**
* Given an element from a response, find all the events that should be emitted.
*/
getEventsToEmit(element: HTMLElement): Array<{event: string, data: any, target: string|null, componentName: string|null }>;
getEventsToEmit(): Array<{event: string, data: any, target: string|null, componentName: string|null }>;

/**
* Given an element from a response, find all the events that should be dispatched.
*/
getBrowserEventsToDispatch(element: HTMLElement): Array<{event: string, payload: any }>;
getBrowserEventsToDispatch(): Array<{event: string, payload: any }>;
}

export class StandardElementDriver implements ElementDriver {
Expand All @@ -43,7 +39,7 @@ export class StandardElementDriver implements ElementDriver {
return JSON.parse(propsJson);
}

findChildComponentElement(id: string, element: HTMLElement): HTMLElement|null {
findChildComponentElement(id: string): HTMLElement|null {
return element.querySelector(`[data-live-id=${id}]`);
}

Expand Down
48 changes: 26 additions & 22 deletions src/LiveComponent/assets/src/Component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ import { PluginInterface } from './plugins/PluginInterface';
import BackendResponse from '../Backend/BackendResponse';
import { ModelBinding } from '../Directive/get_model_binding';
import ExternalMutationTracker from '../Rendering/ExternalMutationTracker';
import ComponentRegistry from '../ComponentRegistry';

declare const Turbo: any;

export type ComponentFinder = (currentComponent: Component, onlyParents: boolean, onlyMatchName: string|null) => Component[];

class ChildComponentWrapper {
component: Component;
modelBindings: ModelBinding[];
Expand All @@ -30,12 +29,13 @@ export default class Component {
readonly element: HTMLElement;
readonly name: string;
// key is the string event name and value is an array of action names
readonly listeners: Map<string, string[]>;
private readonly componentFinder: ComponentFinder;
readonly listeners: Map<string, string[]> = new Map();
private backend: BackendInterface;
private readonly elementDriver: ElementDriver;
id: string|null;

static componentRegistry = new ComponentRegistry();

/**
* A fingerprint that identifies the props/input that was used on
* the server to create this component, especially if it was a
Expand Down Expand Up @@ -67,36 +67,29 @@ export default class Component {
private children: Map<string, ChildComponentWrapper> = new Map();
private parent: Component|null = null;

private externalMutationTracker: ExternalMutationTracker;
private readonly externalMutationTracker: ExternalMutationTracker;

/**
* @param element The root element
* @param name The name of the component
* @param props Readonly component props
* @param listeners Array of event -> action listeners
* @param componentFinder
* @param fingerprint
* @param name Component name - used for emitting events to specific components
* @param props Starting component props
* @param fingerprint A deterministic fingerprint generated from the input/props
* used to create this component. When a parent component re-renders,
* it will send this fingerprint to the server. This is used to
* determine if the input/props to this child component have changed
* and thus, if the child component needs to be re-rendered.
* @param id Some unique id to identify this component. Needed to be a child component
* @param backend Backend instance for updating
* @param elementDriver Class to get "model" name from any element.
*/
constructor(element: HTMLElement, name: string, props: any, listeners: Array<{ event: string; action: string }>, componentFinder: ComponentFinder, fingerprint: string|null, id: string|null, backend: BackendInterface, elementDriver: ElementDriver) {
constructor(element: HTMLElement, name: string, props: any, fingerprint: string|null, id: string|null, backend: BackendInterface, elementDriver: ElementDriver) {
this.element = element;
this.name = name;
this.componentFinder = componentFinder;
this.backend = backend;
this.elementDriver = elementDriver;
this.id = id;
this.fingerprint = fingerprint;

this.listeners = new Map();
listeners.forEach((listener) => {
if (!this.listeners.has(listener.event)) {
this.listeners.set(listener.event, []);
}
this.listeners.get(listener.event)?.push(listener.action);
});

this.valueStore = new ValueStore(props);
this.unsyncedInputsTracker = new UnsyncedInputsTracker(this, elementDriver);
this.hooks = new HookManager();
Expand Down Expand Up @@ -124,13 +117,25 @@ export default class Component {
plugin.attachToComponent(this);
}

setListeners(listeners: Array<{ event: string; action: string }>) {
this.listeners.clear();
listeners.forEach((listener) => {
if (!this.listeners.has(listener.event)) {
this.listeners.set(listener.event, []);
}
this.listeners.get(listener.event)?.push(listener.action);
});
}

connect(): void {
Component.componentRegistry.registerComponent(this);
this.hooks.triggerHook('connect', this);
this.unsyncedInputsTracker.activate();
this.externalMutationTracker.start();
}

disconnect(): void {
Component.componentRegistry.unregisterComponent(this);
this.hooks.triggerHook('disconnect', this);
this.clearRequestDebounceTimeout();
this.unsyncedInputsTracker.deactivate();
Expand Down Expand Up @@ -265,7 +270,7 @@ export default class Component {
}

private performEmit(name: string, data: any, emitUp: boolean, matchingName: string|null): void {
const components = this.componentFinder(this, emitUp, matchingName);
const components = Component.componentRegistry.findComponents(this, emitUp, matchingName);
components.forEach((component) => {
component.doEmit(name, data);
});
Expand Down Expand Up @@ -494,7 +499,6 @@ export default class Component {
(element: HTMLElement) => getValueFromElement(element, this.valueStore),
Array.from(this.getChildren().values()),
this.elementDriver.findChildComponentElement,
this.elementDriver.getKeyFromElement,
this.externalMutationTracker
);
this.externalMutationTracker.start();
Expand Down
4 changes: 2 additions & 2 deletions src/LiveComponent/assets/src/ComponentRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export default class {
*/
private componentMapByComponent = new Map<Component, string>();

public registerComponent(element: HTMLElement, component: Component) {
this.componentMapByElement.set(element, component);
public registerComponent(component: Component) {
this.componentMapByElement.set(component.element, component);
this.componentMapByComponent.set(component, component.name);
}

Expand Down
12 changes: 3 additions & 9 deletions src/LiveComponent/assets/src/live_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ import PollingPlugin from './Component/plugins/PollingPlugin';
import SetValueOntoModelFieldsPlugin from './Component/plugins/SetValueOntoModelFieldsPlugin';
import { PluginInterface } from './Component/plugins/PluginInterface';
import getModelBinding from './Directive/get_model_binding';
import ComponentRegistry from './ComponentRegistry';
import QueryStringPlugin from './Component/plugins/QueryStringPlugin';

export { Component };
export const getComponent = (element: HTMLElement): Promise<Component> =>
LiveControllerDefault.componentRegistry.getComponent(element);
Component.componentRegistry.getComponent(element);

export interface LiveEvent extends CustomEvent {
detail: {
Expand Down Expand Up @@ -73,8 +72,6 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
];
private pendingFiles: { [key: string]: HTMLInputElement } = {};

static componentRegistry = new ComponentRegistry();

initialize() {
this.handleDisconnectedChildControllerEvent = this.handleDisconnectedChildControllerEvent.bind(this);

Expand All @@ -84,14 +81,13 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
this.element,
this.nameValue,
this.propsValue,
this.listenersValue,
(currentComponent: Component, onlyParents: boolean, onlyMatchName: string | null) =>
LiveControllerDefault.componentRegistry.findComponents(currentComponent, onlyParents, onlyMatchName),
this.fingerprintValue,
id,
new Backend(this.urlValue, this.requestMethodValue, this.csrfValue),
new StandardElementDriver()
);
// TODO: reset listeners on value change?
this.component.setListeners(this.listenersValue);
this.proxiedComponent = proxifyComponent(this.component);

// @ts-ignore Adding the dynamic property
Expand All @@ -115,7 +111,6 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
}

connect() {
LiveControllerDefault.componentRegistry.registerComponent(this.element, this.component);
this.component.connect();

this.elementEventListeners.forEach(({ event, callback }) => {
Expand All @@ -126,7 +121,6 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
}

disconnect() {
LiveControllerDefault.componentRegistry.unregisterComponent(this.component);
this.component.disconnect();

this.elementEventListeners.forEach(({ event, callback }) => {
Expand Down
1 change: 0 additions & 1 deletion src/LiveComponent/assets/src/morphdom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export function executeMorphdom(
getElementValue: (element: HTMLElement) => any,
childComponents: Component[],
findChildComponent: (id: string, element: HTMLElement) => HTMLElement | null,
getKeyFromElement: (element: HTMLElement) => string | null,
externalMutationTracker: ExternalMutationTracker
) {
const childComponentMap: Map<HTMLElement, Component> = new Map();
Expand Down