1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-04-02 04:05:52 +02:00

Compare commits

...

4 Commits

Author SHA1 Message Date
330f702409
🔤 examples+best paractises 2025-03-17 12:53:50 +01:00
2f57f115a0
v0.9.3 2025-03-17 11:11:48 +01:00
d1b017c1a1
🐛 __dde_reactive 2025-03-17 10:39:00 +01:00
992370b4e3
Replaces defined with name/host 2025-03-17 10:19:53 +01:00
19 changed files with 96 additions and 177 deletions

View File

@ -41,19 +41,6 @@ function observedAttributes(instance, observedAttribute2) {
function kebabToCamel(name) { function kebabToCamel(name) {
return name.replace(/-./g, (x) => x[1].toUpperCase()); return name.replace(/-./g, (x) => x[1].toUpperCase());
} }
var Defined = class extends Error {
constructor() {
super();
const [curr, ...rest] = this.stack.split("\n");
const curr_file = curr.slice(curr.indexOf("@"), curr.indexOf(".js:") + 4);
const curr_lib = curr_file.includes("src/helpers.js") ? "src/" : curr_file;
this.stack = rest.find((l) => !l.includes(curr_lib)) || curr;
}
get compact() {
const { stack } = this;
return stack.slice(0, stack.indexOf("@") + 1) + "\u2026" + stack.slice(stack.lastIndexOf("/"));
}
};
// src/dom-lib/common.js // src/dom-lib/common.js
var enviroment = { var enviroment = {
@ -303,7 +290,7 @@ var store_abort = /* @__PURE__ */ new WeakMap();
var scope = { var scope = {
/** /**
* Gets the current scope * Gets the current scope
* @returns {Object} Current scope context * @returns {typeof scopes[number]} Current scope context
*/ */
get current() { get current() {
return scopes[scopes.length - 1]; return scopes[scopes.length - 1];
@ -663,9 +650,9 @@ function memo(key, generator) {
memo.isScope = function(obj) { memo.isScope = function(obj) {
return obj[memoMark]; return obj[memoMark];
}; };
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) { memo.scope = function memoScopeCreate(fun, { signal: signal2, onlyLast } = {}) {
let cache = oCreate(); let cache = oCreate();
function memoScope2(...args) { function memoScope(...args) {
if (signal2 && signal2.aborted) if (signal2 && signal2.aborted)
return fun.apply(this, args); return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate(); let cache_local = onlyLast ? cache : oCreate();
@ -680,10 +667,10 @@ memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) {
cache = cache_local; cache = cache_local;
return out; return out;
} }
memoScope2[memoMark] = true; memoScope[memoMark] = true;
memoScope2.clear = () => cache = oCreate(); memoScope.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope2.clear); if (signal2) signal2.addEventListener("abort", memoScope.clear);
return memoScope2; return memoScope;
}; };
// src/signals-lib/helpers.js // src/signals-lib/helpers.js
@ -800,17 +787,17 @@ signal.clear = function(...signals2) {
}; };
var key_reactive = "__dde_reactive"; var key_reactive = "__dde_reactive";
signal.el = function(s, map) { signal.el = function(s, map) {
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true }); const mapScoped = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true); const { current } = scope, { scope: sc } = current;
const mark_start = createElement.mark({ type: "reactive", component: sc && sc.name || "" }, true);
const mark_end = mark_start.end; const mark_end = mark_start.end;
const out = enviroment.D.createDocumentFragment(); const out = enviroment.D.createDocumentFragment();
out.append(mark_start, mark_end); out.append(mark_start, mark_end);
const { current } = scope;
const reRenderReactiveElement = (v) => { const reRenderReactiveElement = (v) => {
if (!mark_start.parentNode || !mark_end.parentNode) if (!mark_start.parentNode || !mark_end.parentNode)
return removeSignalListener(s, reRenderReactiveElement); return removeSignalListener(s, reRenderReactiveElement);
scope.push(current); scope.push(current);
let els = map(v); let els = mapScoped(v);
scope.pop(); scope.pop();
if (!Array.isArray(els)) if (!Array.isArray(els))
els = [els]; els = [els];
@ -830,7 +817,7 @@ signal.el = function(s, map) {
current.host(on.disconnected( current.host(on.disconnected(
() => ( () => (
/*! Clears cached elements for reactive element `S.el` */ /*! Clears cached elements for reactive element `S.el` */
map.clear() mapScoped.clear()
) )
)); ));
return out; return out;
@ -902,9 +889,8 @@ var signals_config = {
function removeSignalsFromElements(s, listener, ...notes) { function removeSignalsFromElements(s, listener, ...notes) {
const { current } = scope; const { current } = scope;
current.host(function(element) { current.host(function(element) {
if (element[key_reactive]) if (!element[key_reactive]) element[key_reactive] = [];
return element[key_reactive].push([[s, listener], ...notes]); element[key_reactive].push([[s, listener], ...notes]);
element[key_reactive] = [];
if (current.prevent) return; if (current.prevent) return;
on.disconnected( on.disconnected(
() => ( () => (
@ -949,7 +935,6 @@ function toSignal(s, value, actions, readonly = false) {
onclear, onclear,
host, host,
listeners: /* @__PURE__ */ new Set(), listeners: /* @__PURE__ */ new Set(),
defined: new Defined().stack,
readonly readonly
}), }),
enumerable: false, enumerable: false,

File diff suppressed because one or more lines are too long

14
dist/esm.js vendored
View File

@ -274,7 +274,7 @@ var store_abort = /* @__PURE__ */ new WeakMap();
var scope = { var scope = {
/** /**
* Gets the current scope * Gets the current scope
* @returns {Object} Current scope context * @returns {typeof scopes[number]} Current scope context
*/ */
get current() { get current() {
return scopes[scopes.length - 1]; return scopes[scopes.length - 1];
@ -634,9 +634,9 @@ function memo(key, generator) {
memo.isScope = function(obj) { memo.isScope = function(obj) {
return obj[memoMark]; return obj[memoMark];
}; };
memo.scope = function memoScope(fun, { signal, onlyLast } = {}) { memo.scope = function memoScopeCreate(fun, { signal, onlyLast } = {}) {
let cache = oCreate(); let cache = oCreate();
function memoScope2(...args) { function memoScope(...args) {
if (signal && signal.aborted) if (signal && signal.aborted)
return fun.apply(this, args); return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate(); let cache_local = onlyLast ? cache : oCreate();
@ -651,10 +651,10 @@ memo.scope = function memoScope(fun, { signal, onlyLast } = {}) {
cache = cache_local; cache = cache_local;
return out; return out;
} }
memoScope2[memoMark] = true; memoScope[memoMark] = true;
memoScope2.clear = () => cache = oCreate(); memoScope.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope2.clear); if (signal) signal.addEventListener("abort", memoScope.clear);
return memoScope2; return memoScope;
}; };
export { export {
assign, assign,

2
dist/esm.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -86,19 +86,6 @@ var DDE = (() => {
function kebabToCamel(name) { function kebabToCamel(name) {
return name.replace(/-./g, (x) => x[1].toUpperCase()); return name.replace(/-./g, (x) => x[1].toUpperCase());
} }
var Defined = class extends Error {
constructor() {
super();
const [curr, ...rest] = this.stack.split("\n");
const curr_file = curr.slice(curr.indexOf("@"), curr.indexOf(".js:") + 4);
const curr_lib = curr_file.includes("src/helpers.js") ? "src/" : curr_file;
this.stack = rest.find((l) => !l.includes(curr_lib)) || curr;
}
get compact() {
const { stack } = this;
return stack.slice(0, stack.indexOf("@") + 1) + "\u2026" + stack.slice(stack.lastIndexOf("/"));
}
};
// src/dom-lib/common.js // src/dom-lib/common.js
var enviroment = { var enviroment = {
@ -348,7 +335,7 @@ var DDE = (() => {
var scope = { var scope = {
/** /**
* Gets the current scope * Gets the current scope
* @returns {Object} Current scope context * @returns {typeof scopes[number]} Current scope context
*/ */
get current() { get current() {
return scopes[scopes.length - 1]; return scopes[scopes.length - 1];
@ -708,9 +695,9 @@ var DDE = (() => {
memo.isScope = function(obj) { memo.isScope = function(obj) {
return obj[memoMark]; return obj[memoMark];
}; };
memo.scope = function memoScope(fun, { signal: signal2, onlyLast } = {}) { memo.scope = function memoScopeCreate(fun, { signal: signal2, onlyLast } = {}) {
let cache = oCreate(); let cache = oCreate();
function memoScope2(...args) { function memoScope(...args) {
if (signal2 && signal2.aborted) if (signal2 && signal2.aborted)
return fun.apply(this, args); return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate(); let cache_local = onlyLast ? cache : oCreate();
@ -725,10 +712,10 @@ var DDE = (() => {
cache = cache_local; cache = cache_local;
return out; return out;
} }
memoScope2[memoMark] = true; memoScope[memoMark] = true;
memoScope2.clear = () => cache = oCreate(); memoScope.clear = () => cache = oCreate();
if (signal2) signal2.addEventListener("abort", memoScope2.clear); if (signal2) signal2.addEventListener("abort", memoScope.clear);
return memoScope2; return memoScope;
}; };
// src/signals-lib/helpers.js // src/signals-lib/helpers.js
@ -845,17 +832,17 @@ var DDE = (() => {
}; };
var key_reactive = "__dde_reactive"; var key_reactive = "__dde_reactive";
signal.el = function(s, map) { signal.el = function(s, map) {
map = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true }); const mapScoped = memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start = createElement.mark({ type: "reactive", source: new Defined().compact }, true); const { current } = scope, { scope: sc } = current;
const mark_start = createElement.mark({ type: "reactive", component: sc && sc.name || "" }, true);
const mark_end = mark_start.end; const mark_end = mark_start.end;
const out = enviroment.D.createDocumentFragment(); const out = enviroment.D.createDocumentFragment();
out.append(mark_start, mark_end); out.append(mark_start, mark_end);
const { current } = scope;
const reRenderReactiveElement = (v) => { const reRenderReactiveElement = (v) => {
if (!mark_start.parentNode || !mark_end.parentNode) if (!mark_start.parentNode || !mark_end.parentNode)
return removeSignalListener(s, reRenderReactiveElement); return removeSignalListener(s, reRenderReactiveElement);
scope.push(current); scope.push(current);
let els = map(v); let els = mapScoped(v);
scope.pop(); scope.pop();
if (!Array.isArray(els)) if (!Array.isArray(els))
els = [els]; els = [els];
@ -875,7 +862,7 @@ var DDE = (() => {
current.host(on.disconnected( current.host(on.disconnected(
() => ( () => (
/*! Clears cached elements for reactive element `S.el` */ /*! Clears cached elements for reactive element `S.el` */
map.clear() mapScoped.clear()
) )
)); ));
return out; return out;
@ -947,9 +934,8 @@ var DDE = (() => {
function removeSignalsFromElements(s, listener, ...notes) { function removeSignalsFromElements(s, listener, ...notes) {
const { current } = scope; const { current } = scope;
current.host(function(element) { current.host(function(element) {
if (element[key_reactive]) if (!element[key_reactive]) element[key_reactive] = [];
return element[key_reactive].push([[s, listener], ...notes]); element[key_reactive].push([[s, listener], ...notes]);
element[key_reactive] = [];
if (current.prevent) return; if (current.prevent) return;
on.disconnected( on.disconnected(
() => ( () => (
@ -994,7 +980,6 @@ var DDE = (() => {
onclear, onclear,
host, host,
listeners: /* @__PURE__ */ new Set(), listeners: /* @__PURE__ */ new Set(),
defined: new Defined().stack,
readonly readonly
}), }),
enumerable: false, enumerable: false,

File diff suppressed because one or more lines are too long

14
dist/iife.js vendored
View File

@ -316,7 +316,7 @@ var DDE = (() => {
var scope = { var scope = {
/** /**
* Gets the current scope * Gets the current scope
* @returns {Object} Current scope context * @returns {typeof scopes[number]} Current scope context
*/ */
get current() { get current() {
return scopes[scopes.length - 1]; return scopes[scopes.length - 1];
@ -676,9 +676,9 @@ var DDE = (() => {
memo.isScope = function(obj) { memo.isScope = function(obj) {
return obj[memoMark]; return obj[memoMark];
}; };
memo.scope = function memoScope(fun, { signal, onlyLast } = {}) { memo.scope = function memoScopeCreate(fun, { signal, onlyLast } = {}) {
let cache = oCreate(); let cache = oCreate();
function memoScope2(...args) { function memoScope(...args) {
if (signal && signal.aborted) if (signal && signal.aborted)
return fun.apply(this, args); return fun.apply(this, args);
let cache_local = onlyLast ? cache : oCreate(); let cache_local = onlyLast ? cache : oCreate();
@ -693,10 +693,10 @@ var DDE = (() => {
cache = cache_local; cache = cache_local;
return out; return out;
} }
memoScope2[memoMark] = true; memoScope[memoMark] = true;
memoScope2.clear = () => cache = oCreate(); memoScope.clear = () => cache = oCreate();
if (signal) signal.addEventListener("abort", memoScope2.clear); if (signal) signal.addEventListener("abort", memoScope.clear);
return memoScope2; return memoScope;
}; };
return __toCommonJS(index_exports); return __toCommonJS(index_exports);
})(); })();

2
dist/iife.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -23,14 +23,6 @@ export function DataDashboard() {
conversion: [2.9, 3.5, 3.7, 2.6, 3.4, 3.5, 2.8, 2.8, 2.8, 3.1, 3.0, 2.7], conversion: [2.9, 3.5, 3.7, 2.6, 3.4, 3.5, 2.8, 2.8, 2.8, 3.1, 3.0, 2.7],
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
}; };
// Application state
const selectedYear = S(2024);
const selectedDataType = S(/** @type {'sales' | 'visitors' | 'conversion'} */ ('sales'));
const isLoading = S(false);
const error = S(null);
// Filter options
const years = [2022, 2023, 2024]; const years = [2022, 2023, 2024];
const dataTypes = [ const dataTypes = [
{ id: 'sales', label: 'Sales', unit: 'K' }, { id: 'sales', label: 'Sales', unit: 'K' },
@ -38,42 +30,32 @@ export function DataDashboard() {
{ id: 'conversion', label: 'Conversion Rate', unit: '%' } { id: 'conversion', label: 'Conversion Rate', unit: '%' }
]; ];
// Computed values // Filter options
const selectedData = S(() => { const selectedYear = S(2024);
return DATA[selectedDataType.get()];
});
const currentDataType = S(() => {
return dataTypes.find(type => type.id === selectedDataType.get());
});
const totalValue = S(() => {
const data = selectedData.get();
return data.reduce((sum, value) => sum + value, 0);
});
const averageValue = S(() => {
const data = selectedData.get();
return data.reduce((sum, value) => sum + value, 0) / data.length;
});
const highestValue = S(() => {
return Math.max(...selectedData.get());
});
// Event handlers
const onYearChange = on("change", e => { const onYearChange = on("change", e => {
selectedYear.set(parseInt(/** @type {HTMLSelectElement} */(e.target).value)); selectedYear.set(parseInt(/** @type {HTMLSelectElement} */(e.target).value));
loadData(); loadData();
}); });
const selectedDataType = S(/** @type {'sales' | 'visitors' | 'conversion'} */ ('sales'));
const onDataTypeChange = on("click", e => { const onDataTypeChange = on("click", e => {
const type = /** @type {'sales' | 'visitors' | 'conversion'} */( const type = /** @type {'sales' | 'visitors' | 'conversion'} */(
/** @type {HTMLButtonElement} */(e.currentTarget).dataset.type); /** @type {HTMLButtonElement} */(e.currentTarget).dataset.type);
selectedDataType.set(type); selectedDataType.set(type);
}); });
const currentDataType = S(() => dataTypes.find(type => type.id === selectedDataType.get()));
const selectedData = S(() => DATA[selectedDataType.get()]);
// Values based on filters
const totalValue = S(() => selectedData.get().reduce((sum, value) => sum + value, 0));
const averageValue = S(() => {
const data = selectedData.get();
return data.reduce((sum, value) => sum + value, 0) / data.length;
});
const highestValue = S(() => Math.max(...selectedData.get()));
// Simulate data loading // Simulate data loading
const isLoading = S(false);
const error = S(null);
function loadData() { function loadData() {
isLoading.set(true); isLoading.set(true);
error.set(null); error.set(null);
@ -114,7 +96,7 @@ export function DataDashboard() {
// Draw grid labels // Draw grid labels
ctx.fillStyle = '#999'; ctx.fillStyle = '#999';
ctx.font = '12px Arial'; ctx.font = '12px Arial';
ctx.fillText(Math.round(maxValue * (i / 5)), 20, y + 5); ctx.fillText(Math.round(maxValue * (i / 5)).toString(), 20, y + 5);
} }
ctx.stroke(); ctx.stroke();
@ -154,7 +136,6 @@ export function DataDashboard() {
) )
), ),
// Error message (only shown when there's an error)
S.el(error, errorMsg => !errorMsg S.el(error, errorMsg => !errorMsg
? el() ? el()
: el("div", { className: "error-message" }).append( : el("div", { className: "error-message" }).append(
@ -163,7 +144,6 @@ export function DataDashboard() {
), ),
), ),
// Loading indicator
S.el(isLoading, loading => !loading S.el(isLoading, loading => !loading
? el() ? el()
: el("div", { className: "loading-spinner" }) : el("div", { className: "loading-spinner" })

View File

@ -27,25 +27,23 @@ const imagesSample = (url=> [
* @returns {HTMLElement} Gallery element * @returns {HTMLElement} Gallery element
*/ */
export function ImageGallery(images= imagesSample) { export function ImageGallery(images= imagesSample) {
// Application state
const selectedImageId = S(null);
const filterTag = S('all'); const filterTag = S('all');
const imagesToDisplay = S(() => { const imagesToDisplay = S(() => {
const tag = filterTag.get(); const tag = filterTag.get();
if (tag === 'all') return images; if (tag === 'all') return images;
else return images.filter(img => img.alt.toLowerCase() === tag); else return images.filter(img => img.alt.toLowerCase() === tag);
}) })
const onFilterChange = tag => on("click", () => {
filterTag.set(tag);
});
// Derived state // Lightbox
const selectedImageId = S(null);
const selectedImage = S(() => { const selectedImage = S(() => {
const id = selectedImageId.get(); const id = selectedImageId.get();
return id ? images.find(img => img.id === id) : null; return id ? images.find(img => img.id === id) : null;
}); });
const isLightboxOpen = S(() => selectedImage.get() !== null); const isLightboxOpen = S(() => selectedImage.get() !== null);
// Event handlers
const onImageClick = id => on("click", () => { const onImageClick = id => on("click", () => {
selectedImageId.set(id); selectedImageId.set(id);
document.body.style.overflow = 'hidden'; // Prevent scrolling when lightbox is open document.body.style.overflow = 'hidden'; // Prevent scrolling when lightbox is open
@ -76,9 +74,6 @@ export function ImageGallery(images= imagesSample) {
const nextIndex = (currentIndex + 1) % images.length; const nextIndex = (currentIndex + 1) % images.length;
selectedImageId.set(images[nextIndex].id); selectedImageId.set(images[nextIndex].id);
}; };
const onFilterChange = tag => on("click", () => {
filterTag.set(tag);
});
// Keyboard navigation handler // Keyboard navigation handler
function handleKeyDown(e) { function handleKeyDown(e) {

View File

@ -73,8 +73,17 @@ export function Form({ initial }) {
this.value[key] = value; this.value[key] = value;
} }
}); });
/**
* Event handler for input events
* @param {"value"|"checked"} prop
* @returns {(ev: Event) => void}
* */
const onChange= prop => ev => {
const input = /** @type {HTMLInputElement} */(ev.target);
S.action(formState, "update", /** @type {keyof FormState} */(input.id), input[prop]);
};
// Derived signals for validation // Form validate state
const nameValid = S(() => formState.get().name.length >= 3); const nameValid = S(() => formState.get().name.length >= 3);
const emailValid = S(() => { const emailValid = S(() => {
const email = formState.get().email; const email = formState.get().email;
@ -89,8 +98,6 @@ export function Form({ initial }) {
return password === confirmPassword && confirmPassword !== ''; return password === confirmPassword && confirmPassword !== '';
}); });
const termsAgreed = S(() => formState.get().agreedToTerms); const termsAgreed = S(() => formState.get().agreedToTerms);
// Overall form validity
const formValid = S(() => const formValid = S(() =>
nameValid.get() && nameValid.get() &&
emailValid.get() && emailValid.get() &&
@ -99,16 +106,6 @@ export function Form({ initial }) {
termsAgreed.get() termsAgreed.get()
); );
// Event handlers
/**
* Event handler for input events
* @param {"value"|"checked"} prop
* @returns {(ev: Event) => void}
* */
const onChange= prop => ev => {
const input = /** @type {HTMLInputElement} */(ev.target);
S.action(formState, "update", /** @type {keyof FormState} */(input.id), input[prop]);
};
const dispatcSubmit = dispatchEvent("form:submit", host); const dispatcSubmit = dispatchEvent("form:submit", host);
const onSubmit = on("submit", e => { const onSubmit = on("submit", e => {
e.preventDefault(); e.preventDefault();

View File

@ -1,4 +1,4 @@
// Example of reactive element marker // Example of reactive element marker
<!--<dde:mark type="reactive" source="...">--> <!--<dde:mark type="reactive" component="<component-name>">-->
<!-- content that updates when signal changes --> <!-- content that updates when signal changes -->
<!--</dde:mark>--> <!--</dde:mark>-->

View File

@ -81,7 +81,6 @@ export function page({ pkg, info }){
el("li", t`actions: Custom actions that can be performed on the signal`), el("li", t`actions: Custom actions that can be performed on the signal`),
el("li", t`onclear: Functions to run when the signal is cleared`), el("li", t`onclear: Functions to run when the signal is cleared`),
el("li", t`host: Reference to the host element/scope in which the signal was created`), el("li", t`host: Reference to the host element/scope in which the signal was created`),
el("li", t`defined: Stack trace information for debugging`),
el("li", t`readonly: Boolean flag indicating if the signal is read-only`) el("li", t`readonly: Boolean flag indicating if the signal is read-only`)
), ),
el("p").append(T` el("p").append(T`

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "deka-dom-el", "name": "deka-dom-el",
"version": "0.9.2-alpha", "version": "0.9.3-alpha",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "deka-dom-el", "name": "deka-dom-el",
"version": "0.9.2-alpha", "version": "0.9.3-alpha",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@size-limit/preset-small-lib": "~11.2", "@size-limit/preset-small-lib": "~11.2",

View File

@ -1,6 +1,6 @@
{ {
"name": "deka-dom-el", "name": "deka-dom-el",
"version": "0.9.2-alpha", "version": "0.9.3-alpha",
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.", "description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.",
"author": "Jan Andrle <andrle.jan@centrum.cz>", "author": "Jan Andrle <andrle.jan@centrum.cz>",
"license": "MIT", "license": "MIT",

View File

@ -19,7 +19,7 @@ const store_abort= new WeakMap();
export const scope= { export const scope= {
/** /**
* Gets the current scope * Gets the current scope
* @returns {Object} Current scope context * @returns {typeof scopes[number]} Current scope context
*/ */
get current(){ return scopes[scopes.length-1]; }, get current(){ return scopes[scopes.length-1]; },

View File

@ -68,21 +68,3 @@ export function observedAttributes(instance, observedAttribute){
* @returns {string} The camelCase string * @returns {string} The camelCase string
*/ */
function kebabToCamel(name){ return name.replace(/-./g, x=> x[1].toUpperCase()); } function kebabToCamel(name){ return name.replace(/-./g, x=> x[1].toUpperCase()); }
/**
* Error class for definition tracking
* Shows the correct stack trace for debugging (signal) creation
*/
export class Defined extends Error{
constructor(){
super();
const [ curr, ...rest ]= this.stack.split("\n");
const curr_file= curr.slice(curr.indexOf("@"), curr.indexOf(".js:")+4);
const curr_lib= curr_file.includes("src/helpers.js") ? "src/" : curr_file;
this.stack= rest.find(l=> !l.includes(curr_lib)) || curr;
}
get compact(){
const { stack }= this;
return stack.slice(0, stack.indexOf("@")+1)+"…"+stack.slice(stack.lastIndexOf("/"));
}
}

View File

@ -27,7 +27,7 @@ memo.isScope= function(obj){ return obj[memoMark]; };
* @param {AbortSignal} options.signal * @param {AbortSignal} options.signal
* @param {boolean} [options.onlyLast=false] * @param {boolean} [options.onlyLast=false]
* */ * */
memo.scope= function memoScope(fun, { signal, onlyLast }= {}){ memo.scope= function memoScopeCreate(fun, { signal, onlyLast }= {}){
let cache= oCreate(); let cache= oCreate();
function memoScope(...args){ function memoScope(...args){
if(signal && signal.aborted) if(signal && signal.aborted)

View File

@ -1,6 +1,6 @@
import { queueSignalWrite, mark } from "./helpers.js"; import { queueSignalWrite, mark } from "./helpers.js";
export { mark }; export { mark };
import { hasOwn, Defined, oCreate, oAssign } from "../helpers.js"; import { hasOwn, oCreate, oAssign } from "../helpers.js";
const Signal = oCreate(null, { const Signal = oCreate(null, {
get: { value(){ return read(this); } }, get: { value(){ return read(this); } },
@ -169,21 +169,21 @@ import { memo } from "../memo.js";
* Creates a reactive DOM element that re-renders when signal changes * Creates a reactive DOM element that re-renders when signal changes
* *
* @param {Object} s - Signal object to watch * @param {Object} s - Signal object to watch
* @param {Function} map - Function mapping signal value to DOM elements * @param {Function} mapScoped - Function mapping signal value to DOM elements
* @returns {DocumentFragment} Fragment containing reactive elements * @returns {DocumentFragment} Fragment containing reactive elements
*/ */
signal.el= function(s, map){ signal.el= function(s, map){
map= memo.isScope(map) ? map : memo.scope(map, { onlyLast: true }); const mapScoped= memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start= el.mark({ type: "reactive", source: new Defined().compact }, true); const { current }= scope, { scope: sc }= current;
const mark_start= el.mark({ type: "reactive", component: sc && sc.name || "" }, true);
const mark_end= mark_start.end; const mark_end= mark_start.end;
const out= env.D.createDocumentFragment(); const out= env.D.createDocumentFragment();
out.append(mark_start, mark_end); out.append(mark_start, mark_end);
const { current }= scope;
const reRenderReactiveElement= v=> { const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeSignalListener(s, reRenderReactiveElement); return removeSignalListener(s, reRenderReactiveElement);
scope.push(current); scope.push(current);
let els= map(v); let els= mapScoped(v);
scope.pop(); scope.pop();
if(!Array.isArray(els)) if(!Array.isArray(els))
els= [ els ]; els= [ els ];
@ -202,7 +202,7 @@ signal.el= function(s, map){
reRenderReactiveElement(s.get()); reRenderReactiveElement(s.get());
current.host(on.disconnected(()=> current.host(on.disconnected(()=>
/*! Clears cached elements for reactive element `S.el` */ /*! Clears cached elements for reactive element `S.el` */
map.clear() mapScoped.clear()
)); ));
return out; return out;
}; };
@ -314,9 +314,8 @@ export const signals_config= {
function removeSignalsFromElements(s, listener, ...notes){ function removeSignalsFromElements(s, listener, ...notes){
const { current }= scope; const { current }= scope;
current.host(function(element){ current.host(function(element){
if(element[key_reactive]) if(!element[key_reactive]) element[key_reactive]= [];
return element[key_reactive].push([ [ s, listener ], ...notes ]); element[key_reactive].push([ [ s, listener ], ...notes ]);
element[key_reactive]= [];
if(current.prevent) return; // typically document.body, doenst need auto-remove as it should happen on page leave if(current.prevent) return; // typically document.body, doenst need auto-remove as it should happen on page leave
on.disconnected(()=> on.disconnected(()=>
/*! Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, ?). /*! Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, ?).
@ -386,7 +385,6 @@ function toSignal(s, value, actions, readonly= false){
value: oAssign(oCreate(protoSigal), { value: oAssign(oCreate(protoSigal), {
value, actions, onclear, host, value, actions, onclear, host,
listeners: new Set(), listeners: new Set(),
defined: new Defined().stack,
readonly readonly
}), }),
enumerable: false, enumerable: false,