https://github.com/e280/sly
🦝 mischievous shadow views
https://github.com/e280/sly
lit lit-element view-library views web web-components
Last synced: 10 months ago
JSON representation
🦝 mischievous shadow views
- Host: GitHub
- URL: https://github.com/e280/sly
- Owner: e280
- License: mit
- Created: 2025-06-28T23:20:32.000Z (12 months ago)
- Default Branch: main
- Last Pushed: 2025-09-01T04:48:05.000Z (10 months ago)
- Last Synced: 2025-09-01T05:08:13.727Z (10 months ago)
- Topics: lit, lit-element, view-library, views, web, web-components
- Language: TypeScript
- Homepage: https://sly.e280.org/
- Size: 235 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# 🦝 sly — mischievous shadow views
> testing page https://sly.e280.org/
- 🍋 web app view library with taste
- 🔥 [lit](https://lit.dev/)-based html rendering
- 🧙♂️ took many years to get it right
- 🌅 sly is the successor that replaces [@benev/slate](https://github.com/benevolent-games/slate)
- 🧑💻 project by [@e280](https://e280.org/)
## 🦝 sly and friends
```sh
npm install @e280/sly lit
```
> [!NOTE]
> - 🔥 [lit](https://lit.dev/) for html rendering
> - ⛏️ [@e280/strata](https://github.com/e280/strata) for state management (signals, state trees)
> - 🏂 [@e280/stz](https://github.com/e280/stz) ***(optional)*** stz is our ts standard library
> - 🐢 [scute](https://github.com/e280/scute) ***(optional)*** is our buildy-bundly-buddy
## 🦝 sly views
> *views are the crown jewel of sly.. shadow-dom'd.. hooks-based.. "ergonomics"..*
```ts
view(use => () => html`
hello world
`)
```
- views are not [web components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components), but they do have [shadow roots](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) and support [slots](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_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](https://github.com/e280/strata) state changes
### 🍋 view example
- **import stuff**
```ts
import {$, view} from "@e280/sly"
import {html, css} from "lit"
```
- **declare a view**
```ts
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`
count ${count()}
+
`
})
```
- each view renders into a `` host (where "counter" is the `use.name` you provided)
- **inject a view into the dom**
```ts
$.render($(".app"), html`
my cool counter demo
${CounterView(1)}
`)
```
- 🤯 **register a view as a web component**
```ts
$.register({MyCounter: CounterView.component(1)})
//
```
### 🍋 view declaration settings
- special settings for views at declaration-time
```ts
export const CoolView = view
.settings({mode: "open", delegatesFocus: true})
.declare(use => (greeting: string) => {
return html`😎 ${greeting} `
})
```
- all [attachShadow params](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#parameters) (like `mode` and `delegatesFocus`) are valid `settings`
- note the `` we'll use in the next example lol
### 🍋 view injection options
- options for views at the template injection site
```ts
$.render($(".app"), html`
super cool example
${CoolView.props("hello")
.attr("class", "hero")
.children(html`spongebob`)
.render()}
`)
```
- `props` — provide props and start a view chain
- `attr` — set html attributes on the `` host element
- `children` — nested content in the host element, can be [slotted](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)
- `render` — end the view chain and render the lit directive
### 🍋 web components
- **build a component directly**
```ts
const MyComponent = view.component(use => html`
hello world
`)
```
- notice that direct components don't take props (do `use.attrs` instead)
- **convert any view into a web component**
```ts
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**
```ts
$.register({MyComponent, MyCounter})
//
//
```
- `$.register` automatically dashes the tag names (`MyComponent` becomes ``)
### 🍋 view "use" hooks reference
- 👮 **follow the hooks rules**
> just like [react hooks](https://react.dev/warnings/invalid-hook-call-warning), the execution order of sly's `use` hooks actually matters..
> you must not call these hooks under `if` conditionals, or `for` loops, or in callbacks, or after a conditional `return` statement, or anything like that.. *otherwise, heed my warning: weird bad stuff will happen..*
- **use.name** — set the "view" attr value, eg ``
```ts
use.name("squarepants")
```
- **use.styles** — attach stylesheets into the view's shadow dom
```ts
use.styles(css1, css2, css3)
```
*(or `use.css` alias)*
- **use.signal** — create a [strata signal](https://github.com/e280/strata)
```ts
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
```ts
const whatever = use.once(() => {
console.log("happens only once")
return 123
})
whatever // 123
```
- **use.mount** — setup mount/unmount lifecycle
```ts
use.mount(() => {
console.log("view mounted")
return () => {
console.log("view unmounted")
}
})
```
- **use.wake** — run fn each time mounted, and return value
```ts
const whatever = use.wake(() => {
console.log("view mounted")
return 123
})
whatever // 123
```
- **use.life** — mount/unmount lifecycle, but also return a value
```ts
const v = use.life(() => {
console.log("mounted")
const value = 123
return [value, () => console.log("unmounted")]
})
v // 123
```
- **use.attrs** — ergonomic typed html attribute access
```ts
const attrs = use.attrs({
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
```
- **use.render** — rerender the view (debounced)
```ts
use.render()
```
- **use.renderNow** — rerender the view instantly (not debounced)
```ts
use.renderNow()
```
- **use.rendered** — promise that resolves *after* the next render
```ts
use.rendered.then(() => {
const slot = use.shadow.querySelector("slot")
console.log(slot)
})
```
- **use.op** — start with an op based on an async fn
```ts
const op = use.op(async() => {
await nap(5000)
return 123
})
```
- **use.op.promise** — start with an op based on a promise
```ts
const op = use.op.promise(doAsyncWork())
```
### 🍋 view "use" recipes
- make a ticker — mount, repeat, and nap
```ts
import {repeat, nap} from "@e280/stz"
```
```ts
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
```ts
use.wake(() => use.rendered.then(() => {
console.log("after first render")
}))
```
## 🦝 sly dom multitool
> *"it's not jquery!"*
### 💲 follow the money
- import the dollarsign
```ts
import {$} from "@e280/sly"
```
### 💲 dom queries
- require an element
```ts
$(".demo")
// HTMLElement (or throws error)
```
- request an element
```ts
$.maybe(".demo")
// HTMLElement | undefined
```
- query all elements
```ts
for (const item of $.all("ul li"))
console.log(item)
```
- specify what element to query under
```ts
$("li", listElement)
// HTMLElement
```
### 💲 dom utilities
- render content into an element
```ts
$.render(element, html`
hello world
`)
```
- register web components
```ts
$.register({MyComponent, AnotherCoolComponent})
//
//
```
## 🦝 sly ops, pods, and loaders
> *async operations and displaying loading spinners.*
```ts
import {nap} from "@e280/stz"
import {Pod, podium, Op, makeLoader, anims} from "@e280/sly"
```
### 🫛 pods: loading/ready/error
- a pod represents an async operation in terms of json-serializable data
- there are three kinds of `Pod`
```ts
// loading pod
["loading"]
// ready pod contains value 123
["ready", 123]
// error pod contains an error
["error", new Error()]
```
### 🫛 podium: helps you work with pods
- get pod status
```ts
podium.status(["ready", 123])
// "ready"
```
- get pod ready value (or undefined)
```ts
podium.value(["loading"])
// undefined
podium.value(["ready", 123])
// 123
```
- see more at [podium.ts](./s/ops/podium.ts)
### 🫛 ops: nice pod ergonomics
- an `Op` wraps a pod with a signal for reactivity
- create an op
```ts
const op = new Op() // loading status by default
```
```ts
const op = Op.loading()
```
```ts
const op = Op.ready(123)
```
```ts
const op = Op.error(new Error())
```
- 🔥 create an op that calls and tracks an async fn
```ts
const op = Op.fn(async() => {
await nap(4000)
return 123
})
```
- await for the next ready value (or thrown error)
```ts
await op // 123
```
- get pod info
```ts
op.pod // ["loading"]
op.status // "loading"
op.value // undefined (or value if ready)
```
```ts
op.isLoading // true
op.isReady // false
op.isError // false
```
- select executes a fn based on the status
```ts
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
```ts
op.morph(n => n + 1)
// ["ready", 124]
```
- you can combine a number of ops into a single pod like this
```ts
Op.all(Op.ready(123), Op.loading())
// ["loading"]
```
```ts
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
### 🫛 loaders: animated loading spinners
- create a `loader` using `makeLoader`
```ts
const 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
```ts
return html`
cool stuff
${loader(op, value => html`
${value}
`)}
`
```
- 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
## 🧑💻 sly by e280
reward us with github stars
build with us at https://e280.org/ but only if you're cool