UNPKG

@chasemoskal/magical

Version:

web toolkit for lit apps

282 lines (208 loc) • 7.55 kB
šŸŖ„ magical ========== *web toolkit for [lit](https://lit.dev/) apps* šŸ•¹ļø [**live demo — magical.chasemoskal.com**](https://magical.chasemoskal.com/) šŸ“¦ `npm install @chasemoskal/magical` šŸ’– *made with open source love* magical is a collection of tools we build, maintain, and use every day to make great [lit](https://lit.dev/) applications. <br/> ## šŸ¤– magic `element` every magic element is also a lit element. but magic elements have a `realize` method instead of a *render* method. in your `realize` method, use `this.use`, to get access to a "hooks" interface for state management. ```ts import {MagicElement, mixinCss, UseElement} from "@chasemoskal/magical" import {html} from "lit" import {property} from "lit/decorators.js" import stylesCss from "./styles.css.js" @mixinCss(stylesCss) export class CounterElement extends MagicElement { @property({type: Number}) start = 0 realize() { const {use} = this const [count, setCount] = use.state(this.start) const increment = () => setCount(x => x + 1) use.setup(() => { const listener = () => console.log("resized") window.addEventListener("resize", listener) return () => window.removeEventListener("resize", listener) }) return html` <div> <p>count ${count}</p> <button @click=${increment}>increment</button> </div> ` } } ``` there are some things to know about: - you should never access `use` outside of `realize` - like any hooks interface, your `use` calls must be in the same order every time - so don't put `use.state` or `use.setup` calls inside a for loop or in a callback function or anything like that - best practice is to keep use calls at the top-level - `use.state` returns an array with four things: - the current value - the setter function - you can pass it a new value - or a function that takes the previous value and returns a new value - the getter function - the getter is useful getting the latest version of state in a callback - the previous value - you could compare current===previous to see if the value has changed - `use.setup` - use this to run a setup routine every time the component connects to the dom - the setup function you provide should return a function that tears down and cleans up any mess, called when the component disconnects from the dom <br/> ## ✨ magic `view` views have the same `use` hook interface, but views are not components or elements. they're *lit directives.* but like elements, views too can have a shadow dom, and their own css styles. ```ts import {view} from "@chasemoskal/magical" import {html} from "lit" import stylesCss from "./styles.css.js" export const CounterView = view({ shadow: true, styles: stylesCss, }, use => (start: number) => { const [count, setCount] = use.state(start) const increment = () => setCount(x => x + 1) return html` <div> <p>count ${count}</p> <button @click=${increment}>increment</button> </div> ` }) ``` the important thing to understand, is how they are used: - views are used like this: ```ts // 🧐 return html` <div> ${CounterView(2)} </div> ` ``` - this is great, because CounterView is fully typescript-typed - and it's directly imported, so it's easy to trace where views are being used (vscode find all references) - typescript will sniff out and complain about places you need to change when you update those parameters - whereas using an element would be like this: ```ts // 🤮 return html` <div> <counter-element start=2></counter-element> </div> ` ``` - this is OK for an html-only interface, but for real app development? - this sucks, no typescript typing - no imports, no vscode find all references - have to worry about dom registrations - views solve all of this compared against elements: - views are typescript functions, so their parameters are fully typed, vscode auto-refactoring works - views are less cumbersome, because they don't need to be registered to the dom compared against simple render functions: - views have state - views are independent rendering contexts - views can have shadow dom and their own stylesheets i think a good way to think about elements and views is like this: - elements are entrypoints at the html-level - most of our app features are implemented as views - our views are comprised of simple render functions <br/> ## šŸ“» magic `event` we have this handy helper for making custom dom events. ```js import {MagicEvent} from "@chasemoskal/magical" export class ProfileChanged extends MagicEvent<{count: number}>("profile_changed") {} // dispatch the event MyCoolEvent .target(window) .dispatch({count: 1}) // listen for the event const unlisten = MyCoolEvent .target(window) .listen(event => { console.log("profile changed", event.detail.count) }) ``` instead of extending MagicEvent, you can just use `ev` directly to listen and dispatch custom events: ```js import {ev} from "@chasemoskal/magical" ev(MyCustomEvent) .target(window) .dispatch({lol: "example"}) const unlisten = ev(MyCustomEvent) .target(window) .listen(event => { console.log("example event", event.detail.lol) }) ``` <br/> ## 🐫 camel `css` we wanted sass-like css nesting, but in our web components. so we built a parser and compiler for a new css language. it can run serverside, as part of a build script, or our preferred method — live on the clientside, compiling stylesheets for our elements and views. camel css can be a drop-in replacement for lit's css tagged-template function: ```js import {css} from "@chasemoskal/magical" const styles = css` div { p { color: red; } } ` ``` camel-css uses `^` instead of sass's `&` <br/> ## šŸŖ„ more magical tools ### āš™ļø `registerElements` and `themeElements` for the love of god, if you're writing a web components library, do not call `customElements.define` in those component modules. be polite, and allow us the opportunity to augment your elements, rename them, apply a css theme, and then we can register our augmented elements. so, when we're making a library, we like to have a function like `getElements` that returns all the library's elements classes. then it's easy for anybody to apply a css theme and register the elements: ```js import {registerElements, themeElements} from "@chasemoskal/magical" registerElements( themeElements( themeCss, getElements(), ) ) ``` - registerElements will automatically take `CamelCaseComponent` names and convert them into `camel-case-component` names ### šŸŽØ `mixins` for your lit elements *TODO documentation for these* - `mixinCss` - `mixinLightDom` - `mixinRefreshInterval` - `mixinContextRequired` ### šŸ€ `debounce` i've made like ten versions of this, and i think this is my masterpiece. it even has unit tests. ```js import {debounce} from "@chasemoskal/magical" const action = () => console.log("action!") const debouncedAction = debounce(1000, action) // debouncedAction is a promise that resolves // after the 1000 millseconds of no activity debouncedAction() debouncedAction() await debouncedAction() //> "action!" // the action only fires once ``` this debouncer - typescript - works with functions or async functions - returns promises - the promises resolve with the actual value <br/> <br/> ------ &nbsp; &nbsp; šŸ’– *made with open source love*