0.1.9 ā€¢ Published 5 months ago

@chasemoskal/magical v0.1.9

Weekly downloads
-
License
MIT
Repository
-
Last release
5 months ago

šŸŖ„ 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 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

āœØ 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 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.

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

0.1.9

5 months ago

0.1.8

6 months ago

0.1.7

9 months ago

0.1.6

9 months ago

0.1.4

11 months ago

0.1.5

11 months ago

0.1.3

1 year ago

0.1.0

1 year ago

0.0.1

2 years ago

0.1.2

1 year ago

0.0.3

1 year ago

0.1.1

1 year ago

0.0.2

1 year ago

0.0.5

1 year ago

0.0.4

1 year ago

0.0.0

2 years ago

0.0.0-dev.1

2 years ago

0.0.0-dev.0

2 years ago