UNPKG

@arminmajerie/dockview

Version:

Zero dependency layout manager supporting tabs, grids and splitviews (SolidJS only)

109 lines (108 loc) 4.1 kB
// packages/dockview/src/solid.tsx import { createSignal, createContext } from 'solid-js'; import { render } from 'solid-js/web'; // Context (use if you actually need context passing) export const SolidPartContext = createContext({}); // Main class. // Uses a signal + plain object spread so components always receive a POJO // (no Proxy). This is critical because SUID and other libraries crash when // they receive a SolidJS Store or mergeProps Proxy as their props object. // When update() is called, the signal bumps a version counter which causes // the component wrapper to re-execute, spreading a fresh POJO. export class SolidPart { parent; portalStore; component; parameters; context; ref; disposed = false; /** Accumulated prop overrides from update() calls */ overrides = {}; /** Signal to trigger re-render when overrides change */ triggerUpdate; version = 0; constructor(parent, portalStore, component, parameters, context) { this.parent = parent; this.portalStore = portalStore; this.component = component; this.parameters = parameters; this.context = context; this.createPortal(); } update(props) { if (this.disposed) { throw new Error("invalid operation: resource is already disposed"); } // Merge into overrides, then bump the signal to trigger re-render Object.assign(this.overrides, props); this.version++; this.triggerUpdate?.(this.version); } createPortal() { if (this.disposed) throw new Error("already disposed"); let cleanup; // Version signal — reading it inside the component creates a dependency. // When update() bumps it, SolidJS re-runs the component function. const [version, setVersion] = createSignal(0); this.triggerUpdate = setVersion; const baseParams = this.parameters; const overridesRef = this.overrides; const Comp = this.component; const ctx = this.context; const parentEl = this.parent; const ComponentWithContext = () => { // Return a FUNCTION so SolidJS treats it as a dynamic expression. // SolidJS will wrap this in a reactive effect, re-executing it // whenever the signals read inside change (i.e. when version bumps). // Previously we read version() in the component body, but component // functions only run ONCE — SolidJS doesn't re-call them. const dynamic = () => { const v = version(); const plainProps = { ...baseParams, ...overridesRef }; const result = Comp(plainProps); return result; }; return ctx ? (<SolidPartContext.Provider value={ctx}> {dynamic} </SolidPartContext.Provider>) : dynamic; }; cleanup = render(ComponentWithContext, parentEl); // Save for disposal this.ref = this.portalStore.addPortal({ dispose: () => { cleanup?.(); this.disposed = true; }, }); } dispose() { this.ref?.dispose(); this.disposed = true; } } /** * A React Hook that returns an array of portals to be rendered by the user of this hook * and a disposable function to add a portal. Calling dispose removes this portal from the * portal array */ export const usePortalsLifecycle = () => { const [portals, setPortals] = createSignal([]); const addPortal = (cleanup) => { setPortals(existing => [...existing, cleanup]); let disposed = false; return { dispose() { if (disposed) throw new Error("invalid operation: resource already disposed"); disposed = true; setPortals(existing => existing.filter(p => p !== cleanup)); cleanup.dispose(); } }; }; return [portals, addPortal]; };