Skip to content

Commit

Permalink
feat(components): add reactive HostController and LoadingController |…
Browse files Browse the repository at this point in the history
… bh | #3016
  • Loading branch information
denyo committed Feb 13, 2024
1 parent 38c6c22 commit 6393381
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 0 deletions.
139 changes: 139 additions & 0 deletions packages/components/src/controllers/host-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { type ComponentInterface, forceUpdate } from '@stencil/core';

// inspired by lit
// https://github.com/lit/lit/blob/59d5ba7649f2957d01996e3115d0d79bdfbf34ac/packages/reactive-element/src/reactive-controller.ts#L11

/**
* An object that can host Reactive Controllers and call their lifecycle
* callbacks.
*/
export type ReactiveControllerHost<T extends object> = {
host: ComponentInterface & T;

/**
* Adds a controller to the host, which sets up the controller's lifecycle
* methods to be called with the host's lifecycle.
*/
addController(controller: ReactiveController): void;

/**
* Removes a controller from the host.
*/
removeController(controller: ReactiveController): void;

/**
* Requests a host update which is processed asynchronously. The update can
* be waited on via the `updateComplete` property.
*/
requestUpdate(): void;

/**
* Returns a Promise that resolves when the host has completed updating.
* The Promise value is a boolean that is `true` if the element completed the
* update without triggering another update. The Promise result is `false` if
* a property was set inside `updated()`. If the Promise is rejected, an
* exception was thrown during the update.
*
* @return A promise of a boolean that indicates if the update resolved
* without triggering another update.
*/
// readonly updateComplete: Promise<boolean>;
};

/**
* A Reactive Controller is an object that enables sub-component code
* organization and reuse by aggregating the state, behavior, and lifecycle
* hooks related to a single feature.
*
* Controllers are added to a host component, or other object that implements
* the `ReactiveControllerHost` interface, via the `addController()` method.
* They can hook their host components's lifecycle by implementing one or more
* of the lifecycle callbacks, or initiate an update of the host component by
* calling `requestUpdate()` on the host.
*/
export type ReactiveController = {
/**
* Called when the host is connected to the component tree. For custom
* element hosts, this corresponds to the `connectedCallback()` lifecycle,
* which is only called when the component is connected to the document.
*/
hostConnected?(): void;

/**
* Called when the host is disconnected from the component tree. For custom
* element hosts, this corresponds to the `disconnectedCallback()` lifecycle,
* which is called the host or an ancestor component is disconnected from the
* document.
*/
hostDisconnected?(): void;

/**
* Called during the client-side host update, just before the host calls
* its own update.
*
* Code in `update()` can depend on the DOM as it is not called in
* server-side rendering.
*/
hostWillUpdate?(): void;

/**
* Called after a host update, just before the host calls firstUpdated and
* updated. It is not called in server-side rendering.
*/
hostDidUpdate?(): void;

hostWillLoad?(): void;
hostDidLoad?(): void;
};

export class ControllerHost<T extends object> implements ReactiveControllerHost<T> {
private controllers = new Set<ReactiveController>();

constructor(public host: ComponentInterface & T) {
const {
connectedCallback,
disconnectedCallback,
componentWillLoad,
componentDidLoad,
componentWillUpdate,
componentDidUpdate,
} = host;

host.connectedCallback = (): void => {
this.controllers.forEach((ctrl) => ctrl.hostConnected?.());
connectedCallback?.apply(host);
};
host.disconnectedCallback = (): void => {
this.controllers.forEach((ctrl) => ctrl.hostDisconnected?.());
disconnectedCallback?.apply(host);
};
host.componentWillLoad = (): void => {
this.controllers.forEach((ctrl) => ctrl.hostWillLoad?.());
componentWillLoad?.apply(host);
};
host.componentDidLoad = (): void => {
this.controllers.forEach((ctrl) => ctrl.hostDidLoad?.());
componentDidLoad?.apply(host);
};
host.componentWillUpdate = (): void => {
this.controllers.forEach((ctrl) => ctrl.hostWillUpdate?.());
componentWillUpdate?.apply(host);
};
host.componentDidUpdate = (): void => {
this.controllers.forEach((ctrl) => ctrl.hostDidUpdate?.());
componentDidUpdate?.apply(host);
};
}

public addController(ctrl: ReactiveController): void {
this.controllers.add(ctrl);
}

public removeController(ctrl: ReactiveController): void {
this.controllers.delete(ctrl);
}

public requestUpdate(): void {
forceUpdate(this.host);
}
}
2 changes: 2 additions & 0 deletions packages/components/src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './host-controller';
export * from './loading-controller';
23 changes: 23 additions & 0 deletions packages/components/src/controllers/loading-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ReactiveController, ReactiveControllerHost } from './host-controller';

export class LoadingController implements ReactiveController {
public initialLoading: boolean = false;

constructor(private controllerHost: ReactiveControllerHost<{ loading?: boolean }>) {
this.controllerHost.addController(this);
}

hostConnected() {
this.initialLoading = this.controllerHost.host.loading;
}

hostWillLoad() {
this.initialLoading = this.controllerHost.host.loading;
}

hostWillUpdate() {
if (this.controllerHost.host.loading) {
this.initialLoading = true;
}
}
}

0 comments on commit 6393381

Please sign in to comment.