@arminmajerie/dockview
Version:
Zero dependency layout manager supporting tabs, grids and splitviews (SolidJS only)
109 lines (108 loc) • 4.1 kB
JSX
// 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];
};