diff --git a/docs/assets/css/components.css b/docs/assets/css/components.css index 0087dfc..e90a389 100644 --- a/docs/assets/css/components.css +++ b/docs/assets/css/components.css @@ -275,6 +275,7 @@ input-button { padding: 0 var(--space-s); font-size: var(--font-size-s); line-height: var(--line-height-s); + white-space: nowrap; opacity: var(--opacity-dimmed); transition: all var(--transition-shorter) var(--easing-inout); diff --git a/docs/assets/css/global.css b/docs/assets/css/global.css index f7ccc28..5031653 100644 --- a/docs/assets/css/global.css +++ b/docs/assets/css/global.css @@ -75,6 +75,8 @@ --easing-in: ease-in; --easing-out: ease-out; --easing-inout: ease-in-out; + + --input-height: 2rem; } @media screen and (min-width: 45em) and (max-width: 75em) { diff --git a/docs/assets/js/main.js b/docs/assets/js/main.js index 0775457..4178a45 100644 --- a/docs/assets/js/main.js +++ b/docs/assets/js/main.js @@ -389,7 +389,7 @@ InputRadiogroup.define('input-radiogroup') class LazyLoad extends UIElement { static observedAttributes = ['src'] static attributeMap = { - src: v => v.map(src => { + src: v => v.map(src => { let url = '' try { url = new URL(src, location.href) // ensure 'src' attribute is a valid URL @@ -402,20 +402,20 @@ class LazyLoad extends UIElement { } return url.toString() }) - } + } connectedCallback() { - // show / hide loading message + // Show / hide loading message this.first('.loading') .forEach(setProperty('ariaHidden', () => !!this.get('error'))) - // set and show / hide error message + // Set and show / hide error message this.first('.error') .map(setText('error')) .forEach(setProperty('ariaHidden', () => !this.get('error'))) - // load content from provided URL + // Load content from provided URL effect(enqueue => { const src = this.get('src') if (!src) return // silently fail if no valid URL is provided @@ -434,7 +434,7 @@ class LazyLoad extends UIElement { }) }) this.set('error', '') - } else { + } else { this.set('error', response.status + ':'+ response.statusText) } }) @@ -525,44 +525,40 @@ class MediaContext extends UIElement { MediaContext.define('media-context') class TodoApp extends UIElement { - connectedCallback() { + connectedCallback() { const [todoList, todoFilter] = ['todo-list', 'input-radiogroup'] .map(selector => this.querySelector(selector)) // Event listener on own element - this.self - .map(on('add-todo', e => todoList?.addItem(e.detail))) - - // Coordinate todo-count - this.first('todo-count') - .map(pass({ active: () => todoList?.get('count').active })) - - // Coordinate todo-list - this.first('todo-list') - .map(pass({ filter: () => todoFilter?.get('value') })) - - // Coordinate .clear-completed button - this.first('.clear-completed') - .map(on('click', () => todoList?.clearCompleted())) - .map(pass({ disabled: () => !todoList?.get('count').completed })) - } + this.self.forEach(on('add-todo', e => todoList?.addItem(e.detail))) + + // Coordinate todo-count + this.first('todo-count').forEach(pass({ + active: () => todoList?.get('count').active + })) + + // Coordinate todo-list + this.first('todo-list').forEach(pass({ + filter: () => todoFilter?.get('value') + })) + + // Coordinate .clear-completed button + this.first('.clear-completed') + .map(on('click', () => todoList?.clearCompleted())) + .forEach(pass({ disabled: () => !todoList?.get('count').completed })) + } } TodoApp.define('todo-app') class TodoCount extends UIElement { connectedCallback() { - this.set('active', 0, false) - this.first('.count') - .map(setText('active')) - this.first('.singular') - .map(setProperty('ariaHidden', () => this.get('active') > 1)) - this.first('.plural') - .map(setProperty('ariaHidden', () => this.get('active') === 1)) - this.first('.remaining') - .map(setProperty('ariaHidden', () => !this.get('active'))) - this.first('.all-done') - .map(setProperty('ariaHidden', () => !!this.get('active'))) - } + this.set('active', 0, false) + this.first('.count').forEach(setText('active')) + this.first('.singular').forEach(setProperty('ariaHidden', () => this.get('active') > 1)) + this.first('.plural').forEach(setProperty('ariaHidden', () => this.get('active') === 1)) + this.first('.remaining').forEach(setProperty('ariaHidden', () => !this.get('active'))) + this.first('.all-done').forEach(setProperty('ariaHidden', () => !!this.get('active'))) + } } TodoCount.define('todo-count') @@ -570,73 +566,71 @@ class TodoForm extends UIElement { connectedCallback() { const inputField = this.querySelector('input-field') - this.first('form') - .forEach(on('submit', e => { - e.preventDefault() - setTimeout(() => { - this.dispatchEvent(new CustomEvent('add-todo', { - bubbles: true, - detail: inputField.get('value') - })) - inputField.clear() - }, 0) - })) - - this.first('input-button') - .forEach(pass({ - disabled: () => inputField.get('empty') - })) + this.first('form').forEach(on('submit', e => { + e.preventDefault() + setTimeout(() => { + this.dispatchEvent(new CustomEvent('add-todo', { + bubbles: true, + detail: inputField.get('value') + })) + inputField.clear() + }, 0) + })) + + this.first('input-button').forEach(pass({ + disabled: () => inputField.get('empty') + })) } } TodoForm.define('todo-form') class TodoList extends UIElement { - connectedCallback() { - this.set('filter', 'all') // set initial filter + connectedCallback() { + this.set('filter', 'all') // set initial filter this.#updateList() // Event listener and attribute on own element - this.self - .map(on('click', e => { - if (e.target.localName === 'button') this.removeItem(e.target) - })) - .map(setAttribute('filter')) - - // Update count on each change - this.set('count', () => { - const tasks = this.get('tasks').map(el => el.signal('checked')) - const completed = tasks.filter(fn => fn()).length - const total = tasks.length - return { + this.self + .map(on('click', e => { + if (e.target.localName === 'button') this.removeItem(e.target) + })) + .forEach(setAttribute('filter')) + + // Update count on each change + this.set('count', () => { + const tasks = this.get('tasks').map(el => el.signal('checked')) + const completed = tasks.filter(fn => fn()).length + const total = tasks.length + return { active: total - completed, completed, total } - }) - } + }) + } - addItem = task => { - const template = this.querySelector('template').content.cloneNode(true) - template.querySelector('span').textContent = task - this.querySelector('ol').appendChild(template) - this.#updateList() - } + addItem = task => { + const template = this.querySelector('template').content.cloneNode(true) + template.querySelector('span').textContent = task + this.querySelector('ol').appendChild(template) + this.#updateList() + } - removeItem = element => { - element.closest('li').remove() - this.#updateList() - } + removeItem = element => { + element.closest('li').remove() + this.#updateList() + } - clearCompleted = () => { - this.get('tasks') - .filter(el => el.get('checked')) - .forEach(el => el.parentElement.remove()) - this.#updateList() - } + clearCompleted = () => { + this.get('tasks') + .filter(el => el.get('checked')) + .forEach(el => el.parentElement.remove()) + this.#updateList() + } #updateList() { - this.set('tasks', Array.from(this.querySelectorAll('input-checkbox'))) - } + this.set('tasks', Array.from(this.querySelectorAll('input-checkbox'))) + } } TodoList.define('todo-list') diff --git a/docs/best-practices-patterns.html b/docs/best-practices-patterns.html index 0faba7d..41a4a79 100644 --- a/docs/best-practices-patterns.html +++ b/docs/best-practices-patterns.html @@ -125,7 +125,7 @@
my-component {
+ my-component {
padding: 1rem;
/* Only divs that are immediate children of my-component will be styled */
@@ -146,7 +146,7 @@ Customize via Class or CSS Custom Properties
- parent-component {
+ parent-component {
--box-bg-color: red;
--box-text-color: white;
}
@@ -178,7 +178,7 @@ Passing State with pass()
- class ParentComponent extends UIElement {
+ class ParentComponent extends UIElement {
connectedCallback() {
this.set('parentColor', 'blue');
this.pass('parentColor', 'child-component', 'color');
@@ -194,7 +194,7 @@ Passing State with pass()
- class ChildComponent extends UIElement {
+ class ChildComponent extends UIElement {
connectedCallback() {
this.first('.box').map(setStyle('background-color', 'color'));
}
@@ -213,7 +213,7 @@ Dispatching Custom Events with emit()
- // In child component
+ // In child component
this.emit('change', { detail: { value: this.get('value') } });
@@ -223,7 +223,7 @@ Handling Custom Events in Parent Components
- // In parent component
+ // In parent component
this.first('child-component').map(on('change', (event) => {
console.log('Received change event:', event.detail.value);
// Handle state changes
diff --git a/docs/contributing-development.html b/docs/contributing-development.html
index f3f2189..e8bf409 100644
--- a/docs/contributing-development.html
+++ b/docs/contributing-development.html
@@ -135,16 +135,16 @@ 3. Setting Up the Development Environment
-
Clone the Repository: Clone the UIElement repository from GitHub:
-
git clone https://github.com/efflore/ui-element.git
+ git clone https://github.com/efflore/ui-element.git
-
Install Dependencies: Navigate to the project directory and install the necessary dependencies using npm:
-
cd ui-element
+ cd ui-element
npm install
-
Run the Development Server: Start the development server to work on the code and see live updates:
-
npm start
+ npm start
This will run the build process and serve the project locally.
@@ -155,15 +155,15 @@ 4. Testing and Building
-
Linting: Run linting to maintain code quality and formatting:
-
npm run lint
+ npm run lint
-
Unit Testing: Execute the test suite to ensure everything works correctly:
-
npm test
+ npm test
-
Building for Production: When you're ready to build the project for production, run:
-
npm run build
+ npm run build
The production build will be created in the `dist` directory.
@@ -173,7 +173,7 @@ 4. Testing and Building
5. Commit Message Guidelines
Use conventional commit messages for consistency and clarity. Examples include:
-
+
feat: add new feature to the component
fix: resolve issue with context updates
docs: update documentation for new API
diff --git a/docs/core-concepts.html b/docs/core-concepts.html
index dc26941..484cf6c 100644
--- a/docs/core-concepts.html
+++ b/docs/core-concepts.html
@@ -115,7 +115,7 @@ Extending Web Components
- class MyComponent extends UIElement {
+ class MyComponent extends UIElement {
/* component definition */
}
@@ -129,7 +129,7 @@ Defining Custom Elements
- MyComponent.define('my-component');
+ MyComponent.define('my-component');
@@ -142,7 +142,7 @@ Using the Custom Element
- <my-component>Content goes here</my-component>
+ <my-component>Content goes here</my-component>
@@ -176,7 +176,7 @@ Signals & State Management
- this.set('count', 0); // Create a signal
+ this.set('count', 0); // Create a signal
this.set('isEven', () => this.get('count') % 2 === 0); // Create a derived signal
@@ -195,7 +195,7 @@ Attributes & State Initialization
- static observedAttributes = ['count'];
+ static observedAttributes = ['count'];
@@ -205,7 +205,7 @@ Attributes & State Initialization
- static attributeMap = {
+ static attributeMap = {
count: asInteger,
}
@@ -217,7 +217,7 @@ Attributes & State Initialization
- static attributeMap = {
+ static attributeMap = {
date: value => value.map(v => new Date(v)),
}
@@ -234,7 +234,7 @@ Events & State Changes
- this.first('.increment').map(on('click', () => this.set('count', v => ++v)));
+ this.first('.increment').map(on('click', () => this.set('count', v => ++v)));
@@ -250,7 +250,7 @@ Effects & Reactive Bindings
- this.first('.count').map(setText('count'));
+ this.first('.count').map(setText('count'));
diff --git a/docs/detailed-walkthrough.html b/docs/detailed-walkthrough.html
index d162833..cb9d471 100644
--- a/docs/detailed-walkthrough.html
+++ b/docs/detailed-walkthrough.html
@@ -116,7 +116,7 @@ Observed Attributes
- import { UIElement, asInteger } from '@efflore/ui-element'
+ import { UIElement, asInteger } from '@efflore/ui-element'
class CounterComponent extends UIElement {
static observedAttributes = ['count']
@@ -131,7 +131,7 @@ Observed Attributes
- <counter-component count="5"></counter-component>
+ <counter-component count="5"></counter-component>
@@ -153,7 +153,7 @@ Defaults from DOM in Auto-Effects
- <hello-world>
+ <hello-world>
<p>Hello, <span class="name">World</span>!</p>
</hello-world>
@@ -164,7 +164,7 @@ Defaults from DOM in Auto-Effects
- this.first('.name').map(setText('name'))
+ this.first('.name').map(setText('name'))
@@ -182,7 +182,7 @@ Manually Set Signals
- // Setting a default state
+ // Setting a default state
this.set('count', 10)
// Setting a derived state
@@ -212,7 +212,7 @@ this.first(selector)
- this.first('.count').map(setText('count'))
+ this.first('.count').map(setText('count'))
@@ -224,7 +224,7 @@ this.all(selector)
- this.all('.item').map(toggleClass('active', 'isActive'))
+ this.all('.item').map(toggleClass('active', 'isActive'))
@@ -246,7 +246,7 @@ The Power of Array Methods
- // Example chaining event handler and effect
+ // Example chaining event handler and effect
this.first('button')
.map(on('click', () => this.set('clicked', true)))
.map(toggleClass('clicked'))
@@ -271,7 +271,7 @@ Event Handlers
- this.first('input').map(on('input', e => this.set('name', e.target.value)))
+ this.first('input').map(on('input', e => this.set('name', e.target.value)))
@@ -290,7 +290,7 @@ Resolved Promises
- fetch('/data')
+ fetch('/data')
.then(response => response.json())
.then(data => this.set('data', data))
diff --git a/docs/examples-recipes.html b/docs/examples-recipes.html
index e6db935..2b625d9 100644
--- a/docs/examples-recipes.html
+++ b/docs/examples-recipes.html
@@ -215,7 +215,7 @@ CodeBlock Example
code-block.html
html
- <code-block collapsed language="html" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
+ <code-block collapsed language="html" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="file">code-block.html</span>
<span class="language">html</span>
@@ -265,20 +265,10 @@ CodeBlock Example
LazyLoad Example
-
-
-
-
-
-
-
-
+
-
-
+ Loading...
+
+
@@ -378,30 +368,32 @@ TodoApp Example
-
- Well done, all done!
- tasks left
-
-
-
-
-
-
-
+
@@ -504,14 +496,14 @@ Configuring Breakpoints
For example, to set a small breakpoint at 40em and a medium breakpoint at 60em, use:
-
+
<media-context sm="40em" md="60em"></media-context>
Source Code
-
+
import { UIElement, maybe } from '@efflore/ui-element'
const VIEWPORT_XS = 'xs';
@@ -599,7 +591,7 @@ ThemedComponent Example
HTML
-
+
<media-context>
<themed-component>
This component changes its background based on the theme!
@@ -611,7 +603,7 @@ HTML
CSS
-
+
themed-component {
display: block;
padding: 20px;
@@ -632,7 +624,7 @@ CSS
JavaScript
-
+
import { UIElement, toggleClass } from '@efflore/ui-element';
class ThemedComponent extends UIElement {
@@ -673,7 +665,7 @@ AnimatedComponent Example
HTML
-
+
<media-context>
<animated-component>
<div class="animated-box">Box 1</div>
@@ -687,7 +679,7 @@ HTML
CSS
-
+
animated-component {
display: block;
padding: 20px;
@@ -730,7 +722,7 @@ CSS
JavaScript
-
+
import { UIElement, toggleClass } from '@efflore/ui-element';
class AnimatedComponent extends UIElement {
@@ -784,7 +776,7 @@ Responsive TabList Example
HTML
-
+
<media-context>
<tab-list>
<button class="tab-button">Tab 1</button>
@@ -811,7 +803,7 @@ HTML
CSS
-
+
tab-list {
display: flex;
flex-direction: column;
@@ -873,7 +865,7 @@ CSS
JavaScript
-
+
import { UIElement, on, pass, toggleClass } from '@efflore/ui-element';
// TabList Component
@@ -957,7 +949,7 @@ Responsive Image Gallery Example
HTML
-
+
<media-context>
<responsive-image-gallery>
<img src="image1.jpg" alt="Image 1">
@@ -973,7 +965,7 @@ HTML
CSS
-
+
responsive-image-gallery {
display: flex;
flex-wrap: wrap;
@@ -1011,7 +1003,7 @@ CSS
JavaScript
-
+
import { UIElement, toggleClass, effect } from '@efflore/ui-element';
import { MySlider } from './my-slider.js'; // Assume this is the existing MySlider component
diff --git a/docs/examples/accordion-panel.html b/docs/examples/accordion-panel.html
index 5e50701..ef699a4 100644
--- a/docs/examples/accordion-panel.html
+++ b/docs/examples/accordion-panel.html
@@ -14,7 +14,7 @@
accordion-panel.html
html
- <accordion-panel>
+ <accordion-panel>
<details open aria-disabled="true">
<summary class="visually-hidden">
<div class="summary">Tab 1</div>
@@ -54,7 +54,7 @@
accordion-panel.css
css
- accordion-panel {
+ accordion-panel {
display: block;
> details {
@@ -115,7 +115,7 @@
accordion-panel.js
js
- import { UIElement, on, setProperty, toggleAttribute } from '@efflore/ui-element'
+ import { UIElement, on, setProperty, toggleAttribute } from '@efflore/ui-element'
class AccordionPanel extends UIElement {
connectedCallback() {
diff --git a/docs/examples/code-block.html b/docs/examples/code-block.html
index 01dc5f7..fdb25d1 100644
--- a/docs/examples/code-block.html
+++ b/docs/examples/code-block.html
@@ -14,7 +14,7 @@
code-block.html
html
- <code-block collapsed language="html" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
+ <code-block collapsed language="html" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="file">code-block.html</span>
<span class="language">html</span>
@@ -51,7 +51,7 @@
code-block.css
css
- code-block {
+ code-block {
position: relative;
display: block;
margin: 0 0 var(--space-l);
@@ -154,7 +154,7 @@
code-block.js
js
- import { UIElement, asBoolean, on, effect, toggleAttribute } from '@efflore/ui-element'
+ import { UIElement, asBoolean, on, effect, toggleAttribute } from '@efflore/ui-element'
import Prism from 'prismjs'
class CodeBlock extends UIElement {
diff --git a/docs/examples/input-button.html b/docs/examples/input-button.html
index 9910585..346e145 100644
--- a/docs/examples/input-button.html
+++ b/docs/examples/input-button.html
@@ -14,7 +14,7 @@
input-button.html
html
- <input-button>
+ <input-button>
<button type="button">Button</button>
</input-button>
@@ -37,7 +37,7 @@
input-button.html.css
css
- input-button {
+ input-button {
display: inline-block;
flex: 0;
@@ -51,6 +51,7 @@
padding: 0 var(--space-s);
font-size: var(--font-size-s);
line-height: var(--line-height-s);
+ white-space: nowrap;
opacity: var(--opacity-dimmed);
transition: all var(--transition-shorter) var(--easing-inout);
@@ -154,7 +155,7 @@
input-button.js
js
- import { UIElement, asBoolean, setText, setProperty } from '@efflore/ui-element'
+ import { UIElement, asBoolean, setText, setProperty } from '@efflore/ui-element'
class InputButton extends UIElement {
static observedAttributes = ['disabled']
diff --git a/docs/examples/input-checkbox.html b/docs/examples/input-checkbox.html
index 19b48e0..dd6c396 100644
--- a/docs/examples/input-checkbox.html
+++ b/docs/examples/input-checkbox.html
@@ -14,7 +14,7 @@
input-checkbox.html
html
- <input-checkbox>
+ <input-checkbox>
<label>
<input type="checkbox">
<span >Checkbox</span>
@@ -43,7 +43,7 @@
input-checkbox.css
css
- input-checkbox {
+ input-checkbox {
flex-grow: 1;
& label {
@@ -119,7 +119,7 @@
input-checkbox.js
js
- import { UIElement, asBoolean, on, setProperty, toggleAttribute } from '@efflore/ui-element'
+ import { UIElement, asBoolean, on, setProperty, toggleAttribute } from '@efflore/ui-element'
class InputCheckbox extends UIElement {
static observedAttributes = ['checked']
diff --git a/docs/examples/input-field.html b/docs/examples/input-field.html
index 7af2c6f..b9546cb 100644
--- a/docs/examples/input-field.html
+++ b/docs/examples/input-field.html
@@ -14,7 +14,7 @@
input-field.html
html
- <input-field>
+ <input-field>
<label for="name-input">Name</label>
<div class="row">
<div class="group auto">
@@ -65,7 +65,7 @@
input-field.css
css
- input-field {
+ input-field {
width: 100%;
&[value="0"] input {
@@ -276,7 +276,7 @@
input-field.js
js
- import { UIElement, effect, maybe, on, setText, setProperty, setAttribute, toggleClass } from '@efflore/ui-element'
+ import { UIElement, effect, maybe, on, setText, setProperty, setAttribute, toggleClass } from '@efflore/ui-element'
const isNumber = num => typeof num === 'number'
const parseNumber = (v, int = false) => int ? parseInt(v, 10) : parseFloat(v)
diff --git a/docs/examples/input-radiogroup.html b/docs/examples/input-radiogroup.html
index 3667e82..5977f74 100644
--- a/docs/examples/input-radiogroup.html
+++ b/docs/examples/input-radiogroup.html
@@ -14,7 +14,7 @@
input-radiogroup.html
html
- <input-radiogroup value="other">
+ <input-radiogroup value="other">
<fieldset>
<legend>Gender</legend>
<label>
@@ -65,7 +65,7 @@
input-radiogroup.css
css
- input-radiogroup {
+ input-radiogroup {
display: inline-block;
> fieldset {
@@ -153,7 +153,7 @@
input-radiogroup.js
js
- import { UIElement, on, setAttribute, toggleClass } from '@efflore/ui-element'
+ import { UIElement, on, setAttribute, toggleClass } from '@efflore/ui-element'
export class InputRadiogroup extends UIElement {
static observedAttributes = ['value']
diff --git a/docs/examples/lazy-load.html b/docs/examples/lazy-load.html
index d25c2e4..66642d4 100644
--- a/docs/examples/lazy-load.html
+++ b/docs/examples/lazy-load.html
@@ -11,53 +11,13 @@
<lazy-load.html count="42">
- <p>
- Count: <span class="count"></span>
- Parity: <span class="parity"></span>
- </p>
- <button id="increment">+</button>
- <button id="decrement">−</button>
-</lazy-load.html>
-
-
-
-
-
-
-
-
-
-
- CSS
-
-
- lazy-load.html {
- display: flex;
- gap: 0.5rem;
- margin-block: 1rem;
-
- p {
- display: inline-block;
- margin: 0;
- flex: 1;
- }
-
- span {
- margin-right: 0.5rem;
- }
-
- button {
- padding: 0.25rem 0.5rem;
- flex: 0;
- }
-}
+ <lazy-load src="./examples/snippets/snippet.html">
+ <p class="loading">Loading...</p>
+ <p class="error"></p>
+</lazy-load>
@@ -71,26 +31,68 @@
import { UIElement, asInteger, on, setText } from '@efflore/ui-element'
+ import { UIElement, setText, setProperty, effect } from '@efflore/ui-element';
-class MyCounter extends UIElement {
- static observedAttributes = ['count']
+class LazyLoad extends UIElement {
+ static observedAttributes = ['src']
static attributeMap = {
- count: asInteger
+ src: v => v.map(src => {
+ let url = ''
+ try {
+ url = new URL(src, location.href) // ensure 'src' attribute is a valid URL
+ if (url.origin !== location.origin) { // sanity check for cross-origin URLs
+ throw new TypeError('Invalid URL origin')
+ }
+ } catch (error) {
+ console.error(error, url)
+ url = ''
+ }
+ return url.toString()
+ })
}
connectedCallback() {
- this.set('parity', () => this.get('count') % 2 ? 'odd' : 'even')
- this.first('.increment').map(on('click', () => this.set('count', v => ++v)))
- this.first('.decrement').map(on('click', () => this.set('count', v => --v)))
- this.first('.count').map(setText('count'))
- this.first('.parity').map(setText('parity'))
+
+ // Show / hide loading message
+ this.first('.loading')
+ .forEach(setProperty('ariaHidden', () => !!this.get('error')))
+
+ // Set and show / hide error message
+ this.first('.error')
+ .map(setText('error'))
+ .forEach(setProperty('ariaHidden', () => !this.get('error')))
+
+ // Load content from provided URL
+ effect(enqueue => {
+ const src = this.get('src')
+ if (!src) return // silently fail if no valid URL is provided
+ fetch(src)
+ .then(async response => {
+ if (response.ok) {
+ const content = await response.text()
+ enqueue(this.root, 'h', el => () => {
+ // UNSAFE!, use only trusted sources in 'src' attribute
+ el.innerHTML = content
+ el.querySelectorAll('script').forEach(script => {
+ const newScript = document.createElement('script')
+ newScript.appendChild(document.createTextNode(script.textContent))
+ el.appendChild(newScript)
+ script.remove()
+ })
+ })
+ this.set('error', '')
+ } else {
+ this.set('error', response.status + ':'+ response.statusText)
+ }
+ })
+ .catch(error => this.set('error', error))
+ })
}
}
-MyCounter.define('lazy-load.html')
+LazyLoad.define('lazy-load')
diff --git a/docs/examples/media-provider.html b/docs/examples/media-provider.html
index 4feceab..3ba02fe 100644
--- a/docs/examples/media-provider.html
+++ b/docs/examples/media-provider.html
@@ -11,10 +11,10 @@
<media-provider.html count="42">
+ <media-provider count="42">
<p>
Count: <span class="count"></span>
Parity: <span class="parity"></span>
@@ -35,10 +35,10 @@
media-provider.html {
+ media-provider {
display: flex;
gap: 0.5rem;
margin-block: 1rem;
@@ -71,10 +71,10 @@
import { UIElement, asInteger, on, setText } from '@efflore/ui-element'
+ import { UIElement, asInteger, on, setText } from '@efflore/ui-element'
class MyCounter extends UIElement {
static observedAttributes = ['count']
@@ -90,7 +90,7 @@
this.first('.parity').map(setText('parity'))
}
}
-MyCounter.define('media-provider.html')
+MyCounter.define('media-provider')
diff --git a/docs/examples/my-counter.html b/docs/examples/my-counter.html
index dbe1930..214c15b 100644
--- a/docs/examples/my-counter.html
+++ b/docs/examples/my-counter.html
@@ -14,7 +14,7 @@
my-counter.html
html
- <my-counter count="42">
+ <my-counter count="42">
<p>
Count: <span class="count"></span>
Parity: <span class="parity"></span>
@@ -38,7 +38,7 @@
my-counter.css
css
- my-counter {
+ my-counter {
display: flex;
gap: 0.5rem;
margin-block: 1rem;
@@ -74,7 +74,7 @@
my-counter.js
js
- import { UIElement, asInteger, on, setText } from '@efflore/ui-element'
+ import { UIElement, asInteger, on, setText } from '@efflore/ui-element'
class MyCounter extends UIElement {
static observedAttributes = ['count']
diff --git a/docs/examples/my-slider.html b/docs/examples/my-slider.html
index 8586ff4..5ce8127 100644
--- a/docs/examples/my-slider.html
+++ b/docs/examples/my-slider.html
@@ -14,7 +14,7 @@
my-slider.html
html
- <my-slider>
+ <my-slider>
<button type="button" class="prev" aria-label="Previous">‹</button>
<div class="slides">
<div class="slide active">Slide 1</div>
@@ -44,7 +44,7 @@
my-slider.css
css
- my-slider {
+ my-slider {
display: flex;
overflow: hidden;
aspect-ratio: 16 / 9;
@@ -131,7 +131,7 @@
my-slider.js
js
- import { UIElement, on, toggleClass } from '@efflore/ui-element'
+ import { UIElement, on, toggleClass } from '@efflore/ui-element'
class MySlider extends UIElement {
connectedCallback() {
diff --git a/docs/examples/snippets/snippet.html b/docs/examples/snippets/snippet.html
new file mode 100644
index 0000000..6558ce8
--- /dev/null
+++ b/docs/examples/snippets/snippet.html
@@ -0,0 +1,53 @@
+Lazy Loaded
+I come from the server and I bring an additional web component:
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/examples/tab-list.html b/docs/examples/tab-list.html
index 632a9e9..6a3e35d 100644
--- a/docs/examples/tab-list.html
+++ b/docs/examples/tab-list.html
@@ -14,7 +14,7 @@
tab-list.html
html
- <tab-list>
+ <tab-list>
<menu>
<li><button type="button" aria-pressed="true">Tab 1</button></li>
<li><button type="button">Tab 2</button></li>
@@ -38,7 +38,7 @@
tab-list.css
css
- tab-list {
+ tab-list {
display: block;
margin-bottom: var(--space-l);
@@ -89,7 +89,7 @@
tab-list.js
js
- import { UIElement, on, pass, setProperty, toggleAttribute } from '@efflore/ui-element'
+ import { UIElement, on, pass, setProperty, toggleAttribute } from '@efflore/ui-element'
class TabList extends UIElement {
static observedAttributes = ['accordion']
diff --git a/docs/examples/todo-app.html b/docs/examples/todo-app.html
index f9d3928..f88f68d 100644
--- a/docs/examples/todo-app.html
+++ b/docs/examples/todo-app.html
@@ -11,17 +11,68 @@
<todo-app.html count="42">
- <p>
- Count: <span class="count"></span>
- Parity: <span class="parity"></span>
- </p>
- <button id="increment">+</button>
- <button id="decrement">−</button>
-</todo-app.html>
+ <<todo-app>
+ <todo-form>
+ <form action="#">
+ <input-field>
+ <label for="add-todo">What needs to be done?</label>
+ <div class="row">
+ <div class="group auto">
+ <input id="add-todo" type="text" value="" required>
+ </div>
+ </div>
+ </input-field>
+ <input-button class="submit">
+ <button type="submit" class="primary" disabled>Add Todo</button>
+ </input-button>
+ </form>
+ </todo-form>
+ <todo-list filter="all">
+ <ol></ol>
+ <template>
+ <li>
+ <input-checkbox class="todo">
+ <label>
+ <input type="checkbox" class="visually-hidden" />
+ <span></span>
+ </label>
+ </input-checkbox>
+ <input-button>
+ <button type="button">Delete</button>
+ </input-button>
+ </li>
+ </template>
+ </todo-list>
+ <footer>
+ <todo-count>
+ <p class="all-done">Well done, all done!</p>
+ <p class="remaining"><span></span> tasks left</p>
+ </todo-count>
+ <input-radiogroup value="all" class="split-button">
+ <fieldset>
+ <legend class="visually-hidden">Filter</legend>
+ <label class="selected">
+ <input type="radio" class="visually-hidden" name="filter" value="all" checked>
+ <span>All</span>
+ </label>
+ <label>
+ <input type="radio" class="visually-hidden" name="filter" value="active">
+ <span>Active</span>
+ </label>
+ <label>
+ <input type="radio" class="visually-hidden" name="filter" value="completed">
+ <span>Completed</span>
+ </label>
+ </fieldset>
+ </input-radiogroup>
+ <input-button class="clear-completed">
+ <button type="button">Clear Completed</button>
+ </input-button>
+ </footer>
+</todo-app>
@@ -35,27 +86,33 @@
todo-app.html {
+ todo-app {
display: flex;
- gap: 0.5rem;
- margin-block: 1rem;
+ flex-direction: column;
+ gap: var(--space-l);
- p {
- display: inline-block;
+ & footer {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ align-items: center;
+ gap: var(--space-m);
margin: 0;
- flex: 1;
- }
-
- span {
- margin-right: 0.5rem;
+
+ .clear-completed {
+ justify-self: end;
+ }
}
+}
- button {
- padding: 0.25rem 0.5rem;
- flex: 0;
+todo-count {
+ justify-self: start;
+
+ & p {
+ font-size: var(--font-size-s);
+ margin: 0;
}
}
@@ -71,26 +128,48 @@
import { UIElement, asInteger, on, setText } from '@efflore/ui-element'
+ import { UIElement, on, pass, setText, setProperty } from '@efflore/ui-element'
-class MyCounter extends UIElement {
- static observedAttributes = ['count']
- static attributeMap = {
- count: asInteger
+class TodoApp extends UIElement {
+ connectedCallback() {
+ const [todoList, todoFilter] = ['todo-list', 'input-radiogroup']
+ .map(selector => this.querySelector(selector))
+
+ // Event listener on own element
+ this.self.forEach(on('add-todo', e => todoList?.addItem(e.detail)))
+
+ // Coordinate todo-count
+ this.first('todo-count').forEach(pass({
+ active: () => todoList?.get('count').active
+ }))
+
+ // Coordinate todo-list
+ this.first('todo-list').forEach(pass({
+ filter: () => todoFilter?.get('value')
+ }))
+
+ // Coordinate .clear-completed button
+ this.first('.clear-completed')
+ .map(on('click', () => todoList?.clearCompleted()))
+ .forEach(pass({ disabled: () => !todoList?.get('count').completed }))
}
+}
+TodoApp.define('todo-app')
+class TodoCount extends UIElement {
connectedCallback() {
- this.set('parity', () => this.get('count') % 2 ? 'odd' : 'even')
- this.first('.increment').map(on('click', () => this.set('count', v => ++v)))
- this.first('.decrement').map(on('click', () => this.set('count', v => --v)))
- this.first('.count').map(setText('count'))
- this.first('.parity').map(setText('parity'))
+ this.set('active', 0, false)
+ this.first('.count').forEach(setText('active'))
+ this.first('.singular').forEach(setProperty('ariaHidden', () => this.get('active') > 1))
+ this.first('.plural').forEach(setProperty('ariaHidden', () => this.get('active') === 1))
+ this.first('.remaining').forEach(setProperty('ariaHidden', () => !this.get('active')))
+ this.first('.all-done').forEach(setProperty('ariaHidden', () => !!this.get('active')))
}
}
-MyCounter.define('todo-app.html')
+TodoCount.define('todo-count')
diff --git a/docs/examples/todo-form.html b/docs/examples/todo-form.html
index 99cd774..8bd0534 100644
--- a/docs/examples/todo-form.html
+++ b/docs/examples/todo-form.html
@@ -11,17 +11,24 @@
<todo-form.html count="42">
- <p>
- Count: <span class="count"></span>
- Parity: <span class="parity"></span>
- </p>
- <button id="increment">+</button>
- <button id="decrement">−</button>
-</todo-form.html>
+ <todo-form>
+ <form action="#">
+ <input-field>
+ <label for="add-todo">What needs to be done?</label>
+ <div class="row">
+ <div class="group auto">
+ <input id="add-todo" type="text" value="" required>
+ </div>
+ </div>
+ </input-field>
+ <input-button class="submit">
+ <button type="submit" class="primary" disabled>Add Todo</button>
+ </input-button>
+ </form>
+</todo-form>
@@ -35,10 +42,10 @@
todo-form.html {
+ todo-form.html {
display: flex;
gap: 0.5rem;
margin-block: 1rem;
@@ -71,26 +78,32 @@
import { UIElement, asInteger, on, setText } from '@efflore/ui-element'
-
-class MyCounter extends UIElement {
- static observedAttributes = ['count']
- static attributeMap = {
- count: asInteger
- }
+ import { UIElement, on, pass } from '@efflore/ui-element'
+class TodoForm extends UIElement {
connectedCallback() {
- this.set('parity', () => this.get('count') % 2 ? 'odd' : 'even')
- this.first('.increment').map(on('click', () => this.set('count', v => ++v)))
- this.first('.decrement').map(on('click', () => this.set('count', v => --v)))
- this.first('.count').map(setText('count'))
- this.first('.parity').map(setText('parity'))
+ const inputField = this.querySelector('input-field')
+
+ this.first('form').forEach(on('submit', e => {
+ e.preventDefault()
+ setTimeout(() => {
+ this.dispatchEvent(new CustomEvent('add-todo', {
+ bubbles: true,
+ detail: inputField.get('value')
+ }))
+ inputField.clear()
+ }, 0)
+ }))
+
+ this.first('input-button').forEach(pass({
+ disabled: () => inputField.get('empty')
+ }))
}
}
-MyCounter.define('todo-form.html')
+TodoForm.define('todo-form')
diff --git a/docs/examples/todo-list.html b/docs/examples/todo-list.html
index 159c5d0..9b24512 100644
--- a/docs/examples/todo-list.html
+++ b/docs/examples/todo-list.html
@@ -11,17 +11,25 @@
<my-counter count="42">
- <p>
- Count: <span class="count"></span>
- Parity: <span class="parity"></span>
- </p>
- <button id="increment">+</button>
- <button id="decrement">−</button>
-</my-counter>
+ <todo-list filter="all">
+ <ol></ol>
+ <template>
+ <li>
+ <input-checkbox class="todo">
+ <label>
+ <input type="checkbox" class="visually-hidden" />
+ <span></span>
+ </label>
+ </input-checkbox>
+ <input-button>
+ <button type="button">Delete</button>
+ </input-button>
+ </li>
+ </template>
+</todo-list>
@@ -35,27 +43,39 @@
my-counter {
- display: flex;
- gap: 0.5rem;
- margin-block: 1rem;
+ todo-list {
+ display: block;
- p {
- display: inline-block;
+ & ol {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-m);
+ list-style: none;
margin: 0;
- flex: 1;
+ padding: 0;
}
- span {
- margin-right: 0.5rem;
+ & li {
+ display: flex;
+ justify-content: space-between;
+ gap: var(--space-m);
+ margin: 0;
+ padding: 0;
+ }
+
+ &[filter="completed"] {
+ li:not(:has([checked])) {
+ display: none;
+ }
}
- button {
- padding: 0.25rem 0.5rem;
- flex: 0;
+ &[filter="active"] {
+ li:has([checked]) {
+ display: none;
+ }
}
}
@@ -71,26 +91,61 @@
import { UIElement, asInteger, on, setText } from '@efflore/ui-element'
+ import { UIElement, on, setAttribute } from '@efflore/ui-element'
-class MyCounter extends UIElement {
- static observedAttributes = ['count']
- static attributeMap = {
- count: asInteger
+class TodoList extends UIElement {
+ connectedCallback() {
+ this.set('filter', 'all') // set initial filter
+ this.#updateList()
+
+ // Event listener and attribute on own element
+ this.self
+ .map(on('click', e => {
+ if (e.target.localName === 'button') this.removeItem(e.target)
+ }))
+ .forEach(setAttribute('filter'))
+
+ // Update count on each change
+ this.set('count', () => {
+ const tasks = this.get('tasks').map(el => el.signal('checked'))
+ const completed = tasks.filter(fn => fn()).length
+ const total = tasks.length
+ return {
+ active: total - completed,
+ completed,
+ total
+ }
+ })
}
- connectedCallback() {
- this.set('parity', () => this.get('count') % 2 ? 'odd' : 'even')
- this.first('.increment').map(on('click', () => this.set('count', v => ++v)))
- this.first('.decrement').map(on('click', () => this.set('count', v => --v)))
- this.first('.count').map(setText('count'))
- this.first('.parity').map(setText('parity'))
+ addItem = task => {
+ const template = this.querySelector('template').content.cloneNode(true)
+ template.querySelector('span').textContent = task
+ this.querySelector('ol').appendChild(template)
+ this.#updateList()
}
+
+ removeItem = element => {
+ element.closest('li').remove()
+ this.#updateList()
+ }
+
+ clearCompleted = () => {
+ this.get('tasks')
+ .filter(el => el.get('checked'))
+ .forEach(el => el.parentElement.remove())
+ this.#updateList()
+ }
+
+ #updateList() {
+ this.set('tasks', Array.from(this.querySelectorAll('input-checkbox')))
+ }
+
}
-MyCounter.define('my-counter')
+TodoList.define('todo-list')
diff --git a/docs/index.html b/docs/index.html
index 130a785..99423fa 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -148,7 +148,7 @@ How UIElement Works
UIElement relies on signals—small pieces of reactive state that notify your components when changes occur. This allows for efficient updates to HTML content, handling reactivity only when necessary.
Signals & Auto-Effects: Signals automatically trigger updates to the DOM when they change.
- this.set('count', 0) // Define a signal for 'count'
+ this.set('count', 0) // Define a signal for 'count'
this.first('.count').forEach(setText('count')) // Automatically update content when 'count' changes
diff --git a/docs/installation-setup.html b/docs/installation-setup.html
index e648e5f..e841bc4 100644
--- a/docs/installation-setup.html
+++ b/docs/installation-setup.html
@@ -113,7 +113,7 @@ Using a CDN
- <script src="https://cdn.jsdelivr.net/npm/@efflore/ui-element@latest/index.min.js"></script>
+ <script src="https://cdn.jsdelivr.net/npm/@efflore/ui-element@latest/index.min.js"></script>
@@ -123,7 +123,7 @@ Using a CDN
- <script type="module">
+ <script type="module">
import { UIElement } from 'https://cdn.jsdelivr.net/npm/@efflore/ui-element@latest/index.min.js'
// Your code here
@@ -147,7 +147,7 @@ Installing with NPM
- npm install @efflore/ui-element
+ npm install @efflore/ui-element
@@ -157,7 +157,7 @@ Installing with NPM
- import { UIElement } from '@efflore/ui-element'
+ import { UIElement } from '@efflore/ui-element'
@@ -172,7 +172,7 @@ Creating Your First Component
- <script type="module">
+ <script type="module">
import { UIElement, setText, on } from 'https://cdn.jsdelivr.net/npm/@efflore/ui-element@latest/index.min.js'
class HelloWorld extends UIElement {
@@ -196,7 +196,7 @@ Creating Your First Component
- <hello-world>
+ <hello-world>
<label>Your name<br>
<input type="text">
</label>
-
-
-
-
-
-
-
-
-
-
-
-
-
-