{"id":30770435,"url":"https://github.com/e280/sly","last_synced_at":"2025-09-04T23:06:46.855Z","repository":{"id":301808034,"uuid":"1010368103","full_name":"e280/sly","owner":"e280","description":"🦝 mischievous shadow views","archived":false,"fork":false,"pushed_at":"2025-09-01T04:48:05.000Z","size":241,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-01T05:08:13.727Z","etag":null,"topics":["lit","lit-element","view-library","views","web","web-components"],"latest_commit_sha":null,"homepage":"https://sly.e280.org/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/e280.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-06-28T23:20:32.000Z","updated_at":"2025-09-01T04:45:23.000Z","dependencies_parsed_at":"2025-06-29T06:46:12.135Z","dependency_job_id":null,"html_url":"https://github.com/e280/sly","commit_stats":null,"previous_names":["e280/sly"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/e280/sly","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Fsly","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Fsly/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Fsly/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Fsly/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/e280","download_url":"https://codeload.github.com/e280/sly/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Fsly/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273685604,"owners_count":25149722,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-04T02:00:08.968Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["lit","lit-element","view-library","views","web","web-components"],"created_at":"2025-09-04T23:05:11.049Z","updated_at":"2025-09-04T23:06:46.837Z","avatar_url":"https://github.com/e280.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n\u003cdiv align=\"center\"\u003e\u003cimg alt=\"\" width=\"256\" src=\"./assets/favicon.png\"/\u003e\u003c/div\u003e\n\n# 🦝 sly — mischievous shadow views\n\u003e testing page https://sly.e280.org/\n\n- 🍋 web app view library with taste\n- 🔥 [lit](https://lit.dev/)-based html rendering\n- 🧙‍♂️ took many years to get it right\n- 🌅 sly is the successor that replaces [@benev/slate](https://github.com/benevolent-games/slate)\n- 🧑‍💻 project by [@e280](https://e280.org/)\n\n\n\n\u003cbr/\u003e\n\n## 🦝 sly and friends\n\n```sh\nnpm install @e280/sly lit\n```\n\n\u003e [!NOTE]\n\u003e - 🔥 [lit](https://lit.dev/) for html rendering\n\u003e - ⛏️ [@e280/strata](https://github.com/e280/strata) for state management (signals, state trees)\n\u003e - 🏂 [@e280/stz](https://github.com/e280/stz) ***(optional)*** stz is our ts standard library\n\u003e - 🐢 [scute](https://github.com/e280/scute) ***(optional)*** is our buildy-bundly-buddy\n\n\n\n\u003cbr/\u003e\n\n## 🦝 sly views\n\u003e *views are the crown jewel of sly.. shadow-dom'd.. hooks-based.. \"ergonomics\"..*\n\n```ts\nview(use =\u003e () =\u003e html`\u003cp\u003ehello world\u003c/p\u003e`)\n```\n\n- 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)\n- any view can be registered as a web component, perfect for entrypoints or sharing widgets with html authors\n- views are typescript-native and comfy for webdevs building apps\n- views automatically rerender whenever any [strata-compatible](https://github.com/e280/strata) state changes\n\n### 🍋 view example\n- **import stuff**\n    ```ts\n    import {$, view} from \"@e280/sly\"\n    import {html, css} from \"lit\"\n    ```\n- **declare a view**\n    ```ts\n    export const CounterView = view(use =\u003e (start: number) =\u003e {\n      use.name(\"counter\")\n      use.styles(css`p {color: green}`)\n\n      const count = use.signal(start)\n      const increment = () =\u003e { count.value++ }\n\n      return html`\n        \u003cp\u003ecount ${count()}\u003c/p\u003e\n        \u003cbutton @click=\"${increment}\"\u003e+\u003c/button\u003e\n      `\n    })\n    ```\n    - each view renders into a `\u003csly-view view=\"counter\"\u003e` host (where \"counter\" is the `use.name` you provided)\n- **inject a view into the dom**\n    ```ts\n    $.render($(\".app\"), html`\n      \u003ch1\u003emy cool counter demo\u003c/h1\u003e\n\n      ${CounterView(1)}\n    `)\n    ```\n- 🤯 **register a view as a web component**\n    ```ts\n    $.register({MyCounter: CounterView.component(1)})\n      // \u003cmy-counter\u003e\u003c/my-counter\u003e\n    ```\n\n### 🍋 view declaration settings\n- special settings for views at declaration-time\n    ```ts\n    export const CoolView = view\n      .settings({mode: \"open\", delegatesFocus: true})\n      .declare(use =\u003e (greeting: string) =\u003e {\n        return html`😎 ${greeting} \u003cslot\u003e\u003c/slot\u003e`\n      })\n    ```\n    - all [attachShadow params](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#parameters) (like `mode` and `delegatesFocus`) are valid `settings`\n    - note the `\u003cslot\u003e\u003c/slot\u003e` we'll use in the next example lol\n\n### 🍋 view injection options\n- options for views at the template injection site\n    ```ts\n    $.render($(\".app\"), html`\n      \u003ch2\u003esuper cool example\u003c/h2\u003e\n\n      ${CoolView.props(\"hello\")\n        .attr(\"class\", \"hero\")\n        .children(html`\u003cem\u003espongebob\u003c/em\u003e`)\n        .render()}\n    `)\n    ```\n    - `props` — provide props and start a view chain\n    - `attr` — set html attributes on the `\u003csly-view\u003e` host element\n    - `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)\n    - `render` — end the view chain and render the lit directive\n\n### 🍋 web components\n- **build a component directly**\n    ```ts\n    const MyComponent = view.component(use =\u003e html`\u003cp\u003ehello world\u003c/p\u003e`)\n    ```\n    - notice that direct components don't take props (do `use.attrs` instead)\n- **convert any view into a web component**\n    ```ts\n    const MyCounter = CounterView.component(1)\n    ```\n    - to convert a view to a component, you provide props\n    - note that the component instance has a render method like `element.render(2)` which can take new props at runtime\n- **register web components to the dom**\n    ```ts\n    $.register({MyComponent, MyCounter})\n      // \u003cmy-component\u003e\u003c/my-component\u003e\n      // \u003cmy-counter\u003e\u003c/my-counter\u003e\n    ```\n    - `$.register` automatically dashes the tag names (`MyComponent` becomes `\u003cmy-component\u003e`)\n\n### 🍋 view \"use\" hooks reference\n- 👮 **follow the hooks rules**  \n    \u003e just like [react hooks](https://react.dev/warnings/invalid-hook-call-warning), the execution order of sly's `use` hooks actually matters..  \n    \u003e 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..*\n- **use.name** — set the \"view\" attr value, eg `\u003csly-view view=\"squarepants\"\u003e`\n    ```ts\n    use.name(\"squarepants\")\n    ```\n- **use.styles** — attach stylesheets into the view's shadow dom\n    ```ts\n    use.styles(css1, css2, css3)\n    ```\n    *(or `use.css` alias)*\n- **use.signal** — create a [strata signal](https://github.com/e280/strata)\n    ```ts\n    const count = use.signal(1)\n\n    // read the signal\n    count() // 1\n\n    // write the signal\n    count(2)\n    ```\n- **use.once** — run fn at initialization, and return a value\n    ```ts\n    const whatever = use.once(() =\u003e {\n      console.log(\"happens only once\")\n      return 123\n    })\n\n    whatever // 123\n    ```\n- **use.mount** — setup mount/unmount lifecycle\n    ```ts\n    use.mount(() =\u003e {\n      console.log(\"view mounted\")\n\n      return () =\u003e {\n        console.log(\"view unmounted\")\n      }\n    })\n    ```\n- **use.wake** — run fn each time mounted, and return value\n    ```ts\n    const whatever = use.wake(() =\u003e {\n      console.log(\"view mounted\")\n      return 123\n    })\n\n    whatever // 123\n    ```\n- **use.life** — mount/unmount lifecycle, but also return a value\n    ```ts\n    const v = use.life(() =\u003e {\n      console.log(\"mounted\")\n      const value = 123\n      return [value, () =\u003e console.log(\"unmounted\")]\n    })\n\n    v // 123\n    ```\n- **use.attrs** — ergonomic typed html attribute access\n    ```ts\n    const attrs = use.attrs({\n      name: String,\n      count: Number,\n      active: Boolean,\n    })\n    ```\n    ```ts\n    attrs.name // \"chase\"\n    attrs.count // 123\n    attrs.active // true\n    ```\n    ```ts\n    attrs.name = \"zenky\"\n    attrs.count = 124\n    attrs.active = false // removes html attr\n    ```\n    ```ts\n    attrs.name = undefined // removes the attr\n    ```\n- **use.render** — rerender the view (debounced)\n    ```ts\n    use.render()\n    ```\n- **use.renderNow** — rerender the view instantly (not debounced)\n    ```ts\n    use.renderNow()\n    ```\n- **use.rendered** — promise that resolves *after* the next render\n    ```ts\n    use.rendered.then(() =\u003e {\n      const slot = use.shadow.querySelector(\"slot\")\n      console.log(slot)\n    })\n    ```\n- **use.op** — start with an op based on an async fn\n    ```ts\n    const op = use.op(async() =\u003e {\n      await nap(5000)\n      return 123\n    })\n    ```\n- **use.op.promise** — start with an op based on a promise\n    ```ts\n    const op = use.op.promise(doAsyncWork())\n    ```\n\n### 🍋 view \"use\" recipes\n- make a ticker — mount, repeat, and nap\n    ```ts\n    import {repeat, nap} from \"@e280/stz\"\n    ```\n    ```ts\n    const seconds = use.signal(0)\n\n    use.mount(() =\u003e repeat(async() =\u003e {\n      await nap(1000)\n      seconds.value++\n    }))\n    ```\n- wake + rendered, to do something after each mount's first render\n    ```ts\n    use.wake(() =\u003e use.rendered.then(() =\u003e {\n      console.log(\"after first render\")\n    }))\n    ```\n\n\n\n\u003cbr/\u003e\n\n## 🦝 sly dom multitool\n\u003e *\"it's not jquery!\"*\n\n### 💲 follow the money\n- import the dollarsign\n    ```ts\n    import {$} from \"@e280/sly\"\n    ```\n\n### 💲 dom queries\n- require an element\n    ```ts\n    $(\".demo\")\n      // HTMLElement (or throws error)\n    ```\n- request an element\n    ```ts\n    $.maybe(\".demo\")\n      // HTMLElement | undefined\n    ```\n- query all elements\n    ```ts\n    for (const item of $.all(\"ul li\"))\n      console.log(item)\n    ```\n- specify what element to query under\n    ```ts\n    $(\"li\", listElement)\n      // HTMLElement\n    ```\n\n### 💲 dom utilities\n- render content into an element\n    ```ts\n    $.render(element, html`\u003cp\u003ehello world\u003c/p\u003e`)\n    ```\n- register web components\n    ```ts\n    $.register({MyComponent, AnotherCoolComponent})\n      // \u003cmy-component\u003e\n      // \u003canother-cool-component\u003e\n    ```\n\n\n\n\u003cbr/\u003e\n\n## 🦝 sly ops, pods, and loaders\n\u003e *async operations and displaying loading spinners.*\n\n```ts\nimport {nap} from \"@e280/stz\"\nimport {Pod, podium, Op, makeLoader, anims} from \"@e280/sly\"\n```\n\n### 🫛 pods: loading/ready/error\n- a pod represents an async operation in terms of json-serializable data\n- there are three kinds of `Pod\u003cV\u003e`\n    ```ts\n    // loading pod\n    [\"loading\"]\n\n    // ready pod contains value 123\n    [\"ready\", 123]\n\n    // error pod contains an error\n    [\"error\", new Error()]\n    ```\n\n### 🫛 podium: helps you work with pods\n- get pod status\n    ```ts\n    podium.status([\"ready\", 123])\n      // \"ready\"\n    ```\n- get pod ready value (or undefined)\n    ```ts\n    podium.value([\"loading\"])\n      // undefined\n\n    podium.value([\"ready\", 123])\n      // 123\n    ```\n- see more at [podium.ts](./s/ops/podium.ts)\n\n### 🫛 ops: nice pod ergonomics\n- an `Op\u003cV\u003e` wraps a pod with a signal for reactivity\n- create an op\n    ```ts\n    const op = new Op\u003cnumber\u003e() // loading status by default\n    ```\n    ```ts\n    const op = Op.loading\u003cnumber\u003e()\n    ```\n    ```ts\n    const op = Op.ready\u003cnumber\u003e(123)\n    ```\n    ```ts\n    const op = Op.error\u003cnumber\u003e(new Error())\n    ```\n- 🔥 create an op that calls and tracks an async fn\n    ```ts\n    const op = Op.fn(async() =\u003e {\n      await nap(4000)\n      return 123\n    })\n    ```\n- await for the next ready value (or thrown error)\n    ```ts\n    await op // 123\n    ```\n- get pod info\n    ```ts\n    op.pod // [\"loading\"]\n    op.status // \"loading\"\n    op.value // undefined (or value if ready)\n    ```\n    ```ts\n    op.isLoading // true\n    op.isReady // false\n    op.isError // false\n    ```\n- select executes a fn based on the status\n    ```ts\n    const result = op.select({\n      loading: () =\u003e \"it's loading...\",\n      ready: value =\u003e `dude, it's ready! ${value}`,\n      error: err =\u003e `dude, there's an error!`,\n    })\n\n    result\n      // \"dude, it's ready! 123\"\n    ```\n- morph returns a new pod, transforming the value if ready\n    ```ts\n    op.morph(n =\u003e n + 1)\n      // [\"ready\", 124]\n    ```\n- you can combine a number of ops into a single pod like this\n    ```ts\n    Op.all(Op.ready(123), Op.loading())\n      // [\"loading\"]\n    ```\n    ```ts\n    Op.all(Op.ready(1), Op.ready(2), Op.ready(3))\n      // [\"ready\", [1, 2, 3]]\n    ```\n    - error if any ops are in error, otherwise\n    - loading if any ops are in loading, otherwise\n    - ready if all the ops are ready\n\n### 🫛 loaders: animated loading spinners\n- create a `loader` using `makeLoader`\n    ```ts\n    const loader = makeLoader(anims.dots)\n    ```\n    - see all the anims available on the testing page https://sly.e280.org/\n    - ngl, i made too many.. *i was having fun, okay?*\n- use the loader to render your op\n    ```ts\n    return html`\n      \u003ch2\u003ecool stuff\u003c/h2\u003e\n\n      ${loader(op, value =\u003e html`\n        \u003cdiv\u003e${value}\u003c/div\u003e\n      `)}\n    `\n    ```\n    - when the op is loading, the loading spinner will animate\n    - when the op is in error, the error will be displayed\n    - when the op is ready, your fn is called and given the value\n\n\n\n\u003cbr/\u003e\n\n## 🧑‍💻 sly by e280\nreward us with github stars  \nbuild with us at https://e280.org/ but only if you're cool  \n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fe280%2Fsly","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fe280%2Fsly","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fe280%2Fsly/lists"}