{"id":27276503,"url":"https://github.com/jameslovallo/ardi","last_synced_at":"2025-04-11T16:14:45.560Z","repository":{"id":58404388,"uuid":"531681490","full_name":"jameslovallo/ardi","owner":"jameslovallo","description":"Ardi makes it easy to create reactive custom elements that work with any website or Javascript framework.","archived":false,"fork":false,"pushed_at":"2023-07-20T03:43:36.000Z","size":10814,"stargazers_count":15,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-08-10T11:16:38.752Z","etag":null,"topics":["angular","context-api","custom-elements","react","svelte","uhtml","vue","web-components"],"latest_commit_sha":null,"homepage":"https://ardi.netlify.app","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jameslovallo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-09-01T20:48:03.000Z","updated_at":"2023-08-25T07:23:48.000Z","dependencies_parsed_at":"2023-02-09T21:15:34.148Z","dependency_job_id":null,"html_url":"https://github.com/jameslovallo/ardi","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jameslovallo%2Fardi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jameslovallo%2Fardi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jameslovallo%2Fardi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jameslovallo%2Fardi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jameslovallo","download_url":"https://codeload.github.com/jameslovallo/ardi/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248438516,"owners_count":21103410,"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":["angular","context-api","custom-elements","react","svelte","uhtml","vue","web-components"],"created_at":"2025-04-11T16:14:44.976Z","updated_at":"2025-04-11T16:14:45.538Z","avatar_url":"https://github.com/jameslovallo.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Ardi\n\n**Welcome to the Weightless Web**\n\nArdi makes it almost too easy to create reactive custom elements that work with any site or framework.\n\n\u003capp-link class=\"demo-link-container\"\u003e\n\n\u003ca href=\"https://ardi.netlify.app/demos\" class=\"demo-link\"\u003eCheck out the demos!\u003c/a\u003e\n\n\u003c/app-link\u003e\n\n## Features\n\n1. Object-oriented API\n2. Single-file components\n3. Reactive props and state\n4. Easy-to-use Context API\n5. Templates in [µhtml](https://www.npmjs.com/package/uhtml), [JSX](https://www.npmjs.com/package/jsx-dom), or [Handlebars](https://www.npmjs.com/package/handlebars)\n6. Helpful lifecycle callbacks\n7. No building, compiling, or tooling\n\n\u003chome-grid\u003e\n\n\u003chome-card\n  src=\"/@/assets/home/fast.svg\"\n  alt=\"rocket lifting off\"\n  heading=\"It's fast\"\n  text=\"Ardi is only 4kb and renders changes with scalpel-like precision.\"\n/\u003e\n\n\u003chome-card\n  src=\"/@/assets/home/familiar.svg\"\n  alt=\"guy chillin in a convertible in orbit\"\n  heading=\"It's familiar\"\n  text=\"Ardi's modern DX works with templates in µhtml, JSX or Handlebars.\"\n/\u003e\n\n\u003chome-card\n  src=\"/@/assets/home/portable.svg\"\n  alt=\"guy getting abducted by a UFO\"\n  heading=\"It's universal\"\n  text=\"Ardi works the same with JS apps, static sites, or all-in-one platforms.\"\n/\u003e\n\n\u003c/home-grid\u003e\n\n## Installation\n\nYou can use Ardi from NPM or a CDN.\n\n### NPM\n\n```sh\nnpm i ardi\n```\n\n```js\nimport ardi, { html } from 'ardi'\n\nardi({ tag: 'my-component' })\n```\n\n### CDN\n\n```html\n\u003cscript type=\"module\"\u003e\n  import ardi, { html } from '//unpkg.com/ardi'\n\n  ardi({ tag: 'my-component' })\n\u003c/script\u003e\n```\n\n## API\n\nArdi uses a straightforward object-oriented API. To demonstrate the API, we'll be looking at simplified code from the \u003capp-link\u003e[podcast demo](https://ardi.netlify.app/demos/podcast)\u003c/app-link\u003e.\n\n\u003cpodcast-embed pagesize=\"5\" style=\"max-width: var(--demo-max-width)\"\u003e\n\n![Podcast Component with 5 episodes of The Daily by The New York Times loaded.](https://ardi.netlify.com/@/assets/home/podcast-demo.png)\n\n\u003c/podcast-embed\u003e\n\n### Tag\n\nDefine the component's tag. The tag must follow the custom element [naming convention](https://html.spec.whatwg.org/#valid-custom-element-name). We'll call this component 'podcast-embed'.\n\n```js\nardi({\n  tag: 'podcast-embed',\n})\n```\n\n### Extends\n\nIf you are building a component that extends a default element, you can define the prototype and tag here. Note that Safari still does not support extending built-in elements 😭.\n\n```js\nardi({\n  extends: [HTMLAnchorElement, 'a'],\n})\n```\n\n### Shadow\n\nArdi renders to the [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) by default. You can disable this behavior if you need to.\n\n```js\nardi({\n  shadow: false,\n})\n```\n\n### Props\n\nProps allow you to configure a component using the element's attributes. To create a property, add a key under `props` whose value is an array containing a handler function and (optionally) a default value. The handler takes the string value from the prop's attribute and transforms it, i.e. from a string `'4'` to a number `4`. The handler can be a built-in function (like String, Number, or JSON.parse) or an arrow function. Every prop is reactive, which means that whether a prop's value is set internally or via its attribute, the change will trigger a render. Prop values are accessible directly from `this`, i.e. `this.pagesize`.\n\nHere are the props configured in the podcast demo.\n\n```js\nardi({\n  props: {\n    feed: [String, 'https://feeds.simplecast.com/54nAGcIl'],\n    pagesize: [Number, 10],\n    pagelabel: [String, 'Page'],\n    prevpagelabel: [String, 'Prevous Page'],\n    nextpagelabel: [String, 'Next Page'],\n    pauselabel: [String, 'pause'],\n    playlabel: [String, 'play'],\n  },\n})\n```\n\n### State\n\nState is a reactive container for data, which means any change will trigger a render. Values declared in state are accessible from `this`, i.e. `this.episodes`.\n\nHere is how state is defined in the podcast demo.\n\n```js\nardi({\n  state: () =\u003e ({\n    feedJSON: {},\n    nowPlaying: null,\n    page: 0,\n    paused: true,\n  }),\n})\n```\n\n### Template\n\n[μhtml](https://www.npmjs.com/package/uhtml) is the default template library, and it's just like JSX except you create your templates using tagged template literals. μhtml is extremely efficient. When the component's state changes, instead of re-rendering an entire element, μhtml makes tiny, surgical DOM updates as-needed.\n\n#### Event Handlers\n\nEvent handlers can be applied to an element using React's `on` syntax (`onClick`) or Vue's `@` syntax (`@click`). Here is a snippet showing the play/pause button for an episode in the podcast demo.\n\n\u003cdiv class=\"highlight-lines\"\u003e\n\n```js\nardi({\n  template() {\n    return html`\n      ...\n      \u003cbutton\n        part=\"play-button\"\n        @click=${() =\u003e this.playPause(track)}\n        aria-label=${this.nowPlaying === track \u0026\u0026 !this.paused\n          ? this.pauselabel\n          : this.playlabel}\n      \u003e\n        ${this.icon(\n          this.nowPlaying === track \u0026\u0026 !this.paused ? 'pause' : 'play'\n        )}\n      \u003c/button\u003e\n      ...\n    `\n  },\n})\n```\n\n\u003cdiv class=\"highlight\" style=\"--line: 7\"\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n#### Lists\n\nLists are handled using the `Array.map()` method. In the podcast demo, we will use a map to list the episodes that are returned by the xml feed. Lists generally do not require a key, but in cases where the order of elements changes you can add a key using `html.for(key)`.\n\n\u003cdiv class=\"highlight-lines\"\u003e\n\n```js\nardi({\n  template() {\n    return html`\n      ...\n      \u003cdiv part=\"episodes\"\u003e\n        ${this.episodes.map((episode) =\u003e {\n          return html`\u003cdiv part=\"episode\"\u003e...\u003c/div\u003e`\n        })}\n      \u003c/div\u003e\n      ...\n    `\n  },\n})\n```\n\n\u003cdiv class=\"highlight\" style=\"--line: 6; --lines: 3;\"\u003e\u003c/div\u003e\n\n\u003c/div\u003e\n\n#### Conditional Rendering\n\nTernary operators are the recommended way to handle conditional rendering. The snippet below shows how elements can be conditionally rendered based on the available data.\n\n\u003cdiv class=\"highlight-lines\"\u003e\n\n\u003c!-- prettier-ignore --\u003e\n```js\nardi({\n  template() {\n    return html`\n      ...\n      \u003caudio ref=\"player\" src=${this.nowPlaying} /\u003e\n      \u003cdiv part=\"header\"\u003e\n        ${image ? html`\u003cimg part=\"image\" src=${image} /\u003e` : ''}\n        \u003cdiv part=\"header-wrapper\"\u003e\n          ${title ? html`\u003cp part=\"title\"\u003e${title}\u003c/p\u003e` : ''}\n          ${author ? html`\u003cp part=\"author\"\u003e${author}\u003c/p\u003e` : ''}\n          ${link ? html`\u003ca part=\"link\" href=${link}\u003e${link}\u003c/a\u003e` : ''}\n        \u003c/div\u003e\n      \u003c/div\u003e\n      ${description ? html`\u003cp part=\"description\"\u003e${description}\u003c/p\u003e` : ''}\n      ...\n    `\n  },\n})\n```\n\n\u003cdiv class=\"highlight\" style=\"--line: 7\"\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\" style=\"--line: 9; --lines: 3;\"\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\" style=\"--line: 14\"\u003e\u003c/div\u003e\n\n\u003c/div\u003e\n\nIf you prefer a more HTML-like syntax, Ardi provides a `\u003cif-else\u003e` element that you can use instead. To use it, just assign the `if` prop with a condition and nest your element inside. If you want to provide a fallback element, you can assign it to the `else` slot and it will be displayed if the condition is falsey. You can see this in action in the \u003capp-link\u003e[Employee Card](/demos/employee)\u003c/app-link\u003e component.\n\n```js\nardi({\n  template() {\n    return html`\n      \u003cif-else if=${image}\u003e\n        \u003cimg part=\"image\" src=${image} /\u003e\n        \u003csvg slot=\"else\" viewBox=\"0 0 24 24\"\u003e\n          \u003cpath d=\"...\" /\u003e\n        \u003c/svg\u003e\n      \u003c/if-else\u003e\n    `\n  },\n})\n```\n\n#### Slots\n\nArdi components use the [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) by default, which means you can use [\u0026lt;slot\u0026gt;](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement) tags to project nested elements into your templates. You can use a single default slot or multiple named slots.\n\nThe podcast demo has two named slots allowing the pagination button icons to be customized.\n\n\u003cdiv class=\"highlight-lines\"\u003e\n\n```js\nardi({\n  template() {\n    return html`\n      ...\n      \u003cbutton\n        part=\"pagination-prev\"\n        @click=${() =\u003e this.page--}\n        disabled=${this.page \u003e 0 ? null : true}\n        aria-label=${this.prevpagelabel}\n      \u003e\n        \u003cslot name=\"prev-icon\"\u003e ${this.icon('leftArrow')} \u003c/slot\u003e\n      \u003c/button\u003e\n      ...\n    `\n  },\n})\n```\n\n\u003cdiv class=\"highlight\" style=\"--line: 11\"\u003e\u003c/div\u003e\n\n\u003c/div\u003e\n\n#### Refs\n\nArdi allows you to add ref attributes to elements in your template, which are accessible from `this.refs`.\n\nIn the podcast component, the `player` ref is used by the `togglePlayback` method to control playback.\n\n\u003cdiv class=\"highlight-lines\"\u003e\n\n```js\nardi({\n  template() {\n    return html`\u003caudio ref=\"player\" src=${this.nowPlaying} /\u003e...`\n  },\n  togglePlayback() {\n    // ...\n    this.refs.player.play()\n    // ...\n  },\n})\n```\n\n\u003cdiv class=\"highlight\" style=\"--line: 3\"\u003e\u003c/div\u003e\n\u003cdiv class=\"highlight\" style=\"--line: 7\"\u003e\u003c/div\u003e\n\n\u003c/div\u003e\n\n### Methods\n\nYou can add any number of methods in your component and access them via `this`. Custom methods can be used in your template, in lifecycle callbacks, or inside of other methods. For examples, you can view the complete code for the \u003capp-link\u003e[podcast demo](https://ardi.netlify.app/demos/demo)\u003c/app-link\u003e. There are many more examples in components listed on the \u003capp-link\u003e[demos page](https://ardi.netlify.app/demos)\u003c/app-link\u003e.\n\n### Context\n\nArdi has a powerful and easy to use context api, allowing one component to share and synchronize its props or state with multiple child components. You can see this API in action in the \u003capp-link\u003e[i18n demo](https://ardi.netlify.app/demos/i18n)\u003c/app-link\u003e, this [CodePen example](https://codepen.io/jameslovallo/pen/poZaXqq?editors=0010), and in the CSS section below.\n\nTo share context from a parent component, add the `context` attribute with a descriptive name, i.e. `context=\"theme\"` You can then use `this.context(\"theme\")` to reference the element and access its props or state. When a child component uses the context to make changes to the parent element's props or state, the parent element will notify every other child component that accesses the same values, keeping the context synchronized throughout the application.\n\n```html\n\u003cardi-component context=\"theme\"\u003e\u003c/ardi-component\u003e\n```\n\n### Styles\n\nArdi components use the [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) by default. Elements in the Shadow DOM can access [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) declared on the page. Elements can also be styled using [part attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/part).\n\n#### Inline CSS\n\nYou can use Javascript in an inline style attribute.\n\n\u003cdiv class=\"highlight-lines\"\u003e\n\n```js\nardi({\n  template() {\n    const { bg, color } = this.context('theme')\n    return html`\u003cnav style=${`background: ${bg}; color: ${color};`}\u003e...\u003c/nav\u003e`\n  },\n})\n```\n\n\u003cdiv class=\"highlight\" style=\"--line: 4\"\u003e\u003c/div\u003e\n\n\u003c/div\u003e\n\n#### Styles Key\n\nIf you have a lot of CSS, it's cleaner to create a `styles` key. Ardi provides a `css` helper function to facilitate working with VSCode and other IDEs that support tagged template literals.\n\n\u003cdiv class=\"highlight-lines\"\u003e\n\n```js\nimport ardi, { css, html } from '//unpkg.com/ardi'\n\nardi({\n  template() {\n    const { bg, color } = this.context('theme')\n    return html`\u003cnav style=${`--bg: ${bg}; --color: ${color};`}\u003e...\u003c/nav\u003e`\n  },\n  styles: css`\n    nav {\n      background: var(--bg);\n      color: var(--color);\n    }\n  `,\n})\n```\n\n\u003cdiv class=\"highlight\" style=\"--line: 8; --lines: 6;\"\u003e\u003c/div\u003e\n\n\u003c/div\u003e\n\n#### Styles Function\n\nIf you prefer, you can also use Javascript variables and functions directly in your CSS by creating the `styles` key as a function.\n\n\u003cdiv class=\"highlight-lines\"\u003e\n\n```js\nardi({\n  template() {\n    return html`\u003cnav\u003e...\u003c/nav\u003e`\n  },\n  styles() {\n    const { bg, color } = this.context('theme')\n    return `\n      nav {\n        background: ${bg};\n        color: ${color};\n      }\n    `\n  },\n})\n```\n\n\u003cdiv class=\"highlight\" style=\"--line: 5; --lines: 9;\"\u003e\u003c/div\u003e\n\n\u003c/div\u003e\n\n#### CSS Pre-Processors\n\nArdi is a runtime framework, designed to work with any app, site, or platform. Since Sass and Less are build-time languages, no official support is provided. If you want to write styles in sass and use them in your components, you can always compile to native CSS and `@import` the file using Ardi's `styles` key.\n\nMany Sass features are redundant when using the Shadow DOM. Complex BEM selectors requiring `\u0026` nesting are unnecessary because styles are scoped to the component, and CSS has native support for variables. Nesting is even coming soon. That being said, if cannot live without using SASS (or another pre-processor) for prototyping, here is \u003capp-link\u003e[a demo](https://ardi.netlify.app/demos/sass)\u003c/app-link\u003e showing how you can.\n\n## Lifecycle\n\nArdi has several lifecycle callbacks, providing a convenient way to fetch data or apply effects.\n\n### created()\n\nThis callback runs as soon as the component is initialized. This is a good place to load data, setup observers, etc.\n\nA great example of this is in the \u003capp-link\u003e[forecast demo](https://ardi.netlify.app/demos/forecast)\u003c/app-link\u003e, where a resize observer is created to apply styles based on the component's rendered width (regardless of the viewport width).\n\n```js\nardi({\n  tag: 'ardi-forecast',\n  created() {\n    new ResizeObserver(() =\u003e\n      requestAnimationFrame(\n        () =\u003e (this.small = this.clientWidth \u003c= this.breakpoint)\n      )\n    ).observe(this)\n  },\n})\n```\n\n### ready()\n\nThis callback runs as soon as the component's template is rendered, allowing you to call methods that access refs defined in the template.\n\n### rendered()\n\nThis method runs each time the component renders an update. This was added to support event listeners when writing templates with Handlebars or untagged template literals, but you can use this method for any purpose.\n\n### changed()\n\nAlthough props are reactive, meaning the template is automatically updated when a prop's value changes, you may encounter scenarios where you need to handle a property's value manually, i.e. to fetch data or apply an effect. You can use this callback to observe and respond to prop updates.\n\nHere is an example from the \u003capp-link\u003e[forecast demo](https://ardi.netlify.app/demos/forecast)\u003c/app-link\u003e.\n\n```js\nardi({\n  tag: 'ardi-forecast',\n  changed(prop) {\n    if (\n      prop.old \u0026\u0026\n      prop.new \u0026\u0026\n      ['lat', 'lon', 'locale', 'unit'].includes(prop.name)\n    ) {\n      this.fetchForecast()\n    }\n  },\n})\n```\n\n### intersected()\n\nThis method is called when the component is scrolled into view. You can use the ratio parameter to determine how much of the component should be visible before you apply an effect. Ardi will only create the intersection observer if you include this method, so omit it if you do not intend to use it.\n\nIn the \u003capp-link\u003e[forecast demo](https://ardi.netlify.app/demos/forecast)\u003c/app-link\u003e, the intersect method is used to lazy-load data once the component is scrolled into view. This trick can save a lot of money if you use paid APIs!\n\n```js\nardi({\n  tag: 'ardi-forecast',\n  intersected(r) {\n    if (!this.gotWeather \u0026\u0026 r \u003e 0.2) {\n      this.fetchForecast()\n    }\n  },\n})\n```\n\n## Template Options\n\nμhtml is tiny, fast and efficient, and we strongly recommend it. However, JSX is king right now, and Handlebars is still holding on strong. That's why Ardi allows you to use whatever template library you prefer. Sample code for each supported option is provided below, for comparison. There is also an interactive [CodePen demo](https://codepen.io/jameslovallo/pen/WNKpqMj?editors=0010) showing all three examples.\n\n### μhtml\n\n\u003c!--prettier-ignore--\u003e\n```js\nimport ardi, { html } from '//unpkg.com/ardi'\n\nardi({\n  tag: 'uhtml-counter',\n  state: () =\u003e ({ count: 0 }),\n  template() {\n    return html`\n    \u003cbutton @click=${() =\u003e this.count++}\u003e\n      Count: ${this.count}\n    \u003c/button\u003e`\n  },\n})\n```\n\n### JSX-Dom\n\n\u003c!--prettier-ignore--\u003e\n```js\nimport ardi, { html } from '//unpkg.com/ardi'\nimport React from '//cdn.skypack.dev/jsx-dom'\n\nardi({\n  tag: 'jsx-counter',\n  state: () =\u003e ({ count: 0 }),\n  template() {\n    return (\n      \u003cbutton onClick={() =\u003e this.count++}\u003e\n        Count: {this.count}\n      \u003c/button\u003e\n    )\n  },\n})\n```\n\n### Handlebars\n\nWith Handlebars (or any template that returns a simple string: i.e. an untagged template literal), event listeners can be added to the `rendered` method. If present, the `rendered` method will run after each render.\n\n\u003cdiv class=\"highlight-lines\"\u003e\n\n```js\nimport ardi, { html } from '//unpkg.com/ardi'\nimport handlebars from 'https://cdn.skypack.dev/handlebars@4.7.7'\n\nardi({\n  tag: 'hbs-counter',\n  state: () =\u003e ({ count: 0 }),\n  template() {\n    return handlebars.compile(\n      `\u003cbutton ref='counter'\u003eCount: {{count}}\u003c/button\u003e`\n    )(this)\n  },\n  rendered() {\n    this.refs.counter.addEventListener('click', () =\u003e this.count++)\n  },\n})\n```\n\n\u003cdiv class=\"highlight\" style=\"--line: 12; --lines: 3;\"\u003e\u003c/div\u003e\n\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjameslovallo%2Fardi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjameslovallo%2Fardi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjameslovallo%2Fardi/lists"}