Last active
August 1, 2022 19:46
-
-
Save tricki/e897f3e40c15fd4dfa089a0c942acf18 to your computer and use it in GitHub Desktop.
Stencil Async Helper (Promise + RXJS/Observables)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component, h, Host, State } from '@stencil/core'; | |
import { Subject } from 'rxjs'; | |
import { async } from '../../utils/StencilAsync'; | |
@Component({ | |
tag: 'async-test', | |
}) | |
export class RxjsTest { | |
test$ = new Subject<number>(); | |
testPromiseResolve: (value: unknown) => void; | |
testPromiseReject: (reason?: any) => void; | |
testPromise = new Promise<number>((resolve, reject) => (this.testPromiseResolve = resolve, this.testPromiseReject = reject)); | |
@State() useAsync = true; | |
addTestValue() { | |
this.test$.next(Math.random()); | |
} | |
render() { | |
console.log('render') | |
return ( | |
<Host> | |
<section> | |
<p> | |
Observable Value: {this.useAsync ? async(this.test$).toFixed() : 'Nothing'} | |
</p> | |
<button onClick={() => this.addTestValue()}>ADD VALUE</button> | |
</section> | |
<section> | |
<p> | |
Promise Value: {this.useAsync ? async(this.testPromise).toFixed() : 'Nothing'} | |
</p> | |
<button onClick={() => this.testPromiseResolve(Math.round(Math.random() * 100))}>RESOLVE VALUE</button> | |
<button onClick={() => this.testPromiseReject(Math.round(Math.random() * 100))}>REJECT VALUE</button> | |
</section> | |
<button onClick={() => this.useAsync = !this.useAsync}>TOGGLE ASYNC</button> | |
</Host> | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ComponentInterface, forceUpdate, getRenderingRef } from '@stencil/core'; | |
import type { Observable, Subscription } from 'rxjs'; | |
import { ComponentRegistration, ObservableRegistration } from "./types"; | |
export function async<T>(obj: Observable<T> | Promise<T>): T | undefined { | |
return getAsyncValue(obj); | |
} | |
export interface ComponentRegistration<T = any> { | |
promises: PromiseMap; | |
observables: Map<Observable<unknown>, ObservableRegistration<T>>; | |
/** | |
* An array of all observables that | |
* were used in the last render. | |
*/ | |
recentlyUsedObservables: Observable<unknown>[]; | |
origMethods: { | |
connectedCallback?: () => unknown, | |
disconnectedCallback?: () => unknown, | |
render?: () => unknown, | |
} | |
} | |
export type PromiseMap<T = any> = Map<Promise<T>, T | undefined>; | |
export interface ObservableRegistration<T> { | |
subscription: Subscription, | |
result?: T | undefined; | |
} | |
/** | |
* A Map of all components that are currently registered with stencil-async. | |
* | |
* @internal | |
*/ | |
const componentRegistrations = new Map<ComponentInterface, ComponentRegistration>(); | |
function init(component: ComponentInterface) { | |
if (componentRegistrations.has(component)) { | |
// component already initialized | |
return; | |
} | |
const compReg: ComponentRegistration = { | |
promises: new Map(), | |
observables: new Map(), | |
recentlyUsedObservables: [], | |
origMethods: { | |
connectedCallback: component.connectedCallback, | |
disconnectedCallback: component.disconnectedCallback, | |
render: component.render, | |
}, | |
}; | |
componentRegistrations.set(component, compReg); | |
component.connectedCallback = function () { | |
// reinit if reconnected | |
init(component); | |
if (compReg.origMethods.connectedCallback) { | |
compReg.origMethods.connectedCallback.call(component); | |
} | |
}; | |
component.disconnectedCallback = function () { | |
destroy(component); | |
if (compReg.origMethods.disconnectedCallback) { | |
compReg.origMethods.disconnectedCallback.call(component); | |
} | |
}; | |
component.render = function () { | |
if (!compReg.origMethods.render) { | |
// TODO should we still unsubscribe? | |
return; | |
} | |
compReg.recentlyUsedObservables = []; | |
const renderResult = compReg.origMethods.render.call(component); | |
// unsubscribe observables that are not used in `render()` anymore | |
[...compReg.observables.keys()] | |
.filter(obs => !compReg.recentlyUsedObservables.includes(obs)) | |
.forEach(unusedObs => unregisterObservable(component, unusedObs)); | |
if ( | |
compReg.recentlyUsedObservables.length === 0 | |
&& compReg.promises.size === 0 | |
) { | |
// completely remove stencil-async from the component | |
// TODO should this be optimized? | |
destroy(component); | |
} | |
return renderResult; | |
} | |
} | |
function destroy(component: ComponentInterface) { | |
if (!componentRegistrations.has(component)) { | |
return; | |
} | |
const compReg = componentRegistrations.get(component) as ComponentRegistration; | |
// unsubscribe all component observables | |
compReg.observables.forEach(obsReg => obsReg.subscription.unsubscribe()); | |
componentRegistrations.delete(component); | |
// reset the methods | |
component.connectedCallback = compReg.origMethods.connectedCallback; | |
component.disconnectedCallback = compReg.origMethods.disconnectedCallback; | |
component.render = compReg.origMethods.render; | |
} | |
function unregisterObservable(component: ComponentInterface, observable: Observable<unknown>) { | |
if (!componentRegistrations.has(component)) { | |
return; | |
} | |
const compReg = componentRegistrations.get(component) as ComponentRegistration; | |
const observableRegistration = compReg.observables.get(observable); | |
observableRegistration?.subscription.unsubscribe(); | |
compReg.observables.delete(observable); | |
} | |
function getComponentRegistration(component: ComponentInterface): ComponentRegistration { | |
if (!componentRegistrations.has(component)) { | |
// add registration if it doesn't exist | |
init(component); | |
} | |
return componentRegistrations.get(component)!; | |
} | |
function getAsyncValue<T>(obj: Observable<T> | Promise<T>): T | undefined { | |
if (isPromise(obj)) { | |
return getPromiseValue(obj as Promise<T>); | |
} | |
if (isSubscribable(obj)) { | |
return getObservableValue(obj as Observable<T>); | |
} | |
console.error('Invalid value: ', typeof obj, obj); | |
} | |
function getPromiseValue<T>(promise: Promise<T>): T | undefined { | |
const component = getRenderingRef(); | |
const compReg = getComponentRegistration(component); | |
if (!compReg.promises.has(promise)) { | |
compReg.promises.set(promise, null); | |
promise.then((...res) => { | |
compReg.promises.set(promise, res); | |
forceUpdate(component); | |
}); | |
} | |
const value = compReg.promises.get(promise); | |
// don't clean up or it will cause a | |
// rerender after every render | |
// if (value) { | |
// // clean up | |
// compReg.promises.delete(promise); | |
// } | |
return value; | |
} | |
function getObservableValue<T>(obs$: Observable<T>): T | undefined { | |
// This function is not really exported by @stencil/core. | |
// Taken from @stencil/store. | |
// @source https://github.com/ionic-team/stencil-store/blob/master/src/subscriptions/stencil.ts#L35 | |
const component = getRenderingRef(); | |
const compReg = getComponentRegistration(component); | |
compReg.recentlyUsedObservables.push(obs$); | |
if (!compReg.observables.has(obs$)) { | |
// subscribe | |
// We need to create an empty object first | |
// because the observable might fire immediately. | |
const observableReg: Partial<ObservableRegistration<T>> = {}; | |
observableReg.subscription = obs$.subscribe(result => { | |
observableReg.result = result; | |
forceUpdate(component); | |
}); | |
compReg.observables.set(obs$, observableReg as ObservableRegistration<T>); | |
} | |
return compReg.observables.get(obs$)?.result; | |
} | |
export function isPromise(obj: any) { | |
return !!obj && typeof obj.then === 'function'; | |
} | |
export function isSubscribable(obj: any) { | |
return !!obj && typeof obj.subscribe === 'function' | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment