{"id":14990111,"url":"https://github.com/zeixcom/ui-element","last_synced_at":"2025-04-12T02:05:39.752Z","repository":{"id":227965038,"uuid":"772826109","full_name":"zeixcom/ui-element","owner":"zeixcom","description":"UIElement - the \"look ma, no JS framework!\" library bringing signals-based reactivity to Web Components","archived":false,"fork":false,"pushed_at":"2025-04-12T00:59:27.000Z","size":2667,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-12T02:05:29.793Z","etag":null,"topics":["custom-elements","effects","reactivity","signals","ui-element","web-components"],"latest_commit_sha":null,"homepage":"https://zeixcom.github.io/ui-element/","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/zeixcom.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","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}},"created_at":"2024-03-16T01:48:55.000Z","updated_at":"2025-04-01T11:10:42.000Z","dependencies_parsed_at":"2024-03-31T13:24:01.223Z","dependency_job_id":"757ef768-7b45-47fb-a27b-f121954f9200","html_url":"https://github.com/zeixcom/ui-element","commit_stats":{"total_commits":152,"total_committers":2,"mean_commits":76.0,"dds":0.02631578947368418,"last_synced_commit":"f8854e6f6b7586153a2c7966b177d09b625b7e07"},"previous_names":["efflore/ui-element","zeixcom/ui-element"],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeixcom%2Fui-element","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeixcom%2Fui-element/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeixcom%2Fui-element/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zeixcom%2Fui-element/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zeixcom","download_url":"https://codeload.github.com/zeixcom/ui-element/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248505863,"owners_count":21115354,"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":["custom-elements","effects","reactivity","signals","ui-element","web-components"],"created_at":"2024-09-24T14:19:28.255Z","updated_at":"2025-04-12T02:05:39.745Z","avatar_url":"https://github.com/zeixcom.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# UIElement\n\nVersion 0.11.0\n\n**UIElement** - transform reusable markup, styles and behavior into powerful, reactive, and maintainable Web Components.\n\n`UIElement` is a base class for Web Components with reactive states and UI effects. UIElement is tiny, around 4kB gzipped JS code, of which unused functions can be tree-shaken by build tools. It uses [Cause \u0026 Effect](https://github.com/zeixcom/cause-effect) internally for state management with signals and for scheduled DOM updates.\n\n## Key Features\n\n* **Reusable Components**: Create highly modular and reusable components to encapsulate styles and behavior.\n* **Declarative States**: Bring static, server-rendered content to life with dynamic interactivity and state management.\n* **Signal-Based Reactivity**: Employ signals for efficient state propagation, ensuring your components react instantly to changes.\n* **Declarative Effects**: Use granular effects to automatically synchronize UI states with minimal code.\n* **Context Support**: Share global states across your component tree without tightly coupling logic.\n\n## Installation\n\n```bash\n# with npm\nnpm install @zeix/ui-element\n\n# or with bun\nbun add @zeix/ui-element\n```\n\n## Documentation\n\nThe full documentation is still work in progress. The following chapters are already reasonably complete:\n\n* [Introduction](https://zeixcom.github.io/ui-element/index.html)\n* [Getting Started](https://zeixcom.github.io/ui-element/getting-started.html)\n* [Building Components](https://zeixcom.github.io/ui-element/building-components.html)\n* [Styling Components](https://zeixcom.github.io/ui-element/styling-components.html)\n* [Data Flow](https://zeixcom.github.io/ui-element/data-flow.html)\n* [About \u0026 Community](https://zeixcom.github.io/ui-element/about-community.html)\n\n## Basic Usage\n\n### Show Appreciation\n\nServer-rendered markup:\n\n```html\n\u003cshow-appreciation aria-label=\"Show appreciation\"\u003e\n    \u003cbutton type=\"button\"\u003e\n        \u003cspan class=\"emoji\"\u003e💐\u003c/span\u003e\n        \u003cspan class=\"count\"\u003e5\u003c/span\u003e\n    \u003c/button\u003e\n\u003c/show-appreciation\u003e\n```\n\nUIElement component:\n\n```js\nimport { UIElement, asInteger, setText } from '@zeix/ui-element'\n\nclass ShowAppreciation extends UIElement {\n    #count = Symbol() // Use a private Symbol as state key\n\n    connectedCallback() {\n        // Initialize count state\n        this.set(this.#count, asInteger(0)(this.querySelector('.count').textContent))\n\n        // Bind click event to increment count\n        this.first('button').on('click', () =\u003e {\n            this.set(this.#count, v =\u003e ++v)\n        })\n\n        // Update .count text when count changes\n        this.first('.count').sync(setText(this.#count))\n    }\n\n    // Expose read-only property for count\n    get count() {\n        return this.get(this.#count)\n    }\n}\nShowAppreciation.define('show-appreciation')\n```\n\nExample styles:\n\n```css\nshow-appreciation {\n    display: inline-block;\n\n    \u0026 button {\n        display: flex;\n        flex-direction: row;\n        gap: var(--space-s);\n        border: 1px solid var(--color-border);\n        border-radius: var(--space-xs);\n        background-color: var(--color-secondary);\n        color: var(--color-text);\n        padding: var(--space-xs) var(--space-s);\n        cursor: pointer;\n        font-size: var(--font-size-m);\n        line-height: var(--line-height-xs);\n        transition: transform var(--transition-short) var(--easing-inout);\n\n        \u0026:hover {\n            background-color: var(--color-secondary-hover);\n        }\n\n        \u0026:active {\n            background-color: var(--color-secondary-active);\n\n            .emoji {\n                transform: scale(1.1);\n            }\n        }\n    }\n}\n```\n\n### Tab List and Panels\n\nAn example demonstrating how to pass states from one component to another. Server-rendered markup:\n\n```html\n\u003ctab-list\u003e\n    \u003cmenu\u003e\n        \u003cli\u003e\u003cbutton type=\"button\" aria-pressed=\"true\"\u003eTab 1\u003c/button\u003e\u003c/li\u003e\n        \u003cli\u003e\u003cbutton type=\"button\"\u003eTab 2\u003c/button\u003e\u003c/li\u003e\n        \u003cli\u003e\u003cbutton type=\"button\"\u003eTab 3\u003c/button\u003e\u003c/li\u003e\n    \u003c/menu\u003e\n    \u003cdetails open\u003e\n        \u003csummary\u003eTab 1\u003c/summary\u003e\n        \u003cp\u003eContent of tab panel 1\u003c/p\u003e\n    \u003c/details\u003e\n    \u003cdetails\u003e\n        \u003csummary\u003eTab 2\u003c/summary\u003e\n        \u003cp\u003eContent of tab panel 2\u003c/p\u003e\n    \u003c/details\u003e\n    \u003cdetails\u003e\n        \u003csummary\u003eTab 3\u003c/summary\u003e\n        \u003cp\u003eContent of tab panel 3\u003c/p\u003e\n    \u003c/details\u003e\n\u003c/tab-list\u003e\n```\n\nUIElement components:\n\n```js\nimport { UIElement, setAttribute, toggleAttribute } from '@zeix/ui-element'\n\nclass TabList extends UIElement {\n    static localName = 'tab-list'\n    static observedAttributes = ['accordion']\n\n    init = {\n        active: 0,\n        accordion: asBoolean,\n    }\n\n    connectedCallback() {\n        super.connectedCallback()\n\n        // Set inital active tab by querying details[open]\n        const getInitialActive = () =\u003e { \n            const panels = Array.from(this.querySelectorAll('details'))\n            for (let i = 0; i \u003c panels.length; i++) {\n                if (panels[i].hasAttribute('open')) return i\n            }\n            return 0\n        }\n        this.set('active', getInitialActive())\n\n        // Reflect accordion attribute (may be used for styling)\n        this.self.sync(toggleAttribute('accordion'))\n\n        // Update active tab state and bind click handlers\n        this.all('menu button')\n            .on('click', (_, index) =\u003e () =\u003e {\n                this.set('active', index)\n            })\n            .sync(setProperty(\n                'ariaPressed',\n                (_, index) =\u003e String(this.get('active') === index)\n            ))\n\n        // Update details panels open, hidden and disabled states\n        this.all('details').sync(\n            setProperty(\n                'open',\n                (_, index) =\u003e !!(this.get('active') === index)\n            ),\n            setAttribute(\n                'aria-disabled',\n                () =\u003e String(!this.get('accordion'))\n            )\n        )\n\n        // Update summary visibility\n        this.all('summary').sync(toggleClass(\n            'visually-hidden',\n            () =\u003e !this.get('accordion')\n        ))\n    }\n}\nTabList.define()\n```\n\nExample styles:\n\n```css\ntab-list {\n\n    \u003e menu {\n        list-style: none;\n        display: flex;\n        gap: 0.2rem;\n        padding: 0;\n\n        \u0026 button[aria-pressed=\"true\"] {\n            color: purple;\n        }\n    }\n\n    \u003e details {\n\n        \u0026:not([open]) {\n            display: none;\n        }\n\n        \u0026[aria-disabled] {\n            pointer-events: none;\n        }\n    }\n\n    \u0026[accordion] {\n\n        \u003e menu {\n            display: none;\n        }\n\n        \u003e details:not([open]) {\n            display: block;\n        }\n    }\n}\n```\n\n### Lazy Load\n\nA more complex component demonstrating async fetch from the server:\n\n```html\n\u003clazy-load src=\"/lazy-load/snippet.html\"\u003e\n    \u003cdiv class=\"loading\" role=\"status\"\u003eLoading...\u003c/div\u003e\n    \u003cdiv class=\"error\" role=\"alert\" aria-live=\"polite\"\u003e\u003c/div\u003e\n\u003c/lazy-load\u003e\n```\n\n```js\nimport { UIElement, setProperty, setText, dangerouslySetInnerHTML } from '@zeix/ui-element'\n\nclass LazyLoad extends UIElement {\n    static localName = 'lazy-load'\n\n    // Remove the following line if you don't want to listen to changes in 'src' attribute\n    static observedAttributes = ['src']\n\n    init = {\n        src: v =\u003e { // Custom attribute parser\n            if (!v) {\n                this.set('error', 'No URL provided in src attribute')\n                return ''\n            } else if ((this.parentElement || this.getRootNode().host)?.closest(`${this.localName}[src=\"${v}\"]`)) {\n                this.set('error', 'Recursive loading detected')\n                return ''\n            }\n            const url = new URL(v, location.href) // Ensure 'src' attribute is a valid URL\n            if (url.origin === location.origin) // Sanity check for cross-origin URLs\n                return url.toString()\n            this.set('error', 'Invalid URL origin')\n            return ''\n        },\n        content: async () =\u003e { // Async Computed callback\n            const url = this.get('src')\n            if (!url) return ''\n            try {\n                const response = await fetch(this.get('src'))\n                this.querySelector('.loading')?.remove()\n                if (response.ok) return response.text()\n                else this.set('error', response.statusText)\n            } catch (error) {\n                this.set('error', error.message)\n            }\n            return ''\n        },\n        error: '',\n    }\n\n    connectedCallback() {\n        super.connectedCallback()\n\n        // Effect to set error message\n        this.first('.error').sync(\n            setProperty('hidden', () =\u003e !this.get('error')),\n            setText('error'),\n        )\n\n        // Effect to set content in shadow root\n        // Remove the second argument (for shadowrootmode) if you prefer light DOM\n        this.self.sync(dangerouslySetInnerHTML('content', 'open'))\n    }\n}\nLazyLoad.define()\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzeixcom%2Fui-element","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzeixcom%2Fui-element","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzeixcom%2Fui-element/lists"}