{"id":18997316,"url":"https://github.com/chase-moskal/magical","last_synced_at":"2026-05-18T14:03:09.278Z","repository":{"id":91093465,"uuid":"500556779","full_name":"chase-moskal/magical","owner":"chase-moskal","description":"web toolkit for lit apps","archived":false,"fork":false,"pushed_at":"2023-11-29T09:35:40.000Z","size":449,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-01T16:47:29.933Z","etag":null,"topics":["lit-element","lit-html","web-components"],"latest_commit_sha":null,"homepage":"https://magical.chasemoskal.com/","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/chase-moskal.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}},"created_at":"2022-06-06T18:53:32.000Z","updated_at":"2023-03-04T05:15:30.000Z","dependencies_parsed_at":null,"dependency_job_id":"0af5b267-8089-451e-bb48-ca828a02c1dd","html_url":"https://github.com/chase-moskal/magical","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chase-moskal%2Fmagical","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chase-moskal%2Fmagical/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chase-moskal%2Fmagical/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chase-moskal%2Fmagical/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chase-moskal","download_url":"https://codeload.github.com/chase-moskal/magical/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240012261,"owners_count":19733875,"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","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-element","lit-html","web-components"],"created_at":"2024-11-08T17:38:58.221Z","updated_at":"2026-04-16T21:30:27.973Z","avatar_url":"https://github.com/chase-moskal.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n🪄 magical\n==========\n\n*web toolkit for [lit](https://lit.dev/) apps*\n\n🕹️ [**live demo — magical.chasemoskal.com**](https://magical.chasemoskal.com/)  \n📦 `npm install @chasemoskal/magical`  \n💖 *made with open source love*  \n\nmagical is a collection of tools we build, maintain, and use every day to make great [lit](https://lit.dev/) applications.\n\n\u003cbr/\u003e\n\n## 🤖 magic `element`\n\nevery magic element is also a lit element.\n\nbut magic elements have a `realize` method instead of a *render* method.\n\nin your `realize` method, use `this.use`, to get access to a \"hooks\" interface for state management.\n\n```ts\nimport {MagicElement, mixinCss, UseElement} from \"@chasemoskal/magical\"\n\nimport {html} from \"lit\"\nimport {property} from \"lit/decorators.js\"\nimport stylesCss from \"./styles.css.js\"\n\n@mixinCss(stylesCss)\nexport class CounterElement extends MagicElement {\n\n  @property({type: Number})\n  start = 0\n\n  realize() {\n    const {use} = this\n    const [count, setCount] = use.state(this.start)\n    const increment = () =\u003e setCount(x =\u003e x + 1)\n\n    use.setup(() =\u003e {\n      const listener = () =\u003e console.log(\"resized\")\n      window.addEventListener(\"resize\", listener)\n      return () =\u003e window.removeEventListener(\"resize\", listener)\n    })\n\n    return html`\n      \u003cdiv\u003e\n        \u003cp\u003ecount ${count}\u003c/p\u003e\n        \u003cbutton @click=${increment}\u003eincrement\u003c/button\u003e\n      \u003c/div\u003e\n    `\n  }\n}\n```\n\nthere are some things to know about:\n- you should never access `use` outside of `realize`\n- like any hooks interface, your `use` calls must be in the same order every time\n  - so don't put `use.state` or `use.setup` calls inside a for loop or in a callback function or anything like that\n  - best practice is to keep use calls at the top-level\n- `use.state` returns an array with four things:\n  - the current value\n  - the setter function\n    - you can pass it a new value\n    - or a function that takes the previous value and returns a new value\n  - the getter function\n    - the getter is useful getting the latest version of state in a callback\n  - the previous value\n    - you could compare current===previous to see if the value has changed\n- `use.setup`\n  - use this to run a setup routine every time the component connects to the dom\n  - 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\n\n\u003cbr/\u003e\n\n## ✨ magic `view`\n\nviews have the same `use` hook interface, but views are not components or elements.\n\nthey're *lit directives.*\n\nbut like elements, views too can have a shadow dom, and their own css styles.\n\n```ts\nimport {view} from \"@chasemoskal/magical\"\n\nimport {html} from \"lit\"\nimport stylesCss from \"./styles.css.js\"\n\nexport const CounterView = view({\n    shadow: true,\n    styles: stylesCss,\n  }, use =\u003e (start: number) =\u003e {\n\n  const [count, setCount] = use.state(start)\n  const increment = () =\u003e setCount(x =\u003e x + 1)\n\n  return html`\n    \u003cdiv\u003e\n      \u003cp\u003ecount ${count}\u003c/p\u003e\n      \u003cbutton @click=${increment}\u003eincrement\u003c/button\u003e\n    \u003c/div\u003e\n  `\n})\n```\n\nthe important thing to understand, is how they are used:\n- views are used like this:\n    ```ts\n    // 🧐\n    return html`\n      \u003cdiv\u003e\n        ${CounterView(2)}\n      \u003c/div\u003e\n    `\n    ```\n    - this is great, because CounterView is fully typescript-typed\n    - and it's directly imported, so it's easy to trace where views are being used (vscode find all references)\n    - typescript will sniff out and complain about places you need to change when you update those parameters\n- whereas using an element would be like this:\n    ```ts\n    // 🤮\n    return html`\n      \u003cdiv\u003e\n        \u003ccounter-element start=2\u003e\u003c/counter-element\u003e\n      \u003c/div\u003e\n    `\n    ```\n    - this is OK for an html-only interface, but for real app development?\n    - this sucks, no typescript typing\n    - no imports, no vscode find all references\n    - have to worry about dom registrations\n    - views solve all of this\n\ncompared against elements:\n- views are typescript functions, so their parameters are fully typed, vscode auto-refactoring works\n- views are less cumbersome, because they don't need to be registered to the dom\n\ncompared against simple render functions:\n- views have state\n- views are independent rendering contexts\n- views can have shadow dom and their own stylesheets\n\ni think a good way to think about elements and views is like this:\n- elements are entrypoints at the html-level\n- most of our app features are implemented as views\n- our views are comprised of simple render functions\n\n\u003cbr/\u003e\n\n## 📻 magic `event`\n\nwe have this handy helper for making custom dom events.\n\n```js\nimport {MagicEvent} from \"@chasemoskal/magical\"\n\nexport class ProfileChanged extends\n  MagicEvent\u003c{count: number}\u003e(\"profile_changed\") {}\n\n// dispatch the event\nMyCoolEvent\n  .target(window)\n  .dispatch({count: 1})\n\n// listen for the event\nconst unlisten = MyCoolEvent\n  .target(window)\n  .listen(event =\u003e {\n    console.log(\"profile changed\", event.detail.count)\n  })\n```\n\ninstead of extending MagicEvent, you can just use `ev` directly to listen and dispatch custom events:\n\n```js\nimport {ev} from \"@chasemoskal/magical\"\n\nev(MyCustomEvent)\n  .target(window)\n  .dispatch({lol: \"example\"})\n\nconst unlisten = ev(MyCustomEvent)\n  .target(window)\n  .listen(event =\u003e {\n    console.log(\"example event\", event.detail.lol)\n  })\n```\n\n\u003cbr/\u003e\n\n## 🐫 camel `css`\n\nwe wanted sass-like css nesting, but in our web components.\n\nso we built a parser and compiler for a new css language.\n\nit can run serverside, as part of a build script, or our preferred method — live on the clientside, compiling stylesheets for our elements and views.\n\ncamel css can be a drop-in replacement for lit's css tagged-template function:\n\n```js\nimport {css} from \"@chasemoskal/magical\"\n\nconst styles = css`\ndiv {\n  p { color: red; }\n}\n`\n```\n\ncamel-css uses `^` instead of sass's `\u0026`\n\n\u003cbr/\u003e\n\n## 🪄 more magical tools\n\n### ⚙️ `registerElements` and `themeElements`\n\nfor the love of god, if you're writing a web components library, do not call `customElements.define` in those component modules.\n\nbe polite, and allow us the opportunity to augment your elements, rename them, apply a css theme, and then we can register our augmented elements.\n\nso, when we're making a library, we like to have a function like `getElements` that returns all the library's elements classes.\n\nthen it's easy for anybody to apply a css theme and register the elements:\n\n```js\nimport {registerElements, themeElements} from \"@chasemoskal/magical\"\n\nregisterElements(\n  themeElements(\n    themeCss,\n    getElements(),\n  )\n)\n```\n\n- registerElements will automatically take `CamelCaseComponent` names and convert them into `camel-case-component` names\n\n### 🎨 `mixins` for your lit elements\n\n*TODO documentation for these*\n\n- `mixinCss`\n- `mixinLightDom`\n- `mixinRefreshInterval`\n- `mixinContextRequired`\n\n### 🏀 `debounce`\n\ni've made like ten versions of this, and i think this is my masterpiece. it even has unit tests.\n\n```js\nimport {debounce} from \"@chasemoskal/magical\"\n\nconst action = () =\u003e console.log(\"action!\")\nconst debouncedAction = debounce(1000, action)\n// debouncedAction is a promise that resolves\n// after the 1000 millseconds of no activity\n\ndebouncedAction()\ndebouncedAction()\nawait debouncedAction()\n//\u003e \"action!\"\n// the action only fires once\n```\n\nthis debouncer\n- typescript\n- works with functions or async functions\n- returns promises\n- the promises resolve with the actual value\n\n\u003cbr/\u003e\n\u003cbr/\u003e\n\n------\n\n\u0026nbsp; \u0026nbsp; 💖 *made with open source love*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchase-moskal%2Fmagical","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchase-moskal%2Fmagical","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchase-moskal%2Fmagical/lists"}