object style hooks / i18n / global state management for solid.js
npm i solid-dollar
yarn add solid-dollar
pnpm add solid-dollar
object wrapper for createSignal
import { $ } from 'solid-dollar'
const data = $(0)
console.log(data()) // 0
console.log(data.$set(1)) // set value
untrack
alias
object wrapper for createMemo
import { $, $memo } from 'solid-dollar'
const test = $('test')
const memo = $memo(() => `value: ${test()}`)
object wrapper for createStore
import { $store } from 'solid-dollar'
const store = $store({ test: { deep: 1 } })
store() // { test: { deep: 1 } }
store.$set('test', 'deep', 2) // set value
patch update store
import { $store, $patchStore } from 'solid-dollar'
const store = $store({ data: { deep: 1 } })
$patchStore(store, data => { data.deep = 2 })
$patchStore(store, { data: { deep: 3 } })
object wrapper for createResource
import { $, $resource } from 'solid-dollar'
const fetcher = (source: string) => Promise.resolve(`${source} data`)
const source = $('source')
const data = $resource(source, fetcher, {
initialValue: 'test'
})
data() // 'test'
data.loading // true
data.state // pending
await Promise.resolve()
data() // 'source data'
data.loading // false
data.state // ready
data.$mutate()
data.$refetch()
pausable and filterable createEffect(on())
, defer by default
import { $watch } from 'solid-dollar'
import { throttle } from '@solid-primitives/scheduled'
const str = $('old')
function filter(newValue: string, times: number) {
return newValue !== 'new'
}
function callback<T>(value: T, oldValue: T, callTimes: number) {
console.log(value, oldValue, callTimes)
}
const {
isWatching,
pause,
resume,
runWithoutEffect,
} = $watch(str, callback, {
// function for trigger callback
triggerFn: fn => throttle(fn, 500),
// function for filter value
filterFn: filter,
// createEffect `onOptions.defer`, default is true
defer: false,
})
normal effect, alias for createEffect
$watch
but createRenderEffect(on())
run effect after rendered, be able to access DOM, alias for createRenderEffect
$watch
but createComputed(on())
run effect instantly, alias for createComputed
defer update notification until browser idle, alias for createDeferred
object wrapper for createSelector
import { For } from 'solid-js'
import { $selector } from 'solid-dollar'
const activeId = $selector(0)
activeId.$set(1)
return (
<For each={list()}>
{item => (
<li classList={{ active: activeId.$bind(item.id) }}>
{item.name}
</li>
)}
</For>
)
convert binary to object url
import { $objectURL } from 'solid-dollar'
const url = $objectURL(new Blob())
const url = $objectURL(new MediaSource())
const url = $objectURL(new Uint8Array(), { type: 'image/png' })
patch update array signal
import { $, $patchArray } from 'solid-dollar'
const arr = $<number[]>([1])
arr() // [1]
$patchArray(arr, a => a.push(3)) // update by mutating it in-place
arr() // [1, 3]
make plain object props reactive
import { $reactive } from 'solid-dollar'
const value = {
deep: {
data: 'str',
},
}
const bar = $reactive(value, 'deep.data')
bar() // 'str'
bar.$set('updated') // 'update'
bar() // 'updated'
global state with auto persistence
support run without provider (fallback to createRoot
)
inspired by pinia
& zustand
import { $state, GlobalStateProvider, useGetters, useActions } from 'solid-dollar/state'
const useTestState = $state('test', {
init: { value: 1, deep: { data: 'hello' } },
getter: state => ({
// without param, will auto wrapped with `createMemo`
doubleValue() {
return state.value * 2
},
}),
action: stateObj => ({
plus(num: number) {
stateObj.$set('value', value => value + num)
},
}),
persist: {
enable: true,
storage: localStorage,
path: ['test'] // type safe, support array
},
}, true) // set true to enable DEV log
// usage
const state = useTestState()
const getters = useTestState<'getter'>() // type only getter parser
const actions = useTestState<'action'>() // type only action parser
render(() => (
<GlobalStateProvider> {/* optional */}
state: <p>{state().value}</p>
getter: <p>{state.doubleValue()}</p>
getter: <p>{getters.doubleValue()}</p>
action: <button onClick={state.double}>double</button>
action: <button onClick={actions.double}>double</button>
action: <button onClick={() => state.plus(2)}>plus 2</button>
</GlobalStateProvider>
))
// use $patchStore
state.$patch((state) => {
state.deep.data = 'patch'
})
state.$patch({
test: 2
})
// $watch
const { pause, resume, isWatching } = state.$subscribe(
s => s.deep.data
state => console.log(state),
{ defer: false },
)
// reset
state.$reset()
or just a global-level context & provider
import { $, $instantEffect, $memo } from 'solid-dollar'
import { $state } from 'solid-dollar/state'
export const useCustomState = $state('custom', (name, log) => {
const plain = $(1)
$instantEffect(() => {
log('$state with custom function:', { name, newValue: plain() })
})
const plus2 = $memo(plain() + 2) // recognized as 'getter' on type
function add() { // recognized as 'action' on type
plain.$set(p => p + 1)
}
return { plain, plus2, add }
})
simple i18n, support async load message file
to get typesafe i18n:
- add first type param
Locale
of$i18n
, - set
datetimeFormats
/numberFormats
keys, - remove useless
Locale
, the$i18n()
is typesafe
or separately define datetimeFormats
/numberFormats
with manually type declartion using type DatetimeFormats
/NumberFormats
{variable}
e.g.
const en = { var: 'show {variable}' }
$t('var', { variable: 'text' }) // show text
{variable}(case=text|case=text)
- case: support number(seprated by ',') / range(seprated by
-
) / '*'(fallback cases) - text: plural text, use
$
to show matched variable
e.g.
const en = { plural: 'at {var}(1=one day|2-3,5=a few days|*=$ days) ago' }
$t('plural', { var: 1 }) // at one day ago
$t('plural', { var: 2 }) // at a few days ago
$t('plural', { var: 4 }) // at 4 days ago
$t('plural', { var: 5 }) // at a few days ago
import { For } from 'solid-js'
import { $i18n, useStaticMessage } from 'solid-dollar/i18n'
// use `as const` to make parameters typesafe
const zh = { t: '1', deep: { t: '{name}' }, plural: '{day}' } as const
const en = { t: '2', deep: { t: '{name}' }, plural: '{day}(0=zero|1=one)' } as const
export const { useI18n, I18nProvider } = $i18n({
message: useStaticMessage({ 'en': en, 'zh-CN': zh }),
defaultLocale: 'en',
datetimeFormats: {
'en': {
short: { dateStyle: 'short' },
long: { dateStyle: 'long' },
custom: d => d.getTime().toString(),
},
'zh-CN': {
short: { dateStyle: 'short' },
long: { dateStyle: 'full' },
custom: d => d.getTime().toString(),
},
},
numberFormats: {
'en': {
currency: { style: 'currency', currency: 'USD' },
},
'zh-CN': {
currency: { style: 'currency', currency: 'CNY' },
},
},
})
// usage
const { $t, $scopeT, $d, $n, availiableLocales, locale } = useI18n(/* optional typesafe scope */)
const scopeT = $scopeT('deep')
return (
<I18nProvider>
<select onChange={e => locale.$set(e.target.value)}>
<For each={availiableLocales}>
{l => <option selected={l === locale()}>{l}</option>}
</For>
</select>
<div>{$t('t')}</div>
<br />
{/* typesafe parameters */}
<div>{$t('t.deep', { name: 'test' })}</div>
<div>{$t('plural', { day: 1 })}</div>
<div>{$d(new Date())}</div>
<div>{$d(new Date(), 'long')}</div>
<div>{$d(new Date(), 'long', 'en')}</div>
<div>{$n(100, 'currency')}</div>
</I18nProvider>
)
using import.meta.glob(...)
to dynamically load message
import { $i18n, useDynamicMessage } from 'solid-dollar/i18n'
export const { useI18n, I18nProvider } = $i18n({
message: useDynamicMessage(
import.meta.glob('./locales/*.yml'),
parseKey: path => path.slice(10, -4)
),
// other options...
})
return (
<I18nProvider useSuspense={<div>loading...</div>}>
{/*...*/}
</I18nProvider>
)
to convert yml, setup built-in vite plugin
vite.config.ts
import { defineConfig } from 'vite'
import { parse } from 'yaml'
import { $i18nPlugin } from 'solid-dollar/plugin'
export default defineConfig({
plugins: [
$i18nPlugin({
include: './src/i18n/locales/*.yml',
transformMessage: content => parse(content),
// generate yml for https://github.com/lokalise/i18n-ally/wiki/Custom-Framework
generateConfigYml: true,
}),
],
})
util for child component event emitting, auto handle optional prop
import { useEmits } from 'solid-dollar/hooks'
import type { EmitProps } from 'solid-dollar/hooks'
type Emits = {
var: number
update: [d1: string, d2?: string, d3?: string]
optional?: { test: number }
}
type BaseProps = { num: number }
function Child(props: EmitProps<Emits, BaseProps>) {
const { emit, $emit } = useEmits<Emits>(props)
// auto emit after value changing, inspird by `defineModel` in Vue
const variable = $emit('var', 1)
const handleClick = () => {
variable.$set(v => v + 1)
// manully emit
emit('update', `emit from child: ${props.num}`, 'second')
emit('optional', { test: 1 })
}
return (
<div>
child:
{props.num}
<button onClick={handleClick}>+</button>
</div>
)
}
function Father() {
const count = $('init')
return (
<Child
num={count()}
$update={console.log}
$var={e => console.log('useEmits:', e)}
/>
)
}
simple two-way binding directive for <input>
, <textare>
, <select>
import { $ } from 'solid-dollar'
const msg = $('')
return <input type="text" use:model={msg} />
typescript support
env.d.ts:
import { ModelDirective } from 'solid-dollar/hooks'
declare module 'solid-js' {
namespace JSX {
interface Directives extends ModelDirective {}
}
}
export { }
use with unplugin-auto-import
vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import { $autoImport } from 'solid-dollar/plugin'
export default defineConfig({
plugins: [
AutoImport({
import: [...$autoImport(true/* directive only */)],
}),
],
})
Vue
like nextTick()
, reference from solidjs-use
Vue
like createApp()
import { useApp } from 'solid-dollar/hooks'
import App from './App'
useApp(App)
.use(RouterProvider, { props })
.use(I18nProvider)
.use(GlobalStoreProvider)
.mount('#app')
is equal to:
render(
<RouterProvider props={props}>
<I18nProvider>
<GlobalStoreProvider>
<App />
</GlobalStoreProvider>
</I18nProvider>
</RouterProvider>,
document.querySelector('#app')
)
reference from solid-utils
object style useContext and Provider
if default value is not defined and use context outside provider, throw Error
when DEV
reference from @solid-primitives/context
import { useContextProvider } from 'solid-dollar/hooks'
const { useDateContext, DateProvider } = useContextProvider(
'date',
() => new Date()
)
// use parameters
const { useDateContext, DateProvider } = useContextProvider(
'date',
(args: { date: string }) => new Date(args.date),
{ date: '2000-01-01' }
)
auto cleanup event listener
reference from @solid-primitives/event-listener
make element draggable
import { $ } from 'solid-dollar'
import { useDraggable } from 'solid-dollar/hooks'
const el = $<HTMLElement>()
const handle = $<HTMLElement>()
const {
position,
resetPosition,
enable,
disable,
isDragging,
isDraggable,
style,
} = useDraggable(el, {
initialPosition: { x: 200, y: 80 },
addStyle: true, // auto add style on el
handleEl: handle,
})
return (
<div
ref={el.$}
style={{ position: 'fixed' }}
>
I am at {Math.round(position().x)}, {Math.round(position().y)}
<div
ref={handle.$}
style={{ position: 'fixed' }}
>
drag me
</div>
</div>
)
load external script / style
import { $ } from 'solid-dollar'
import { useScriptLoader } from 'solid-dollar/hooks'
const script = $('console.log(`test load script`)')
const { element, cleanup } = useScriptLoader(script, {/* options */})
load external CSS code
import { useStyleLoader } from 'solid-dollar/hooks'
const { element, cleanup } = useStyleLoader('.card{color:#666}', {/* options */})
create callbacks with runWithOwner
, auto get current owner
reference from @solid-primitives/rootless
import { $watch } from 'solid-dollar'
import { useCallback } from 'solid-dollar/hooks'
const handleClick = useCallback(() => {
$watch(() => {...})
})
setTimeOut(handleClick, 100)
auto persist value to storage(sync or async)
reference from @solid-primitives/storage
import { $, $store } from 'solid-dollar'
import { usePersist } from 'solid-dollar/hooks'
// default to persist to `localeStorage`
const val = usePersist('key', $(1))
const itemState = usePersist('item', { test: 'loading' }, {
storage: {/* async or sync storage */}
serializer: {
read: JSON.parse, // default
write: JSON.stringify, // default
}
})