Skip to content

Instantly share code, notes, and snippets.

@trusktr
Last active February 2, 2024 17:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trusktr/00c42b913350fe95fd1d1b3868a21e0b to your computer and use it in GitHub Desktop.
Save trusktr/00c42b913350fe95fd1d1b3868a21e0b to your computer and use it in GitHub Desktop.
Solid.js directives
import {createEffect, untrack} from 'solid-js';
type GetterSetter = [() => string, (v: string) => void];
type ObjectAndKey = [Record<string, unknown>, string]; // TODO better type
/**
* Use this to make a two-way binding from an input to a signal or a reactive
* object such as a store or mutable. F.e.
*
* ```js
* return <input use:model={[someSignal, setSomeSignal]} />
* ```
*
* or
*
* ```js
* return <input use:model={[storeOrMutable.some.path.to.object, 'someProp']} />
* ```
*/
export function model(
input: HTMLSelectElement | HTMLInputElement,
accessor: () => GetterSetter | ObjectAndKey,
) {
const [getValue, setValue] = accessor();
let _get: () => string, _set: (v: string) => void;
if (typeof getValue === 'object' && typeof setValue === 'string') {
const obj = getValue;
const key = setValue;
_get = () => String(obj[key]);
_set = v => (obj[key] = v);
} else if (typeof getValue === 'function' && typeof setValue === 'function') {
_get = getValue;
_set = setValue;
} else {
throw new Error('invalid args passed to use:model');
}
input.addEventListener('input', () => untrack(() => _set(input.value)));
createEffect(() => (input.value = _get()));
}
declare module 'solid-js' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
interface Directives {
model: GetterSetter | ObjectAndKey;
}
}
}
import {render} from 'solid-js/web'
import type {JSX} from 'solid-js'
/**
* Use this to give an element a ShadowRoot. Can be useful for vanilla native CSS
* scoping without a CSS framework/library, or for adding a native <slot> mechanism
* to your Solid components. F.e.
*
* ```js
* export function MySolidComponent(props) {
* return <div use:shadow={
* <div>
* <p>This is ShadowDOM content.</p>
* <slot name="foo">This default content is replaced by any children with slot="foo" attributes.</slot>
* <style note="this style is scoped!">
* p {color: royalblue}
* </style>
* </div>
* {props.children}
* } />
* }
* ```
*
* Provide ShadowRoot options:
*
* ```js
* return <div use:shadow={[<>...</>, {mode: 'closed'}]} />
* ```
*
* Get the ShadowRoot instance:
*
* ```js
* const [root, setRoot] = createSignal<ShadowRoot>()
*
* createEffect(() => console.log('root:': root()))
*
* return <div use:shadow={[<>...</>, {...options}, setRoot]} />
* ```
*
* Why is this a directive, and not a <ShadowRoot> component? Who knows!
*/
export async function shadow(el: Element, args: () => (JSX.Element | (() => JSX.Element)) | ShadowArgsTuple | true) {
const _args = args()
const [shadowChildren, shadowOptions = {mode: 'open'}, setRoot] =
_args === true
? [() => <></>] // no args
: isShadowArgTuple(_args)
? typeof _args[0] === 'function'
? _args
: [() => _args[0], _args[1], _args[2]]
: [() => _args]
// FIXME HACKY: Defer for one microtask so custom element upgrades can happen. Will this always work?
await Promise.resolve()
if (el.tagName.includes('-') && !customElements.get(el.tagName.toLowerCase())) {
await Promise.race([
new Promise<void>(resolve =>
setTimeout(() => {
console.warn(
'Custom element is not defined after 1 second, skipping. Overriden attachShadow methods may break if the element is defined later.',
)
resolve()
}, 1000),
),
customElements.whenDefined(el.tagName.toLowerCase()),
])
}
try {
const root = el.attachShadow(shadowOptions)
render(
// @ts-expect-error it works
shadowChildren,
root,
)
setRoot?.(root)
} catch (e) {
console.warn('skipped making a new root:')
console.error(e)
}
}
type ShadowArgsTuple = [
el: JSX.Element | (() => JSX.Element),
init: ShadowRootInit,
setRoot?: (root: ShadowRoot) => void,
]
function isShadowArgTuple(a: any): a is ShadowArgsTuple {
if (Array.isArray(a) && (a.length === 2 || a.length === 3) && 'mode' in a[1]) return true
return false
}
declare module 'solid-js' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
interface Directives {
shadow: JSX.Element | ShadowArgsTuple | true
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment