Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/efflore/ui-element into main
Browse files Browse the repository at this point in the history
  • Loading branch information
estherbrunner committed Jul 15, 2024
2 parents f9c559c + 79f49c6 commit 5477b28
Show file tree
Hide file tree
Showing 25 changed files with 699 additions and 239 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ In the `connectedCallback()` you setup references to inner elements, add event l

`UIElement` is fast. In fact, faster than any JavaScript framework. Only direct fine-grained DOM updates in vanilla JavaScript can beat its performance. But then, you have no loose coupling of components and need to parse attributes and track changes yourself. This tends to get tedious and messy rather quickly. `UIElement` provides a structured way to keep your components simple, consistent and self-contained.

`UIElement` is tiny. 914 bytes gzipped over the wire. And it has zero dependiences. If you want to understand how it works, you have to study the source code of [one single file](./index.js).
`UIElement` is tiny. 930 bytes gzipped over the wire. And it has zero dependiences. If you want to understand how it works, you have to study the source code of [one single file](./index.js).

That's all.

Expand Down Expand Up @@ -203,7 +203,7 @@ It consists of three functions:
- `derive()` returns a getter function for the current value of the derived computation
- `effect()` accepts a callback function to be exectuted when used signals change

Cause & Effect is possibly the simplest way to turn JavaScript into a reactive language – with just 312 bytes gezipped code. Unlike the [TC39 Signals Proposal](https://github.com/tc39/proposal-signals), Cause & Effect uses a much simpler approach, effectively just decorator functions around signal getters and setters. All work is done synchronously and eagerly. As long as your computed functions are pure and DOM side effects are kept to a minimum, this should pose no issues and is even faster than doing all the checks and memoization in the more sophisticated push-then-pull approach of the Signals Proposal.
Cause & Effect is possibly the simplest way to turn JavaScript into a reactive language – with just 340 bytes gezipped code. Unlike the [TC39 Signals Proposal](https://github.com/tc39/proposal-signals), Cause & Effect uses a much simpler approach, effectively just decorator functions around signal getters and setters. All work is done synchronously and eagerly. As long as your computed functions are pure and DOM side effects are kept to a minimum, this should pose no issues and is even faster than doing all the checks and memoization in the more sophisticated push-then-pull approach of the Signals Proposal.

If you however want to use side-effects or expensive work in computed function or updating / rendering in the DOM in effects takes longer than an animation frame, you might encounter glitches. If that's what you are doing, you are better off with a mature, full-fledged JavaScript framework.

Expand Down
19 changes: 14 additions & 5 deletions cause-effect.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const isState = (value) => isFunction(value) && isFunction(value.set);
* Define a reactive state
*
* @since 0.1.0
* @param {unknown} value - initial value of the state; may be a function for derived state
* @param {any} value - initial value of the state; may be a function for derived state
* @returns {UIState} getter function for the current value with a `set` method to update the value
*/
const cause = (value) => {
Expand All @@ -46,10 +46,19 @@ const cause = (value) => {
* Create a derived state from an existing state
*
* @since 0.1.0
* @param {() => unknown} fn - existing state to derive from
* @returns {() => unknown} derived state
* @param {() => any} fn - existing state to derive from
* @returns {() => any} derived state
*/
const derive = (fn) => fn;
const derive = (fn) => {
const computed = () => {
const prev = active;
active = computed;
const value = fn();
active = prev;
return value;
};
return computed;
};
/**
* Define what happens when a reactive state changes
*
Expand All @@ -66,7 +75,7 @@ const effect = (fn) => {
targets.get(element).add(domFn);
});
active = prev;
queueMicrotask(() => {
(targets.size || cleanup) && queueMicrotask(() => {
for (const domFns of targets.values()) {
for (const domFn of domFns)
domFn();
Expand Down
2 changes: 1 addition & 1 deletion cause-effect.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 10 additions & 5 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// @ts-nocheck
import globals from "globals";
import pluginJs from "@eslint/js";

import globals from 'globals';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default [
{languageOptions: { globals: globals.browser }},
pluginJs.configs.recommended,
{
languageOptions: {
globals: globals.browser
},
},
eslint.configs.recommended,
...tseslint.configs.recommended,
];
29 changes: 18 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const isState = (value) => isFunction(value) && isFunction(value.set);
* Define a reactive state
*
* @since 0.1.0
* @param {unknown} value - initial value of the state; may be a function for derived state
* @param {any} value - initial value of the state; may be a function for derived state
* @returns {UIState} getter function for the current value with a `set` method to update the value
*/
const cause = (value) => {
Expand Down Expand Up @@ -58,7 +58,7 @@ const effect = (fn) => {
targets.get(element).add(domFn);
});
active = prev;
queueMicrotask(() => {
(targets.size || cleanup) && queueMicrotask(() => {
for (const domFns of targets.values()) {
for (const domFn of domFns)
domFn();
Expand Down Expand Up @@ -105,13 +105,24 @@ class ContextRequestEvent extends Event {
}
}

/* === Internal function === */
/**
* Parse a attribute or context mapping value into a key-value pair
*
* @param {[PropertyKey, UIAttributeParser | UIContextParser] | UIAttributeParser | UIContextParser} value
* @param {PropertyKey} defaultKey
* @returns {[PropertyKey, UIAttributeParser | UIContextParser]}
*/
const getArrayMapping = (value, defaultKey) => {
return Array.isArray(value) ? value : [defaultKey, isFunction(value) ? value : (v) => v];
};
/* === Default export === */
/**
* Base class for reactive custom elements
*
* @class UIElement
* @extends HTMLElement
* @type {UIElement}
* @type {IUIElement}
*/
class UIElement extends HTMLElement {
/**
Expand Down Expand Up @@ -154,11 +165,9 @@ class UIElement extends HTMLElement {
attributeChangedCallback(name, old, value) {
if (value !== old) {
const input = this.attributeMap[name];
const [key, parser] = Array.isArray(input)
? input :
[name, input];
this.set(key, isFunction(parser)
? parser(value, this, old)
const [key, fn] = getArrayMapping(input, name);
this.set(key, isFunction(fn)
? fn(value, this, old)
: value);
}
}
Expand All @@ -180,9 +189,7 @@ class UIElement extends HTMLElement {
proto.consumedContexts?.forEach((context) => {
const event = new ContextRequestEvent(context, (value) => {
const input = this.contextMap[context];
const [key, fn] = Array.isArray(input)
? input
: [context, input];
const [key, fn] = getArrayMapping(input, context);
this.#states.set(key || context, isFunction(fn)
? fn(value, this)
: value);
Expand Down
2 changes: 1 addition & 1 deletion index.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 5477b28

Please sign in to comment.