'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
/*
* This file is a collection of strings used to store values
* in the tram-one global space. If you ever need to debug Tram-One's
* internal state, you can inspect these on the window.
*
* e.g. `window['tram-space']['tram-hook-key']`
*/
const TRAM_HOOK_KEY = 'tram-hook-key';
const TRAM_EFFECT_STORE = 'tram-effect-store';
const TRAM_EFFECT_QUEUE = 'tram-effect-queue';
const TRAM_KEY_STORE = 'tram-key-store';
const TRAM_KEY_QUEUE = 'tram-key-queue';
const TRAM_GLOBAL_KEY_QUEUE = 'tram-global-key-queue';
const TRAM_OBSERVABLE_STORE = 'tram-observable-store';
const TRAM_MUTATION_OBSERVER = 'tram-mutation-observer';
/*
* namespace is a generic interface for global tram-one state that needs
* to be persisted in the app container. It exposes a setup and get function.
*/
const getTramWindow = () => {
// if tram-one is setup it will have a defined value in the 'tram-space'
const tramOneIsSetup = window['tram-space'];
// otherwise, we should warn
// this usually happens when calling a hook outside of a component function
// but this could be potentially triggered other ways - if we find those, we should broaden the message then
if (!tramOneIsSetup) {
throw new Error(`
Tram-One: app has not started yet, but hook was called. Is it being invoked outside a component function?
https://github.com/Tram-One/tram-one/issues/178
`);
}
return window;
};
const setupTramOneSpace = () => {
window['tram-space'] = {};
};
const buildNamespace = (constructor) => {
const setup = (namespace) => {
const tramWindow = getTramWindow();
tramWindow['tram-space'][namespace] = constructor();
return tramWindow['tram-space'][namespace];
};
const get = (namespace) => {
const tramWindow = getTramWindow();
return tramWindow['tram-space'][namespace];
};
const set = (namespace, value) => {
const tramWindow = getTramWindow();
tramWindow['tram-space'][namespace] = value;
};
return { setup, get, set };
};
/*
* This file defines all the functions required to interact with
* a working-key object. This working-key object is used to help
* hooks understand where in the mounting process we are, and what
* values or effects to pull / trigger.
*/
const defaultWorkingKey = () => ({
// list of custom tags that we've stepped into
branch: [],
// map of branches to index value (used as a cursor for hooks)
branchIndices: {
'': 0,
},
});
const { setup: setupWorkingKey, get: getWorkingKey } = buildNamespace(defaultWorkingKey);
const getWorkingBranch = (keyName) => {
const workingkeyObject = getWorkingKey(keyName);
return workingkeyObject.branch.join('/');
};
/**
* push a new branch value, usually when we step into a new
* custom component when mounting.
*/
const pushWorkingKeyBranch = (keyName, branch) => {
const workingKey = getWorkingKey(keyName);
workingKey.branch.push(branch);
if (!workingKey.branchIndices[getWorkingBranch(keyName)]) {
workingKey.branchIndices[getWorkingBranch(keyName)] = 0;
}
};
/**
* pops the current branch value, usually when we are done mounting
* a single child component.
*/
const popWorkingKeyBranch = (keyName) => {
const workingKey = getWorkingKey(keyName);
workingKey.branch.pop();
};
/**
* increments the value for the current branch.
* These values are used to pull the correct hook value on re-renders.
*/
const incrementWorkingKeyBranch = (keyName) => {
const workingKey = getWorkingKey(keyName);
workingKey.branchIndices[getWorkingBranch(keyName)] += 1;
};
/**
* used to get a unique string that will be used as a key for observables and effects.
* This unique string _should_ be consistent over many re-renders.
*/
const getWorkingKeyValue = (keyName) => {
const workingKey = getWorkingKey(keyName);
const index = workingKey.branchIndices[getWorkingBranch(keyName)];
return `${getWorkingBranch(keyName)}[${index}]`;
};
/**
* returns a deep copy of the existing key, usually used as a restore point later
*/
const copyWorkingKey = (keyName) => {
const key = getWorkingKey(keyName);
return {
branch: [...key.branch],
branchIndices: { ...key.branchIndices },
};
};
/**
* if we needed to reset pre-emptively, use this to get back
* to where the branches were before
*/
const restoreWorkingKey = (keyName, restoreKey) => {
const key = getWorkingKey(keyName);
const branches = key.branchIndices;
key.branch = [...restoreKey.branch];
const resetBranchValue = (branch) => {
branches[branch] = restoreKey.branchIndices[branch] || 0;
};
Object.keys(key.branchIndices).forEach(resetBranchValue);
};
/*
* This file is a collection of strings used to store values
* in custom elements. If you ever need to debug Tram-One's
* internal state, you can inspect these on individual elements.
*
* e.g. `$0['tram-hook-key']`
*/
const TRAM_TAG = 'tram-tag';
const TRAM_TAG_REACTION = 'tram-tag-reaction';
const TRAM_TAG_STORE_KEYS = 'tram-tag-store-keys';
const TRAM_TAG_NEW_EFFECTS = 'tram-tag-new-effects';
const TRAM_TAG_CLEANUP_EFFECTS = 'tram-tag-cleanup-effects';
// Debug properties used for tram-one dev tools
const TRAM_TAG_NAME = 'tram-tag-name';
const TRAM_TAG_PROPS = 'tram-tag-props';
const TRAM_TAG_CHILDREN = 'tram-tag-children';
const TRAM_TAG_GLOBAL_STORE_KEYS = 'tram-tag-global-store-keys';
const { observe: observe$1 } = require('@nx-js/observer-util');
// functions to go to nodes or indices (made for .map)
const toIndices = (node, index) => index;
// sorting function that prioritizes indices that are closest to a target
// e.g. target = 3, [1, 2, 3, 4, 5] => [3, 2, 4, 1, 5]
const byDistanceFromIndex = (targetIndex) => (indexA, indexB) => {
const diffFromTargetA = Math.abs(indexA - targetIndex);
const diffFromTargetB = Math.abs(indexB - targetIndex);
return diffFromTargetA - diffFromTargetB;
};
const hasMatchingTagName = (tagName) => (node) => {
const nodeHasMatchingTagName = 'tagName' in node && node.tagName === tagName;
// if the tagName matches, we want to process the node, otherwise skip it
return nodeHasMatchingTagName ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
};
// get an array including the element and all it's children
const parentAndChildrenElements = (node, tagName) => {
const matchesTagName = hasMatchingTagName(tagName);
const componentWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT, matchesTagName);
const parentAndChildren = [componentWalker.currentNode];
while (componentWalker.nextNode()) {
parentAndChildren.push(componentWalker.currentNode);
}
// since we are looking for elements (things with tagNames)
// we can safely declare this as an array of Elements
return parentAndChildren;
};
const defaultRemovedElementWithFocusData = {
index: -1,
tagName: '',
scrollLeft: 0,
scrollTop: 0,
selectionStart: null,
selectionEnd: null,
selectionDirection: undefined,
};
/**
* This is a helper function for the dom creation.
* This function observes any state values used when making the tag, and allow it to update
* independently when one of those state values updates.
*
* The mutation-observer will unobserve any reactions here when the node is removed.
*
* The parameter tagFunction is almost a TramOneComponent, but it already has the props and children prepopulated,
* and so has no parameters, but returns a TramOneElement
*/
var observeTag = (tagFunction) => {
let tagResult;
const buildAndReplaceTag = () => {
// if there is an existing tagResult, it is the last rendering, and so we want to re-render over it
let oldTag = tagResult;
let removedElementWithFocusData = defaultRemovedElementWithFocusData;
// remove oldTag first so that we unobserve before we re-observe
if (oldTag) {
// we need to blow away any old focus data we had
removedElementWithFocusData = defaultRemovedElementWithFocusData;
// determine if this element (or any element under it) had focus
const oldTagHasFocusedElement = oldTag.contains(document.activeElement);
// if an element had focus, copy over all the selection data (so we can copy it back later)
if (oldTagHasFocusedElement) {
// we'll assume that the element is an HTMLInputElement, in reality other kinds of elements will be caught here,
// but that's fine, since they have null as selection attributes, and setting them to null is fine
const activeElement = document.activeElement;
// first, we need to get all the elements that are similar (we'll use tagName)
// this way, when we rerender, we can search for those tagNames, and just use the index we got here
const allActiveLikeElements = parentAndChildrenElements(oldTag, activeElement.tagName);
removedElementWithFocusData.index = allActiveLikeElements.findIndex((element) => element === activeElement);
// copy over the data
removedElementWithFocusData.tagName = activeElement.tagName;
removedElementWithFocusData.scrollLeft = activeElement.scrollLeft;
removedElementWithFocusData.scrollTop = activeElement.scrollTop;
removedElementWithFocusData.selectionStart = activeElement.selectionStart;
removedElementWithFocusData.selectionEnd = activeElement.selectionEnd;
removedElementWithFocusData.selectionDirection = activeElement.selectionDirection || undefined;
}
const emptyDiv = document.createElement('div');
oldTag.replaceWith(emptyDiv);
// copy the reaction and effects from the old tag to the empty div so we don't lose them
emptyDiv[TRAM_TAG_REACTION] = oldTag[TRAM_TAG_REACTION];
emptyDiv[TRAM_TAG_NEW_EFFECTS] = oldTag[TRAM_TAG_NEW_EFFECTS];
emptyDiv[TRAM_TAG_CLEANUP_EFFECTS] = oldTag[TRAM_TAG_CLEANUP_EFFECTS];
// copy over development props
if (process.env.NODE_ENV === 'development') {
emptyDiv[TRAM_TAG_NAME] = oldTag[TRAM_TAG_NAME];
emptyDiv[TRAM_TAG_PROPS] = oldTag[TRAM_TAG_PROPS];
emptyDiv[TRAM_TAG_GLOBAL_STORE_KEYS] = oldTag[TRAM_TAG_GLOBAL_STORE_KEYS];
}
// set oldTag to emptyDiv, so we can replace it later
oldTag = emptyDiv;
}
// build the component
tagResult = tagFunction();
// if oldTag was defined, then we need to replace it with the new result
if (oldTag) {
// if an element had focus, reapply it
let elementToGiveFocus;
if (removedElementWithFocusData.index >= 0) {
const allActiveLikeElements = parentAndChildrenElements(tagResult, removedElementWithFocusData.tagName);
// we'll look through the elements (in order of nodes closest to original index) and find a tag that matches.
// this means if it didn't move, we'll get it right away,
// if it did, we'll look at the elements closest to the original position
const elementIndexToGiveFocus = allActiveLikeElements
.map(toIndices)
.sort(byDistanceFromIndex(removedElementWithFocusData.index))[0];
elementToGiveFocus = allActiveLikeElements[elementIndexToGiveFocus];
// also try to set the selection, if there is a selection for this element
try {
if (elementToGiveFocus.setSelectionRange !== undefined) {
elementToGiveFocus.setSelectionRange(removedElementWithFocusData.selectionStart, removedElementWithFocusData.selectionEnd, removedElementWithFocusData.selectionDirection);
}
}
catch (exception) {
// don't worry if we fail
// this can happen if the element has a `setSelectionRange` but it isn't supported
// e.g. input with type="range"
}
elementToGiveFocus.scrollLeft = removedElementWithFocusData.scrollLeft;
elementToGiveFocus.scrollTop = removedElementWithFocusData.scrollTop;
}
// don't lose track that this is still a tram-one element
tagResult[TRAM_TAG] = true;
// copy the reaction and effects from the old tag to the new one
tagResult[TRAM_TAG_REACTION] = oldTag[TRAM_TAG_REACTION];
tagResult[TRAM_TAG_NEW_EFFECTS] = oldTag[TRAM_TAG_NEW_EFFECTS];
tagResult[TRAM_TAG_CLEANUP_EFFECTS] = oldTag[TRAM_TAG_CLEANUP_EFFECTS];
// copy over development props
if (process.env.NODE_ENV === 'development') {
tagResult[TRAM_TAG_NAME] = oldTag[TRAM_TAG_NAME];
tagResult[TRAM_TAG_PROPS] = oldTag[TRAM_TAG_PROPS];
tagResult[TRAM_TAG_GLOBAL_STORE_KEYS] = oldTag[TRAM_TAG_GLOBAL_STORE_KEYS];
}
// both these actions cause forced reflow, and can be performance issues
oldTag.replaceWith(tagResult);
if (elementToGiveFocus && elementToGiveFocus.focus)
elementToGiveFocus.focus();
}
};
const tagReaction = observe$1(buildAndReplaceTag);
// tagResult is always assigned as an artifact of the observe() call above
// if it isn't, we want to know about it
if (tagResult === undefined) {
throw new Error(`
Tram-One: tagResult was not defined after building the tag.
https://github.com/Tram-One/tram-one/issues/177
`);
}
// save the reaction to the node, so that the mutation-observer can unobserve it later
tagResult[TRAM_TAG_REACTION] = tagReaction;
return tagResult;
};
/*
* EffectStores in Tram-One are used for basic key-value object mappings that need
* to be persisted in the globalSpace.
*
* Currently this is used with useEffect to keep track of what
* new effects should be triggered or cleaned up
*/
const newDefaultEffectStore = () => {
return {};
};
const { setup: setupEffectStore, get: getEffectStore, set: setEffectStore, } = buildNamespace(newDefaultEffectStore);
/**
* clear the effect store
* usually called when we want to empty the effect store
*/
const clearEffectStore = (effectStoreName) => {
const effectStore = getEffectStore(effectStoreName);
Object.keys(effectStore).forEach((key) => delete effectStore[key]);
};
/**
* restore the effect store to a previous value
* usually used when we had to interrupt the processing of effects
*/
const restoreEffectStore = setEffectStore;
/*
* The KeyQueue in Tram-One is a basic list of keys
* that needs to be persisted in the globalSpace.
*
* Currently this is used with useStore to keep track of what
* stores need to be associated with generated elements
*/
const newDefaultKeyQueue = () => {
return [];
};
const { setup: setupKeyQueue, get: getKeyQueue, set: setKeyQueue } = buildNamespace(newDefaultKeyQueue);
/**
* clear the key queue
* usually called when we want to empty the key queue
*/
const clearKeyQueue = (keyQueueName) => {
const keyQueue = getKeyQueue(keyQueueName);
keyQueue.splice(0, keyQueue.length);
};
/**
* restore the key queue to a previous value
* usually used when we had to interrupt the processing of keys
*/
const restoreKeyQueue = setKeyQueue;
/**
* This is a helper function for the dom creation.
* This function stores any keys generated when building a tag in the resulting node that is generated.
*
* These are later processed by the mutation-observer, and cleaned up when the node is removed by the mutation-observer.
*
* This function is called every time state changes in an observable store
*/
var processHooks = (tagFunction) => {
// save the existing effect queue and key queue for any components we are in the middle of building
const existingQueuedEffects = { ...getEffectStore(TRAM_EFFECT_QUEUE) };
const existingQueuedKeys = [...getKeyQueue(TRAM_KEY_QUEUE)];
// clear the queues (so we can get just new effects and keys)
clearEffectStore(TRAM_EFFECT_QUEUE);
clearKeyQueue(TRAM_KEY_QUEUE);
clearKeyQueue(TRAM_GLOBAL_KEY_QUEUE);
// create the component, which will save new effects to the effect queue
const tagResult = tagFunction();
// see if there are any brand new effects
const existingEffects = getEffectStore(TRAM_EFFECT_STORE);
const queuedEffects = getEffectStore(TRAM_EFFECT_QUEUE);
// pull new effects that have yet to be processed from the tag
// these can appear when a component re-exposes another component at its root
const existingNewEffects = tagResult[TRAM_TAG_NEW_EFFECTS] || [];
// store new effects (and the existing new effects) in the node we just built
const newEffects = Object.keys(queuedEffects).filter((effect) => !(effect in existingEffects));
const newEffectFunctions = newEffects.map((newEffectKey) => queuedEffects[newEffectKey]);
const existingNewAndBrandNewEffects = existingNewEffects.concat(newEffectFunctions);
tagResult[TRAM_TAG_NEW_EFFECTS] = existingNewAndBrandNewEffects;
// same as the existingNewEffects, but for state values
const existingNewKeys = tagResult[TRAM_TAG_STORE_KEYS] || [];
// get all new keys
const newKeys = getKeyQueue(TRAM_KEY_QUEUE);
// store keys in the node we just built
const existingNewAndBrandNewKeys = existingNewKeys.concat(newKeys);
tagResult[TRAM_TAG_STORE_KEYS] = existingNewAndBrandNewKeys;
// if this is development environment, save global store keys to the element
if (process.env.NODE_ENV === 'development') {
const existingNewGlobalKeys = tagResult[TRAM_TAG_GLOBAL_STORE_KEYS] || [];
const newGlobalKeys = getKeyQueue(TRAM_GLOBAL_KEY_QUEUE);
// store global store keys in the node we just built
const existingNewAndBrandNewGlobalKeys = existingNewGlobalKeys.concat(newGlobalKeys);
tagResult[TRAM_TAG_GLOBAL_STORE_KEYS] = existingNewAndBrandNewGlobalKeys;
}
// restore the effect and key queues to what they were before we started
restoreEffectStore(TRAM_EFFECT_QUEUE, existingQueuedEffects);
restoreKeyQueue(TRAM_KEY_QUEUE, existingQueuedKeys);
return tagResult;
};
const nanohtml = require('@tram-one/nanohtml');
const rbel = require('@tram-one/rbel');
const hyperx = require('@tram-one/hyperx');
/**
* This function takes in a namespace and registry of custom components,
* and builds a `dom` template tag function that can take in a template XML string.
*
* This function shouldn't need to be called directly, instead, you can use `registerHtml` or `registerSvg`
*
* @param registry mapping of tag names to component functions
* @param namespace namespace to create nodes in (by default XHTML namespace)
*/
const registerDom = (namespace, registry = {}) => {
// modify the registry so that each component function updates the hook working key
const hookedRegistry = Object.keys(registry).reduce((newRegistry, tagName) => {
const tagFunction = registry[tagName];
const hookedTagFunction = (props, children) => {
// push a new branch onto the working key so any values that need to be unique among components
// but consistent across renders can be read
const stringifiedProps = JSON.stringify(props);
const newBranch = `${tagName}[${stringifiedProps}]`;
pushWorkingKeyBranch(TRAM_HOOK_KEY, newBranch);
// increment branch so that we have a unique value (in case we are rendering a list of components)
incrementWorkingKeyBranch(TRAM_HOOK_KEY);
const uniqueBranch = copyWorkingKey(TRAM_HOOK_KEY);
// create a tag function that has the args passed in
const populatedTagFunction = () => {
// reset working key so we have the correct place when starting a new component
restoreWorkingKey(TRAM_HOOK_KEY, uniqueBranch);
const processedChildren = children?.map((child) => {
if (child instanceof Element)
return child;
return `${child}`;
});
return tagFunction(props, processedChildren);
};
// observe store usage and process any new effects that were called when building the component
const processHooksAndBuildTagResult = () => processHooks(populatedTagFunction);
const tagResult = observeTag(processHooksAndBuildTagResult);
// pop the branch off (since we are done rendering this component)
popWorkingKeyBranch(TRAM_HOOK_KEY);
// decorate the properties expected on TramOneElements (see node-names.ts)
tagResult[TRAM_TAG] = true;
// we won't decorate TRAM_TAG_REACTION, that needs to be done later when we observe the tag
tagResult[TRAM_TAG_NEW_EFFECTS] = tagResult[TRAM_TAG_NEW_EFFECTS] || [];
// cleanup effects will be populated when new effects are processed
tagResult[TRAM_TAG_CLEANUP_EFFECTS] = [];
// save properties for development debugging
if (process.env.NODE_ENV === 'development') {
tagResult[TRAM_TAG_NAME] = tagName;
tagResult[TRAM_TAG_PROPS] = props;
tagResult[TRAM_TAG_CHILDREN] = children;
}
return tagResult;
};
return { ...newRegistry, [tagName]: hookedTagFunction };
}, {});
return rbel(hyperx, nanohtml(namespace), hookedRegistry);
};
/**
* @name useEffect
* @link https://tram-one.io/#use-effect
* @description
* Hook that triggers component start, update, and cleanup effects.
* If the return of effect is another function, then that function is called on when the component is removed.
* If the effect is dependent on a observable, it will automatically trigger again if that value updates.
*
* @param effect function to run on component mount
*/
var useEffect = (effect) => {
// get the store of effects
const effectQueue = getEffectStore(TRAM_EFFECT_QUEUE);
// get the key value from working-key
const key = getWorkingKeyValue(TRAM_HOOK_KEY);
// increment the working key branch value
// this makes successive useEffects calls unique (until we reset the key)
incrementWorkingKeyBranch(TRAM_HOOK_KEY);
// append () so that it's easier to debug effects from components
const callLikeKey = `${key}()`;
// add the effect to the effect queue, so it can be processed later
effectQueue[callLikeKey] = effect;
};
const xml = registerDom(null);
const fragment = (props, children) => {
useEffect((ref) => {
(children || []).forEach((child) => {
if (typeof child === 'string') {
ref.insertAdjacentText('beforebegin', child);
}
else {
ref.insertAdjacentElement('beforebegin', child);
}
});
ref.remove();
});
return xml `${children}`;
};
/**
* @name registerHtml
* @link https://tram-one.io/#register-html
* @description
* Function to generate a tagged template function for XHTML / HTML.
* Takes in a registry that allows you to import other tag functions and use them in your template string.
*
* @param registry map of tag names to functions, use this to use custom elements built in tram-one
* @return tagged template function that builds HTML components
*/
const registerHtml = (registry) => {
return registerDom(null, { '': fragment, ...registry });
};
/**
* @name registerSvg
* @link https://tram-one.io/#register-svg
* @description
* Function to generate a tagged template function for SVG.
*
* @param registry map of tag names to functions, use this to use custom elements built in tram-one
* @return tagged template function that builds SVG components
*/
const registerSvg = (registry) => {
return registerDom('http://www.w3.org/2000/svg', { '': fragment, ...registry });
};
const { observable } = require('@nx-js/observer-util');
/*
* Observable Stores in Tram-One are used for objects whose properties need to be observed.
* This stores the values in the useStore and useGlobalStore hooks, internally tracking
* them as proxies, and making observed functions respond to their changes.
*/
const { setup: setupObservableStore, get: getObservableStore } = buildNamespace(() => observable({}));
/**
* Shared source code for both observable hooks, useStore, and useGlobalStore.
* This hook exposes a globally stored value (in either case), that can cause the component
* to update when a subfield of that value is updated.
*
* It has a similar interface to React's useState
*/
var observableHook = (key, value) => {
// get the store of effects
const observableStore = getObservableStore(TRAM_OBSERVABLE_STORE);
// increment the working key branch value
// this makes successive hooks unique (until we reset the key)
incrementWorkingKeyBranch(TRAM_HOOK_KEY);
// if a key was passed in, use that, otherwise, generate a key
const resolvedKey = key || getWorkingKeyValue(TRAM_HOOK_KEY);
// saves value into the store if it doesn't exist in the observableStore yet
// and if the value we are writing is defined
if (!Object.prototype.hasOwnProperty.call(observableStore, resolvedKey) && value !== undefined) {
// save the value as a shallow copy of the parameter passed in
observableStore[resolvedKey] = Array.isArray(value) ? [...value] : { ...value };
}
// get value for key
const keyValue = observableStore[resolvedKey];
// if we weren't passed in a key, this is a local obserable (not global),
const isLocalStore = !key;
if (isLocalStore) {
// if this is local, we should associate it with the element by putting it in the keyQueue
getKeyQueue(TRAM_KEY_QUEUE).push(resolvedKey);
}
// if this is a development environment, save the global store key to the element
if (!isLocalStore && process.env.NODE_ENV === 'development') {
getKeyQueue(TRAM_GLOBAL_KEY_QUEUE).push(resolvedKey);
}
// return value
return keyValue;
};
/**
* @name useStore
* @link https://tram-one.io/#use-store
* @description
* Hook that stores local component state.
*
* If the subfield of an object, or element of an array is updated
* it will cause only the components that are dependent on that value to update.
*
* @param defaultValue the default value to start the store at
*
* @returns the store to interact with.
*/
var useStore = (defaultValue) => observableHook(undefined, defaultValue);
const urlListener = require('url-listener');
const useUrlParams = require('use-url-params');
/**
* @name useUrlParams
* @link https://tram-one.io/#use-url-params
* @description
* Hook that returns path variables based on the route.
* Can return path parameters, query params, and more.
* It's internal functionality is powered by the package
* {@link https://www.npmjs.com/package/rlite-router rlite}
*
* @param pattern path to match on (can include path variables)
*
* @returns object with a `matches` key, and (if it matched) path and query parameters
*/
var useUrlParams$1 = (pattern) => {
// save and update results in an observable, so that we can update
// components and effects in a reactive way
const initialParams = useUrlParams(pattern);
const observedUrlParams = useStore(initialParams);
// urlListener can re-read the route and save the new results to the observable
urlListener(() => {
const updatedParams = useUrlParams(pattern);
// get all keys so we can override new and old ones (without having to override the whole object)
const allParamKeys = [...Object.keys(initialParams), ...Object.keys(updatedParams)];
allParamKeys.forEach((paramKey) => {
observedUrlParams[paramKey] = updatedParams[paramKey];
});
});
return observedUrlParams;
};
/** Implementation of the two function definitions */
function useGlobalStore(key, defaultValue) {
return observableHook(key, defaultValue);
}
/**
* Updates a container with an initial component for the first render.
* @param component the tram-one component to render
* @param container an element to render the component on
*/
var mount = (component, container) => {
const html = registerHtml({
app: component,
});
// this sadly needs to be wrapped in some element so we can process effects
// otherwise the root node will not have effects applied on it
const renderedApp = html ``;
container.replaceChild(renderedApp, container.firstElementChild);
};
/**
* Helper function for getting an element when given a string or element
* @param target either a CSS selector, or Element to attach the component to.
* @returns the container that we can mount on
*/
const getContainerElement = (target) => {
// if the selector is a string, try to find the element,
// otherwise it's probably DOM that we should write directly to
if (typeof target === 'string') {
const selectedElement = document.querySelector(target);
if (selectedElement === null) {
throw new Error(`
Tram-One: could not find target, is the element on the page yet?
https://github.com/Tram-One/tram-one/issues/179
`);
}
return selectedElement;
}
else {
return target;
}
};
/**
* Function to determine (or create) the element that we will mount our tram-one app onto
* @param target either a CSS selector, or Element to attach the component to.
* This elememnt should be initially empty.
*
* @returns the container, now with a div that tram-one can manage
*/
var buildContainer = (target) => {
const container = getContainerElement(target);
// build a div to render the app on
// - if it doesn't exist as a child of the selector, create one first
if (!container.firstElementChild) {
const containerChild = document.createElement('div');
container.appendChild(containerChild);
}
return container;
};
/*
* The KeyStore in Tram-One is a basic key-value object
* that needs to be persisted in the globalSpace.
*
* Currently this is used with useStore and useGlobalStore to keep
* track of what stores need to be cleaned up when removing elements
*/
const newDefaultKeyStore = () => {
return {};
};
const { setup: setupKeyStore, get: getKeyStore, set: setKeyStore } = buildNamespace(newDefaultKeyStore);
/**
* increment (or set initial value) for the keyStore
*/
const incrementKeyStoreValue = (keyStoreName, key) => {
const keyStore = getKeyStore(keyStoreName);
keyStore[key] = keyStore[key] + 1 || 1;
};
/**
* decrement a value in the keyStore
*/
const decrementKeyStoreValue = (keyStoreName, key) => {
const keyStore = getKeyStore(keyStoreName);
keyStore[key]--;
};
/*
* The mutation-observer is a global instance of browsers MutationObserver
* which tracks when nodes are added or removed.
*
* When nodes are added we process their effects. When nodes are removed we process any cleanup,
* and stop observers that would trigger for that node.
*/
const { observe, unobserve } = require('@nx-js/observer-util');
/**
* process side-effects for new tram-one nodes
* (this includes calling effects, and keeping track of stores)
*/
const processTramTags = (node) => {
// if this element doesn't have a TRAM_TAG, it's not a Tram-One Element
if (!(TRAM_TAG in node)) {
return;
}
const hasStoreKeys = node[TRAM_TAG_STORE_KEYS];
if (hasStoreKeys) {
// for every store associated with this element, increment the count
// - this ensures that it doesn't get blown away when we clean up old stores
node[TRAM_TAG_STORE_KEYS].forEach((key) => {
incrementKeyStoreValue(TRAM_KEY_STORE, key);
});
}
const hasEffects = node[TRAM_TAG_NEW_EFFECTS];
if (hasEffects) {
// create an array for the cleanup effects
node[TRAM_TAG_CLEANUP_EFFECTS] = [];
// run all the effects, saving any cleanup functions to the node
node[TRAM_TAG_NEW_EFFECTS].forEach((effect) => {
let cleanup;
// this is called when an effect is re-triggered
const effectReaction = observe(() => {
// verify that cleanup is a function before calling it (in case it was a promise)
if (typeof cleanup === 'function')
cleanup();
cleanup = effect(node);
});
// this is called when a component with an effect is removed
const totalCleanup = () => {
// verify that cleanup is a function before calling it (in case it was a promise)
if (typeof cleanup === 'function')
cleanup();
unobserve(effectReaction);
};
node[TRAM_TAG_CLEANUP_EFFECTS].push(totalCleanup);
});
// set new tag effects to an empty array
node[TRAM_TAG_NEW_EFFECTS] = [];
}
};
/**
* call all cleanup effects on the node
*/
const cleanupEffects = (cleanupEffects) => {
cleanupEffects.forEach((cleanup) => cleanup());
};
/**
* remove the association of the store with this specific element
*/
const removeStoreKeyAssociation = (storeKeys) => {
storeKeys.forEach((storeKey) => {
decrementKeyStoreValue(TRAM_KEY_STORE, storeKey);
});
};
/**
* remove any stores that no longer have any elements associated with them
* see removeStoreKeyAssociation above
*/
const cleanUpObservableStores = () => {
const observableStore = getObservableStore(TRAM_OBSERVABLE_STORE);
const keyStore = getKeyStore(TRAM_KEY_STORE);
Object.entries(keyStore).forEach(([key, observers]) => {
if (observers === 0) {
delete observableStore[key];
delete keyStore[key];
}
});
};
/**
* unobserve the reaction tied to the node, and run all cleanup effects for the node
*/
const clearNode = (node) => {
// if this element doesn't have a TRAM_TAG, it's not a Tram-One Element
if (!(TRAM_TAG in node)) {
return;
}
unobserve(node[TRAM_TAG_REACTION]);
cleanupEffects(node[TRAM_TAG_CLEANUP_EFFECTS]);
removeStoreKeyAssociation(node[TRAM_TAG_STORE_KEYS]);
};
const isTramOneComponent = (node) => {
// a node is a component if it has `TRAM_TAG` key on it
const nodeIsATramOneComponent = TRAM_TAG in node;
// if it is a tram-one component, we want to process it, otherwise skip it
return nodeIsATramOneComponent ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
};
/**
* function to get the children (as a list) of the node passed in
*/
const childrenComponents = (node) => {
const componentWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT, isTramOneComponent);
const children = [];
while (componentWalker.nextNode()) {
children.push(componentWalker.currentNode);
}
return children;
};
const mutationObserverNamespaceConstructor = () => new MutationObserver((mutationList) => {
// cleanup orphaned nodes that are no longer on the DOM
const removedNodesInMutation = (mutation) => [...mutation.removedNodes];
const removedNodes = mutationList.flatMap(removedNodesInMutation);
const removedChildNodes = removedNodes.flatMap(childrenComponents);
removedChildNodes.forEach(clearNode);
// call new effects on any new nodes
const addedNodesInMutation = (mutation) => [...mutation.addedNodes];
const newNodes = mutationList.flatMap(addedNodesInMutation);
const newChildNodes = newNodes.flatMap(childrenComponents);
newChildNodes.forEach(processTramTags);
// clean up all local observable stores that have no observers
cleanUpObservableStores();
});
const { setup: setupMutationObserver, get: getMutationObserver } = buildNamespace(mutationObserverNamespaceConstructor);
// tell the mutation observer to watch the given node for changes
const startWatcher = (observerName, node) => {
const observerStore = getMutationObserver(observerName);
observerStore.observe(node, { childList: true, subtree: true });
};
/**
* @name start
* @link https://tram-one.io/#start
* @description
* Function to attach a component to an existing element on the page.
* This function also starts all the listeners and allows the basic hooks to function.
*
* This should only be called for the initial render / building of the app.
*
* @param component top-level component to attach to the page.
* @param target either a CSS selector, or Node to attach the component to
*/
var start = (component, target) => {
/* setup all the internal engines required for tram-one to work */
// get the container to mount the app on
const container = buildContainer(target);
// setup the window object to hold stores and queues
// in the future, we may allow this to be customized
// for multiple, sandboxed, instances of Tram-One
setupTramOneSpace();
// setup store for effects
setupEffectStore(TRAM_EFFECT_STORE);
// setup queue for new effects when resolving mounts
setupEffectStore(TRAM_EFFECT_QUEUE);
// setup working key for hooks
setupWorkingKey(TRAM_HOOK_KEY);
// setup observable store for the useStore and useGlobalStore hooks
setupObservableStore(TRAM_OBSERVABLE_STORE);
// setup key store for keeping track of stores to clean up
setupKeyStore(TRAM_KEY_STORE);
// setup key queue for new observable stores when resolving mounts
setupKeyQueue(TRAM_KEY_QUEUE);
// setup key queue for global observable stores when resolving mounts
setupKeyQueue(TRAM_GLOBAL_KEY_QUEUE);
// setup a mutation observer for cleaning up removed elements and triggering effects
setupMutationObserver(TRAM_MUTATION_OBSERVER);
// watch for changes on the target so that we can process node changes
startWatcher(TRAM_MUTATION_OBSERVER, container);
// trigger an initial mount
mount(component, container);
};
exports.registerHtml = registerHtml;
exports.registerSvg = registerSvg;
exports.start = start;
exports.useEffect = useEffect;
exports.useGlobalStore = useGlobalStore;
exports.useStore = useStore;
exports.useUrlParams = useUrlParams$1;
//# sourceMappingURL=tram-one.cjs.map