
<div align="center"><img alt="" width="256" src="./s/assets/favicon.png"/></div>

# 🦝 sly
### [e280](https://e280.org/)'s buildercore view library.

> *built on [lit](https://lit.dev/). powered by [strata](https://github.com/e280/strata).*

- 🎭 [**#views,**](#views) light-dom or shadow-dom
- 🪝 [**#hooks,**](#hooks) react-like composable hooks
- ⏳ [**#spinners,**](#spinners) display async operations with animations
- 💅 [**#spa,**](#spa) tiny router for hashy little single-page-apps
- 🪙 [**#loot,**](#loot) drag-and-drop facilities
- 🪄 [**#dom,**](#dom) the "it's not jquery" multitool
- 🧪 **https://sly.e280.org/** sly's testing page

```bash
npm install @e280/sly @e280/strata @e280/stz lit
```



<br/><br/>
<a id="views"></a>

## 🎭 views
> *reactive lit-html views*

- 🔮 [**see codepen demo,**](https://codepen.io/editor/ChaseMoskal/pen/019cd681-722b-7f51-a961-bc16e3d524a9) plain html (no build!)
- 🌗 **light or shadow,** render nakedly on the page, or within a cozy shadow bubble
- 🪝 **hooks-based,** familiar react-style [hooks](#hooks)
- ⚡ **auto-reactive,** views magically rerender on [strata](https://github.com/e280/strata)-compatible state changes
- 🪶 **no compile step,** just god's honest javascript via [lit](https://lit.dev/)-html tagged-templates
- 🧩 **not web components,** no dom registration needed, just good vibes and good typings

```ts
import {html} from "lit"
import {light, shadow, dom} from "@e280/sly"

export const MyLightView = light(() => html`<p>blinded by the light</p>`)

export const MyShadowView = shadow(() => html`<p>shrouded in darkness</p>`)
```

### 🌞 light views
> *lit, signals, hooks — life is good*

- **define a light view**
    ```ts
    import {html} from "lit"
    import {light, useSignal} from "@e280/sly"

    export const MyCounter = light((start: number) => {
      const $count = useSignal(start)
      const increment = () => $count($count() + 1)

      return html`
        <button @click="${increment}">${$count()}</button>
      `
    })
    ```
- **render it into the dom**
    ```ts
    dom.in(".demo").render(html`
      <h1>my cool counter demo</h1>
      ${MyCounter(123)}
    `)
    ```
- **remember, light views are naked.**  
    so they don't have a containing host element,  
    and they can't have their own styles.  

### 🌚 shadow views
> *each shadow view gets its own cozy [shadow-dom](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) bubble to protect its local css, and it also supports [slotting](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)*

- **define a shadow view**
    ```ts
    import {css, html} from "lit"
    import {shadow, useName, useCss, useSignal} from "@e280/sly"

    export const MyShadowCounter = shadow((start: number) => {
      useName("counter")
      useCss(css`button { color: cyan }`)

      const $count = useSignal(start)
      const increment = () => $count($count() + 1)

      return html`
        <button @click="${increment}">${$count()}</button>
        <slot></slot>
      `
    })
    ```
- **render it into the dom**
    ```ts
    dom.in(".demo").render(html`
      <h1>my cool counter demo</h1>
      ${MyShadowCounter(234)}
    `)
    ```
    - shadow views have a host element, rendered output looks like:
        ```html
        <h1>my cool counter demo</h1>
        <sly-shadow view="counter"></sly-shadow>
        ```
- **.with to nest children or set attrs**
    ```ts
    dom.in(".demo").render(html`
      <h1>my cool counter demo</h1>

      ${MyShadowCounter.with({
        props: [234],
        attrs: {"data-whatever": 555},
        children: html`
          <p>woah, slotting support!</p>
        `,
      })}
    `)
    ```
- **you can do custom shadow setup if needed** (default shown)
    ```ts
    import {SlyShadow} from "@e280/sly"

    const customShadow = shadow.setup(() => {
      SlyShadow.register()
      const host = document.createElement("sly-shadow")
      const shadow = host.attachShadow({mode: "open"})
      return {host, shadow}
    })

    const MyShadowView = customShadow(() => html`<p>shrouded in darkness</p>`)
    ```

### 🍨 web components
> *web-native custom elements*

- **they use hooks like the views, but they don't take props**
    ```ts
    import {html} from "lit"
    import {lightElement, shadowElement} from "@e280/sly"

    const MyLight = lightElement(() => html`hello`)
    const MyShadow = shadowElement(() => html`hello`)

    dom.register({MyLight, MyShadow})
    ```
    ```html
    <my-light></my-light>
    <my-shadow></my-shadow>
    ```



<br/><br/>
<a id="hooks"></a>

## 🪝 hooks
> *composable view state and utilities*

### 👮 follow the hooks rules, or you go to hooks jail

just like [react hooks](https://react.dev/warnings/invalid-hook-call-warning), the execution order of hooks seriously matters.

you must not call these hooks under if-conditionals, or for-loops, or inside callback functions, or after a conditional return statement, or anything like that.. *otherwise, heed my warning: weird bad stuff will happen..*

### 🌚 shadow-only hooks
- **useName,** set the "view" attribute value
    ```ts
    useName("squarepants")
      // <sly-shadow view="squarepants">
    ```
- **useCss,** attach stylesheets (use lit's `css`!) to the shadow root
    ```ts
    useCss(css1, css2, css3)
    ```
- **useHost,** get the host element
    ```ts
    const host = useHost()
    ```
- **useShadow,** get the shadow root
    ```ts
    const shadow = useShadow()
    ```
- **useAttrs,** access host element attributes (and rerender on attr changes)
    ```ts
    const attrs = useAttrs({
      name: String,
      count: Number,
      active: Boolean,
    })

    attrs.count = 123 // set the attr
    ```

### 🌞 universal hooks
- **useState,** react-like hook to create some reactive state (we prefer signals)
    ```ts
    const [count, setCount] = useState(0)

    const increment = () => setCount(n => n + 1)
    ```
- **useRef,** react-like hook to make a non-reactive box for a value
    ```ts
    const ref = useRef(0)

    ref.current // 0
    ref.current = 1 // does not trigger rerender
    ```
- **useSignal,** create a [strata](https://github.com/e280/strata) signal
    ```ts
    const $count = useSignal(1)

    // read the signal
    $count()

    // write the signal
    $count(2)
    ```
- **useDerived,** create a [strata](https://github.com/e280/strata) derived formula
    ```ts
    const $product = useDerived(() => $count() * $whatever())
    ```
- **useEffect,** run a fn whenever [strata](https://github.com/e280/strata) state changes
    ```ts
    useEffect(() => console.log($count))
    ```
- **useOnce,** run fn at initialization, and return a value
    ```ts
    const whatever = useOnce(() => {
      console.log("happens one time")
      return 123
    })

    whatever // 123
    ```
- **useMount,** setup mount/unmount lifecycle
    ```ts
    useMount(() => {
      console.log("mounted")
      return () => console.log("unmounted")
    })
    ```
- **useWake,** run fn each time mounted, and return value
    ```ts
    const whatever = useWake(() => {
      console.log("mounted")
      return 123
    })

    whatever // 123
    ```
- **useLifecycle,** mount/unmount lifecycle, but also return a value
    ```ts
    const whatever = useLifecycle(() => {
      console.log("mounted")
      const value = 123
      return [value, () => console.log("unmounted")]
    })

    whatever // 123
    ```
- **useRender,** returns a fn to rerender the view (debounced)
    ```ts
    const render = useRender()

    render().then(() => console.log("render done"))
    ```
- **useRendered,** get a promise that resolves *after* the next render
    ```ts
    useRendered().then(() => console.log("rendered"))
    ```
- **useWait,** start loading a [strata#wait](https://github.com/e280/strata#wait) signal
    ```ts
    const $wait = useWait(async() => {
      await nap(2000)
      return 123
    })
    ```
    - look at the current `Wait` state
        ```ts
        $wait()
          // {done: true, ok: true, value: 123}
        ```
    - await for when the value is ready
        ```ts
        await $wait.ready
          // 123
        ```
- **useWaitFormal,** start a [strata#wait](https://github.com/e280/strata#wait), but with a formal [stz#ok](https://github.com/e280/stz#ok) ok/err result
    ```ts
    const $wait = useWaitFormal(async() => {
      await nap(2000)
      return (Math.random() > 0.5)
        ? ok(123)
        : err("uh oh")
    })
    ```

### 🧑‍🍳 happy hooks recipes
- make a ticker, mount, cycle, and nap
    ```ts
    import {cycle, nap} from "@e280/stz"
    ```
    ```ts
    const $seconds = useSignal(0)

    useMount(() => cycle(async() => {
      await nap(1000)
      $seconds($seconds() + 1)
    }))
    ```
- wake + rendered, to do something after each mount's first render
    ```ts
    const rendered = useRendered()

    useWake(() => rendered.then(() => {
      console.log("after first render")
    }))
    ```



<br/><br/>
<a id="spinners"></a>

## ⏳️ spinners
> *animated loading spinners*

sly's spinners integrate with [strata wait](https://github.com/e280/strata#wait), which in turn integrates with [stz ok](https://github.com/e280/stz#ok), so you might want to read each of those docs.

### ⏳️ ui jumpstart
- **okay, so let's just do a loading spinner example**
    ```ts
    import {html} from "lit"
    import {shadow, useWait, spinner} from "@e280/sly"

    const MyView = shadow(() => {

      // ⏳️ create a $wait signal
      const $wait = useWait(async() => {
        await nap(2000) // contrived async job
        return 123 // return a value
      })

      // ⏳️ ui display for the changing $wait signal
      return spinner($wait(), value => html`
        <p>done, the value is ${value}</p>
      `)
    })
    ```
    - while the async fn is running, an animated spinner will be shown
    - when the async fn resolves, our little `<p>` tag will render
    - if the async fn errors out, the error message will be displayed in red
- **stock spinners for your convenience** *(earth is my favorite)*
    ```ts
    import {spinner, dotsSpinner, waveSpinner, earthSpinner, moonSpinner} from "@e280/sly"
    ```

### ⏳️ make your own spinners
- **it's easy**
    ```ts
    import {makeSpinner, makeAsciiAnim, ErrorDisplay} from "@e280/sly"

    export const pieSpinner = makeSpinner(
      makeAsciiAnim(10, ["◷", "◶", "◵", "◴"]),
      ErrorDisplay,
    )
    ```
    - so makeSpinner accepts two views, one for the loading state, and one for the error state
    - feel free to make your own views



<br/><br/>
<a id="spa"></a>

## 💅 spa
> *tiny router for cozy single page apps*

```ts
import {derived} from "@e280/strata"
import {router, norm, hashNav, hashSignal} from "@e280/sly"
```

the spa router is agnostic about whether you're routing `location.hash` or `location.pathname` or otherwise.

- **router**
    ```ts
    const route = router({
      "": () => "home", // 💁 routes can return anything
      "settings": () => "settings",

      // 🧩 params use braces
      "user/{id}": params => `user ${params.id}`,

      // 🔀 subpath with {*}
      "user/{id}/{*}": (params, subpath) => `user ${params.id} ${subpath}`,
    })
    ```
    you get a fn that resolves the path you give it
    ```ts
    route("")
      // "home"

    route("settings")
      // "settings"

    route("user/123/profile")
      // "user 123 profile"

    route("unknown/whatever")
      // undefined
    ```
- **`norm` fn** chops off leading slashes and/or hash chars
    ```ts
    route(norm(location.hash))
      // "#/settings" -> "settings"
    ```
    ```ts
    route(norm(location.pathname))
      // "/settings" -> "settings"
    ```
- **subrouting pattern**
    ```ts
    // here's a subrouter
    const user = (params: {id: string}) => router({
      "profile": () => `user ${params.id} profile`,
      "invites": () => `user ${params.id} invites`,
    })

    // here's the main router, where we can nest the subrouter
    const route = router({

      // this {*} captures the rest of the string, we pass it to the subrouter
      "user/{id}/{*}": (params, subpath) => user(params)(subpath),
    })
    ```
    ```ts
    route("user/123/profile")
      // "user 123 profile"
    ```

now, if you want to setup `location.hash` routing, you might want these primitives.

- **hashNav** fn to trigger navigations
    ```ts
    const go = hashNav({
      home: () => ``,
      settings: () => `settings`,
      user: (id: string) => `user/${id}`,
      userProfile: (id: string) => `user/${id}/profile`,
      userInvites: (id: string) => `user/${id}/invites`,
    })

    go.settings()
      // navigates to "#/settings"

    go.user("123")
      // navigates to "#/user/123"
    ```
- **hashSignal** create a [strata](https://github.com/e280/strata) signal for the current normalized `location.hash`
    ```ts
    const $hash = hashSignal()
    ```
    ```ts
    $hash()
      // "user/123/profile"
    ```
    - the signal value auto-updates whenever the hash changes
    - the value is run through the `norm` fn to chop off the leading `#/`
    - whenever the hash changes, it runs `cleanHash` fn which aesthetically converts `e280.org/#/` to just `e280.org/` in the address bar
- **you should setup a derived signal** that routes whenever that hash signal changes
    ```ts
    const $content = derived(() => route($hash()))
      // "user 123 profile"
    ```
    then you can plop that content into your lit html
    ```ts
    html`
      <div>
        ${$content()}
      </div>
    `
    ```



<br/><br/>
<a id="loot"></a>

## 🪙 loot
> *drag-and-drop facilities*  

```ts
import {loot, view, dom} from "@e280/sly"
import {ev} from "@e280/stz"
```

### 🪙 `loot.Drops`
> *accept the user dropping stuff like files onto the page*
- **setup drops**
    ```ts
    const drops = new loot.Drops({
      predicate: loot.hasFiles,
      acceptDrop: event => {
        const files = loot.files(event)
        console.log("files dropped", files)
      },
    })
    ```
- **attach event listeners to your dropzone,** one of these ways:
  - **view example**
      ```ts
      light(() => html`
        <div
          ?data-indicator="${drops.$indicator()}"
          @dragover="${drops.dragover}"
          @dragleave="${drops.dragleave}"
          @drop="${drops.drop}">
            my dropzone
        </div>
      `)
      ```
  - **vanilla-js whole-page example**
      ```ts
      // attach listeners to the body
      ev(document.body, {
        dragover: drops.dragover,
        dragleave: drops.dragleave,
        drop: drops.drop,
      })

      // sly attribute handler for the body
      const attrs = dom.attrs(document.body).spec({
        "data-indicator": Boolean,
      })

      // sync the data-indicator attribute
      drops.$indicator.on(bool => attrs["data-indicator"] = bool)
      ```
- **flashy css indicator for the dropzone,** so the user knows your app is eager to accept the drop
    ```css
    [data-indicator] {
      border: 0.5em dashed cyan;
    }
    ```

### 🪙 `loot.DragAndDrops`
> *setup drag-and-drops between items within your page*
- **declare types for your draggy and droppy things**
    ```ts
    // money that can be picked up and dragged
    type Money = {value: number}
      // dnd will call this a "draggy"

    // bag that money can be dropped into
    type Bag = {id: number}
      // dnd will call this a "droppy"
    ```
- **make your dnd**
    ```ts
    const dnd = new loot.DragAndDrops<Money, Bag>({
      acceptDrop: (event, money, bag) => {
        console.log("drop!", {money, bag})
      },
    })
    ```
- **attach dragzone listeners** (there can be many dragzones...)
    ```ts
    light(() => {
      const money = useOnce((): Money => ({value: 280}))
      const dragzone = useOnce(() => dnd.dragzone(() => money))

      return html`
        <div
          draggable="${dragzone.draggable}"
          @dragstart="${dragzone.dragstart}"
          @dragend="${dragzone.dragend}">
            money ${money.value}
        </div>
      `
    })
    ```
- **attach dropzone listeners** (there can be many dropzones...)
    ```ts
    light(() => {
      const bag = useOnce((): Bag => ({id: 1}))
      const dropzone = useOnce(() => dnd.dropzone(() => bag))
      const indicator = !!(dnd.dragging && dnd.hovering === bag)

      return html`
        <div
          ?data-indicator="${indicator}"
          @dragenter="${dropzone.dragenter}"
          @dragleave="${dropzone.dragleave}"
          @dragover="${dropzone.dragover}"
          @drop="${dropzone.drop}">
            bag ${bag.id}
        </div>
      `
    })
    ```

### 🪙 loot helpers
- **`loot.hasFiles(event)`** — return true if `DragEvent` contains any files (useful in `predicate`)
- **`loot.files(event)`** — returns an array of files in a drop's `DragEvent` (useful in `acceptDrop`)



<br/><br/>
<a id="dom"></a>

## 🪄 dom
> *the "it's not jquery!" multitool*

```ts
import {dom} from "@e280/sly"
```

### 🪄 dom queries
- `need` an element
    ```ts
    dom(".demo")
      // HTMLElement (or throws)
    ```
    ```ts
    // alias
    dom.need(".demo")
      // HTMLElement (or throws)
    ```
- `maybe` get an element
    ```ts
    dom.maybe(".demo")
      // HTMLElement | undefined
    ```
- `all` matching elements in an array
    ```ts
    dom.all(".demo ul li")
      // HTMLElement[]
    ```

### 🪄 dom.in scope
- make a scope
    ```ts
    dom.in(".demo") // selector
      // Dom instance
    ```
    ```ts
    dom.in(demoElement) // element
      // Dom instance
    ```
- run queries in that scope
    ```ts
    dom.in(demoElement).need(".button")
    ```
    ```ts
    dom.in(demoElement).maybe(".button")
    ```
    ```ts
    dom.in(demoElement).all("ol li")
    ```

### 🪄 dom utilities
- `dom.register` web components
    ```ts
    dom.register({MyComponent, AnotherCoolComponent})
      // <my-component>
      // <another-cool-component>
    ```
    - `dom.register` automatically dashes the tag names (`MyComponent` becomes `<my-component>`)
- `dom.render` content into an element
    ```ts
    dom.render(element, html`<p>hello world</p>`)
    ```
    ```ts
    dom.in(".demo").render(html`<p>hello world</p>`)
    ```
- `dom.el` little element builder
    ```ts
    const div = dom.el("div", {"data-whatever": 123, "data-active": true})
      // <div data-whatever="123" data-active></div>
    ```
- `dom.elmer` make an element with a fluent chain
    ```ts
    const div = dom.elmer("div")
      .attr("data-whatever", 123)
      .attr("data-active")
      .children("hello world")
      .done()
        // HTMLElement
    ```
- `dom.mk` make an element with a lit template (returns the first)
    ```ts
    const div = dom.mk(html`
      <div data-whatever="123" data-active>
        hello world
      </div>
    `) // HTMLElement
    ```
- `dom.events` <a id="dom.events"></a> to attach event listeners
    ```ts
    const detach = dom.events(element, {
      keydown: (e: KeyboardEvent) => console.log("keydown", e.code),
      keyup: (e: KeyboardEvent) => console.log("keyup", e.code),
    })
    ```
    ```ts
    const detach = dom.in(".demo").events({
      keydown: (e: KeyboardEvent) => console.log("keydown", e.code),
      keyup: (e: KeyboardEvent) => console.log("keyup", e.code),
    })
    ```
    ```ts
    // unattach those event listeners when you're done
    detach()
    ```
- `dom.attrs` <a id="dom.attrs"></a> to setup a type-happy html attribute helper
    ```ts
    const attrs = dom.attrs(element).spec({
      name: String,
      count: Number,
      active: Boolean,
    })
    ```
    ```ts
    const attrs = dom.in(".demo").attrs.spec({
      name: String,
      count: Number,
      active: Boolean,
    })
    ```
    ```ts
    attrs.name // "chase"
    attrs.count // 123
    attrs.active // true
    ```
    ```ts
    attrs.name = "zenky"
    attrs.count = 124
    attrs.active = false // removes html attr
    ```
    ```ts
    attrs.name = undefined // removes the attr
    attrs.count = undefined // removes the attr
    ```
    or if you wanna be more loosey-goosey, skip the spec
    ```ts
    const {attrs} = dom.in(".demo")
    attrs.strings.name = "pimsley"
    attrs.numbers.count = 125
    attrs.booleans.active = true
    ```



<br/><br/>
<a id="e280"></a>

## 🧑‍💻 sly is by e280
reward us with github stars  
build with us at https://e280.org/ but only if you're cool  

