testing page https://sly.e280.org/
- 🍋 web app view library with taste
- 🥷 leverage shadow-dom and slots
- 🤯 register any view as a web component
- 💲 handy little dom multitool
- 🫛 ops for fancy loading spinners
- 🧙♂️ took many years to get it right
- 🌅 sly is the successor that replaces @benev/slate
- 🧑💻 project by @e280
npm install @e280/sly litNote
- 🔥 lit for html rendering
- ⛏️ @e280/strata for state management (signals, state trees)
- 🏂 @e280/stz (optional) stz is our ts standard library
- 🐢 scute (optional) is our buildy-bundly-buddy
views are the crown jewel of sly.. shadow-dom'd.. hooks-based.. "ergonomics"..
view(use => () => html`<p>hello world</p>`)- views are not web components, but they do have shadow roots and support slots
- any view can be registered as a web component, perfect for entrypoints or sharing widgets with html authors
- views are typescript-native and comfy for webdevs building apps
- views automatically rerender whenever any strata-compatible state changes
- import stuff
import {$, view} from "@e280/sly" import {html, css} from "lit"
- declare a view
export const CounterView = view(use => (start: number) => { use.name("counter") use.styles(css`p {color: green}`) const count = use.signal(start) const increment = () => { count.value++ } return html` <p>count ${count()}</p> <button @click="${increment}">+</button> ` })
- each view renders into a
<sly-view view="counter">host (where "counter" is theuse.nameyou provided)
- each view renders into a
- inject a view into the dom
$.render($(".app"), html` <h1>my cool counter demo</h1> ${CounterView(1)} `)
- 🤯 register a view as a web component
$.register({MyCounter: CounterView.component(1)}) // <my-counter></my-counter>
- special settings for views at declaration-time
export const CoolView = view .settings({mode: "open", delegatesFocus: true}) .declare(use => (greeting: string) => { return html`😎 ${greeting} <slot></slot>` })
- all attachShadow params (like
modeanddelegatesFocus) are validsettings - note the
<slot></slot>we'll use in the next example lol
- all attachShadow params (like
- options for views at the template injection site
$.render($(".app"), html` <h2>super cool example</h2> ${CoolView.props("hello") .attr("class", "hero") .children(html`<em>spongebob</em>`) .render()} `)
props— provide props and start a view chainattr— set html attributes on the<sly-view>host elementchildren— nested content in the host element, can be slottedrender— end the view chain and render the lit directive
- build a component directly
const MyComponent = view.component(use => html`<p>hello world</p>`)
- notice that direct components don't take props (do
use.attrsinstead)
- notice that direct components don't take props (do
- convert any view into a web component
const MyCounter = CounterView.component(1)
- to convert a view to a component, you provide props
- note that the component instance has a render method like
element.render(2)which can take new props at runtime
- register web components to the dom
$.register({MyComponent, MyCounter}) // <my-component></my-component> // <my-counter></my-counter>
$.registerautomatically dashes the tag names (MyComponentbecomes<my-component>)
- 👮 follow the hooks rules
just like react hooks, the execution order of sly's
usehooks actually matters..
you must not call these hooks underifconditionals, orforloops, or in callbacks, or after a conditionalreturnstatement, or anything like that.. otherwise, heed my warning: weird bad stuff will happen.. - use.name — set the "view" attr value, eg
<sly-view view="squarepants">use.name("squarepants")
- use.styles — attach stylesheets into the view's shadow dom
use.styles(css1, css2, css3)
- use.signal — create a strata signal
const count = use.signal(1) // read the signal count() // 1 // write the signal count(2)
- use.once — run fn at initialization, and return a value
const whatever = use.once(() => { console.log("happens only once") return 123 }) whatever // 123
- use.mount — setup mount/unmount lifecycle
use.mount(() => { console.log("view mounted") return () => { console.log("view unmounted") } })
- use.wake — run fn each time mounted, and return value
const whatever = use.wake(() => { console.log("view mounted") return 123 }) whatever // 123
- use.life — mount/unmount lifecycle, but also return a value
const v = use.life(() => { console.log("mounted") const value = 123 return [value, () => console.log("unmounted")] }) v // 123
- use.attrs — ergonomic typed html attribute access
const attrs = use.attrs({ name: String, count: Number, active: Boolean, })
attrs.name // "chase" attrs.count // 123 attrs.active // true
attrs.name = "zenky" attrs.count = 124 attrs.active = false // removes html attr
attrs.name = undefined // removes the attr
- use.render — rerender the view (debounced)
use.render()
- use.renderNow — rerender the view instantly (not debounced)
use.renderNow()
- use.rendered — promise that resolves after the next render
use.rendered.then(() => { const slot = use.shadow.querySelector("slot") console.log(slot) })
- use.op — start with an op based on an async fn
const op = use.op(async() => { await nap(5000) return 123 })
- use.op.promise — start with an op based on a promise
const op = use.op.promise(doAsyncWork())
- make a ticker — mount, repeat, and nap
import {repeat, nap} from "@e280/stz"
const seconds = use.signal(0) use.mount(() => repeat(async() => { await nap(1000) seconds.value++ }))
- wake + rendered, to do something after each mount's first render
use.wake(() => use.rendered.then(() => { console.log("after first render") }))
"it's not jquery!"
- import the dollarsign
import {$} from "@e280/sly"
- require an element
$(".demo") // HTMLElement (or throws error)
- request an element
$.maybe(".demo") // HTMLElement | undefined
- query all elements
for (const item of $.all("ul li")) console.log(item)
- specify what element to query under
$("li", listElement) // HTMLElement
- render content into an element
$.render(element, html`<p>hello world</p>`)
- register web components
$.register({MyComponent, AnotherCoolComponent}) // <my-component> // <another-cool-component>
async operations and displaying loading spinners.
import {nap} from "@e280/stz"
import {Pod, podium, Op, makeLoader, anims} from "@e280/sly"- a pod represents an async operation in terms of json-serializable data
- there are three kinds of
Pod<V>// loading pod ["loading"] // ready pod contains value 123 ["ready", 123] // error pod contains an error ["error", new Error()]
- get pod status
podium.status(["ready", 123]) // "ready"
- get pod ready value (or undefined)
podium.value(["loading"]) // undefined podium.value(["ready", 123]) // 123
- see more at podium.ts
- an
Op<V>wraps a pod with a signal for reactivity - create an op
const op = new Op<number>() // loading status by default
const op = Op.loading<number>()
const op = Op.ready<number>(123)
const op = Op.error<number>(new Error())
- 🔥 create an op that calls and tracks an async fn
const op = Op.fn(async() => { await nap(4000) return 123 })
- await for the next ready value (or thrown error)
await op // 123
- get pod info
op.pod // ["loading"] op.status // "loading" op.value // undefined (or value if ready)
op.isLoading // true op.isReady // false op.isError // false
- select executes a fn based on the status
const result = op.select({ loading: () => "it's loading...", ready: value => `dude, it's ready! ${value}`, error: err => `dude, there's an error!`, }) result // "dude, it's ready! 123"
- morph returns a new pod, transforming the value if ready
op.morph(n => n + 1) // ["ready", 124]
- you can combine a number of ops into a single pod like this
Op.all(Op.ready(123), Op.loading()) // ["loading"]
Op.all(Op.ready(1), Op.ready(2), Op.ready(3)) // ["ready", [1, 2, 3]]
- error if any ops are in error, otherwise
- loading if any ops are in loading, otherwise
- ready if all the ops are ready
- create a
loaderusingmakeLoaderconst loader = makeLoader(anims.dots)
- see all the anims available on the testing page https://sly.e280.org/
- ngl, i made too many.. i was having fun, okay?
- use the loader to render your op
return html` <h2>cool stuff</h2> ${loader(op, value => html` <div>${value}</div> `)} `
- when the op is loading, the loading spinner will animate
- when the op is in error, the error will be displayed
- when the op is ready, your fn is called and given the value
reward us with github stars
build with us at https://e280.org/ but only if you're cool