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 @@

Scope Styles via Custom Element Name

css

-
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

css

-
parent-component {
+					
parent-component {
 	--box-bg-color: red;
 	--box-text-color: white;
 }
@@ -178,7 +178,7 @@ 

Passing State with pass()

js

-
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()

js

-
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()

js

-
// In child component
+					
// In child component
 this.emit('change', { detail: { value: this.get('value') } });
@@ -223,7 +223,7 @@

Handling Custom Events in Parent Components

js

-
// 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

  1. 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
  2. Install Dependencies: Navigate to the project directory and install the necessary dependencies using npm: -
    cd ui-element
    +					
    cd ui-element
     			  npm install
  3. 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

js

-
class MyComponent extends UIElement {
+					
class MyComponent extends UIElement {
 	/* component definition */
 }
@@ -129,7 +129,7 @@

Defining Custom Elements

js

-
MyComponent.define('my-component');
+
MyComponent.define('my-component');
@@ -142,7 +142,7 @@

Using the Custom Element

html

-
<my-component>Content goes here</my-component>
+
<my-component>Content goes here</my-component>
@@ -176,7 +176,7 @@

Signals & State Management

js

-
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

js

-
static observedAttributes = ['count'];
+
static observedAttributes = ['count'];
@@ -205,7 +205,7 @@

Attributes & State Initialization

js

-
static attributeMap = {
+					
static attributeMap = {
 	count: asInteger,
 }
@@ -217,7 +217,7 @@

Attributes & State Initialization

js

-
static attributeMap = {
+				
static attributeMap = {
 	date: value => value.map(v => new Date(v)),
 }
@@ -234,7 +234,7 @@

Events & State Changes

js

-
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

js

-
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

js

-
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

html

-
<counter-component count="5"></counter-component>
+
<counter-component count="5"></counter-component>
@@ -153,7 +153,7 @@

Defaults from DOM in Auto-Effects

html

-
<hello-world>
+					
<hello-world>
 	<p>Hello, <span class="name">World</span>!</p>
 </hello-world>
@@ -164,7 +164,7 @@

Defaults from DOM in Auto-Effects

js

-
this.first('.name').map(setText('name'))
+
this.first('.name').map(setText('name'))
@@ -182,7 +182,7 @@

Manually Set Signals

js

-
// Setting a default state
+					
// Setting a default state
 this.set('count', 10)
 
 // Setting a derived state
@@ -212,7 +212,7 @@ 

this.first(selector)

js

-
this.first('.count').map(setText('count'))
+
this.first('.count').map(setText('count'))
@@ -224,7 +224,7 @@

this.all(selector)

js

-
this.all('.item').map(toggleClass('active', 'isActive'))
+
this.all('.item').map(toggleClass('active', 'isActive'))
@@ -246,7 +246,7 @@

The Power of Array Methods

js

-
// 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

js

-
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

js

-
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

- - -
-
Slide 1
-
Slide 2
-
Slide 3
-
- -
- - - -
-
+ +

Loading...

+

+
@@ -378,30 +368,32 @@

TodoApp Example

- -

Well done, all done!

-

tasks left

-
- -
- Filter - - - -
-
- - - +
+ +

Well done, all done!

+

tasks left

+
+ +
+ Filter + + + +
+
+ + + +
@@ -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.html + lazy-load.html html

-
<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.css - 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 @@

- lazy-load.html.js + lazy-load.js js

-
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.html + media-provider.html html

-
<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.css + media-provider.css css

-
media-provider.html {
+				
media-provider {
 	display: flex;
 	gap: 0.5rem;
 	margin-block: 1rem;
@@ -71,10 +71,10 @@
 			
 			
 				

- media-provider.html.js + media-provider.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']
@@ -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.html + todo-app.html html

-
<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.css + todo-app.css css

-
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 @@

- todo-app.html.js + todo-app.js js

-
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.html + todo-form.html html

-
<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.css + todo-form.css css

-
todo-form.html {
+				
todo-form.html {
 	display: flex;
 	gap: 0.5rem;
 	margin-block: 1rem;
@@ -71,26 +78,32 @@
 			
 			
 				

- todo-form.html.js + todo-form.js js

-
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.html + todo-list.html html

-
<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.css + todo-list.css css

-
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 @@

- my-counter.js + todo-list.js js

-
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

html

-
<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

html

-
<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

bash

-
npm install @efflore/ui-element
+
npm install @efflore/ui-element
@@ -157,7 +157,7 @@

Installing with NPM

js

-
import { UIElement } from '@efflore/ui-element'
+
import { UIElement } from '@efflore/ui-element'
@@ -172,7 +172,7 @@

Creating Your First Component

js

-
<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

html

-
<hello-world>
+					
<hello-world>
 	<label>Your name<br>
 		<input type="text">
 	</label>