An open API service indexing awesome lists of open source software.

https://github.com/zeixcom/ui-element

UIElement - the "look ma, no JS framework!" library bringing signals-based reactivity to Web Components
https://github.com/zeixcom/ui-element

custom-elements effects reactivity signals ui-element web-components

Last synced: about 1 year ago
JSON representation

UIElement - the "look ma, no JS framework!" library bringing signals-based reactivity to Web Components

Awesome Lists containing this project

README

          

# UIElement

Version 0.11.0

**UIElement** - transform reusable markup, styles and behavior into powerful, reactive, and maintainable Web Components.

`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 & Effect](https://github.com/zeixcom/cause-effect) internally for state management with signals and for scheduled DOM updates.

## Key Features

* **Reusable Components**: Create highly modular and reusable components to encapsulate styles and behavior.
* **Declarative States**: Bring static, server-rendered content to life with dynamic interactivity and state management.
* **Signal-Based Reactivity**: Employ signals for efficient state propagation, ensuring your components react instantly to changes.
* **Declarative Effects**: Use granular effects to automatically synchronize UI states with minimal code.
* **Context Support**: Share global states across your component tree without tightly coupling logic.

## Installation

```bash
# with npm
npm install @zeix/ui-element

# or with bun
bun add @zeix/ui-element
```

## Documentation

The full documentation is still work in progress. The following chapters are already reasonably complete:

* [Introduction](https://zeixcom.github.io/ui-element/index.html)
* [Getting Started](https://zeixcom.github.io/ui-element/getting-started.html)
* [Building Components](https://zeixcom.github.io/ui-element/building-components.html)
* [Styling Components](https://zeixcom.github.io/ui-element/styling-components.html)
* [Data Flow](https://zeixcom.github.io/ui-element/data-flow.html)
* [About & Community](https://zeixcom.github.io/ui-element/about-community.html)

## Basic Usage

### Show Appreciation

Server-rendered markup:

```html


💐
5

```

UIElement component:

```js
import { UIElement, asInteger, setText } from '@zeix/ui-element'

class ShowAppreciation extends UIElement {
#count = Symbol() // Use a private Symbol as state key

connectedCallback() {
// Initialize count state
this.set(this.#count, asInteger(0)(this.querySelector('.count').textContent))

// Bind click event to increment count
this.first('button').on('click', () => {
this.set(this.#count, v => ++v)
})

// Update .count text when count changes
this.first('.count').sync(setText(this.#count))
}

// Expose read-only property for count
get count() {
return this.get(this.#count)
}
}
ShowAppreciation.define('show-appreciation')
```

Example styles:

```css
show-appreciation {
display: inline-block;

& button {
display: flex;
flex-direction: row;
gap: var(--space-s);
border: 1px solid var(--color-border);
border-radius: var(--space-xs);
background-color: var(--color-secondary);
color: var(--color-text);
padding: var(--space-xs) var(--space-s);
cursor: pointer;
font-size: var(--font-size-m);
line-height: var(--line-height-xs);
transition: transform var(--transition-short) var(--easing-inout);

&:hover {
background-color: var(--color-secondary-hover);
}

&:active {
background-color: var(--color-secondary-active);

.emoji {
transform: scale(1.1);
}
}
}
}
```

### Tab List and Panels

An example demonstrating how to pass states from one component to another. Server-rendered markup:

```html


  • Tab 1

  • Tab 2

  • Tab 3



  • Tab 1

    Content of tab panel 1




    Tab 2

    Content of tab panel 2




    Tab 3

    Content of tab panel 3


    ```

    UIElement components:

    ```js
    import { UIElement, setAttribute, toggleAttribute } from '@zeix/ui-element'

    class TabList extends UIElement {
    static localName = 'tab-list'
    static observedAttributes = ['accordion']

    init = {
    active: 0,
    accordion: asBoolean,
    }

    connectedCallback() {
    super.connectedCallback()

    // Set inital active tab by querying details[open]
    const getInitialActive = () => {
    const panels = Array.from(this.querySelectorAll('details'))
    for (let i = 0; i < panels.length; i++) {
    if (panels[i].hasAttribute('open')) return i
    }
    return 0
    }
    this.set('active', getInitialActive())

    // Reflect accordion attribute (may be used for styling)
    this.self.sync(toggleAttribute('accordion'))

    // Update active tab state and bind click handlers
    this.all('menu button')
    .on('click', (_, index) => () => {
    this.set('active', index)
    })
    .sync(setProperty(
    'ariaPressed',
    (_, index) => String(this.get('active') === index)
    ))

    // Update details panels open, hidden and disabled states
    this.all('details').sync(
    setProperty(
    'open',
    (_, index) => !!(this.get('active') === index)
    ),
    setAttribute(
    'aria-disabled',
    () => String(!this.get('accordion'))
    )
    )

    // Update summary visibility
    this.all('summary').sync(toggleClass(
    'visually-hidden',
    () => !this.get('accordion')
    ))
    }
    }
    TabList.define()
    ```

    Example styles:

    ```css
    tab-list {

    > menu {
    list-style: none;
    display: flex;
    gap: 0.2rem;
    padding: 0;

    & button[aria-pressed="true"] {
    color: purple;
    }
    }

    > details {

    &:not([open]) {
    display: none;
    }

    &[aria-disabled] {
    pointer-events: none;
    }
    }

    &[accordion] {

    > menu {
    display: none;
    }

    > details:not([open]) {
    display: block;
    }
    }
    }
    ```

    ### Lazy Load

    A more complex component demonstrating async fetch from the server:

    ```html

    Loading...

    ```

    ```js
    import { UIElement, setProperty, setText, dangerouslySetInnerHTML } from '@zeix/ui-element'

    class LazyLoad extends UIElement {
    static localName = 'lazy-load'

    // Remove the following line if you don't want to listen to changes in 'src' attribute
    static observedAttributes = ['src']

    init = {
    src: v => { // Custom attribute parser
    if (!v) {
    this.set('error', 'No URL provided in src attribute')
    return ''
    } else if ((this.parentElement || this.getRootNode().host)?.closest(`${this.localName}[src="${v}"]`)) {
    this.set('error', 'Recursive loading detected')
    return ''
    }
    const url = new URL(v, location.href) // Ensure 'src' attribute is a valid URL
    if (url.origin === location.origin) // Sanity check for cross-origin URLs
    return url.toString()
    this.set('error', 'Invalid URL origin')
    return ''
    },
    content: async () => { // Async Computed callback
    const url = this.get('src')
    if (!url) return ''
    try {
    const response = await fetch(this.get('src'))
    this.querySelector('.loading')?.remove()
    if (response.ok) return response.text()
    else this.set('error', response.statusText)
    } catch (error) {
    this.set('error', error.message)
    }
    return ''
    },
    error: '',
    }

    connectedCallback() {
    super.connectedCallback()

    // Effect to set error message
    this.first('.error').sync(
    setProperty('hidden', () => !this.get('error')),
    setText('error'),
    )

    // Effect to set content in shadow root
    // Remove the second argument (for shadowrootmode) if you prefer light DOM
    this.self.sync(dangerouslySetInnerHTML('content', 'open'))
    }
    }
    LazyLoad.define()
    ```