Last active
February 2, 2024 17:46
-
-
Save trusktr/00c42b913350fe95fd1d1b3868a21e0b to your computer and use it in GitHub Desktop.
Solid.js directives
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 {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; | |
} | |
} | |
} |
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 {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