@chasemoskal/magical v0.1.9
šŖ magical
web toolkit for lit apps
š¹ļø live demo ā 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 applications.
š¤ 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.
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 ofrealize
- like any hooks interface, your
use
calls must be in the same order every time- so don't put
use.state
oruse.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
- so don't put
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
āØ 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.
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:
// š§ 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:
// š¤® 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
š» magic event
we have this handy helper for making custom dom events.
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:
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)
})
š« 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:
import {css} from "@chasemoskal/magical"
const styles = css`
div {
p { color: red; }
}
`
camel-css uses ^
instead of sass's &
šŖ 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:
import {registerElements, themeElements} from "@chasemoskal/magical"
registerElements(
themeElements(
themeCss,
getElements(),
)
)
- registerElements will automatically take
CamelCaseComponent
names and convert them intocamel-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.
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
Ā Ā š made with open source love
5 months ago
6 months ago
9 months ago
9 months ago
11 months ago
11 months ago
1 year ago
1 year ago
2 years ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago