+ If you want to import components from an external source, or would like to break up your component definitions into
+ separate files, you can use the provided import-components.js in a script tag. You can even do this in
+ projects that don't otherwise import Tram-Lite!
+
+
+
+
+
+
+
+ The script depends on an attribute tl-components, which is a space separated list of paths to the
+ components you'd like to import.
+
+
Javascript API
If you only want access to the Javascript API, or are using tram-lite in a non-browser environment, you can pull just
diff --git a/examples/hybrid/index.html b/examples/hybrid/index.html
new file mode 100644
index 0000000..02da9e0
--- /dev/null
+++ b/examples/hybrid/index.html
@@ -0,0 +1,81 @@
+
+
+
+ Tram-Lite Example Components
+
+
+
+
+
+
+
+
+
+
+
+ =
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 442374e..5191f08 100644
--- a/package.json
+++ b/package.json
@@ -15,9 +15,9 @@
"build": "npm run bundle && npm run set && npm run minify",
"bundle": "uglifyjs src/TramLite.js src/processors/*.js -o output/bundle-step.js --define APP_VERSION=\"'4.1.1'\" -b --comments all",
"set": "npm run set:api && npm run set:tram-lite && npm run set:import-component",
- "set:api": "uglifyjs output/bundle-step.js -o output/api.js --define MODULE=\"true\" -b --comments all ",
- "set:tram-lite": "uglifyjs output/bundle-step.js -o output/tram-lite.js --define MODULE=\"false\" -b --comments all ",
- "set:import-component": "uglifyjs output/bundle-step.js src/import-component.js -o output/import-component.js --define MODULE=\"true\" -b --comments all ",
+ "set:api": "uglifyjs output/bundle-step.js -o output/api.js --define MODULE=\"true\" --define INSTALL=\"false\" -b --comments all ",
+ "set:tram-lite": "uglifyjs output/bundle-step.js -o output/tram-lite.js --define MODULE=\"false\" --define INSTALL=\"true\" -b --comments all ",
+ "set:import-component": "uglifyjs output/bundle-step.js src/import-component.js -o output/import-component.js --define MODULE=\"false\" --define INSTALL=\"false\" -b --comments all -e",
"minify": "npm run minify:api && npm run minify:tram-lite && npm run minify:import-component",
"minify:api": "uglifyjs output/api.js -o output/api.min.js -c -m",
"minify:tram-lite": "uglifyjs output/tram-lite.js -o output/tram-lite.min.js -c -m",
diff --git a/src/TramLite.js b/src/TramLite.js
index 989cfbd..4a1c804 100644
--- a/src/TramLite.js
+++ b/src/TramLite.js
@@ -218,7 +218,8 @@ if (MODULE === true) {
if (typeof module !== 'undefined') {
module.exports = TramLite;
}
-} else {
+}
+if (INSTALL === true) {
// if this is a script tag, note that we've installed Tram-Lite listeners
TramLite.installed = true;
}
diff --git a/src/processors/ComponentDefinition.js b/src/processors/ComponentDefinition.js
index 26942f2..9ec3402 100644
--- a/src/processors/ComponentDefinition.js
+++ b/src/processors/ComponentDefinition.js
@@ -134,7 +134,8 @@ if (MODULE === true) {
if (typeof module !== 'undefined') {
module.exports.ComponentDefinition = ComponentDefinition;
}
-} else {
+}
+if (INSTALL === true) {
// setup mutation observer so that template elements created will automatically be defined
ComponentDefinition.setupMutationObserverForTemplates();
}
diff --git a/src/processors/ComponentEffect.js b/src/processors/ComponentEffect.js
index 85eabe9..e3d1574 100644
--- a/src/processors/ComponentEffect.js
+++ b/src/processors/ComponentEffect.js
@@ -65,7 +65,8 @@ if (MODULE === true) {
if (typeof module !== 'undefined') {
module.exports.ComponentEffect = ComponentEffect;
}
-} else {
+}
+if (INSTALL === true) {
// setup shadow root processor so that tl-effects that are added are processed correctly
TramLite.appendShadowRootProcessor('[tl-effect]', ComponentEffect);
}
diff --git a/src/processors/ControlledInput.js b/src/processors/ControlledInput.js
index 2b2945a..8ac2b06 100644
--- a/src/processors/ControlledInput.js
+++ b/src/processors/ControlledInput.js
@@ -40,7 +40,8 @@ if (MODULE === true) {
if (typeof module !== 'undefined') {
module.exports.ControlledInput = ControlledInput;
}
-} else {
+}
+if (INSTALL === true) {
// setup shadow root processor so that tl-controlled that are added are processed correctly
TramLite.appendShadowRootProcessor('[tl-controlled]', ControlledInput);
}
From 8ae56c4b7b24a7a2b7eb5befbe118768f4a03ff4 Mon Sep 17 00:00:00 2001
From: Jesse Jurman
Date: Sat, 7 Oct 2023 00:36:15 -0400
Subject: [PATCH 06/30] simplify build tooling
---
build.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++
package.json | 11 +--------
2 files changed, 70 insertions(+), 10 deletions(-)
create mode 100644 build.js
diff --git a/build.js b/build.js
new file mode 100644
index 0000000..4573084
--- /dev/null
+++ b/build.js
@@ -0,0 +1,69 @@
+const fs = require('fs');
+const UglifyJS = require('uglify-js');
+const { version } = require('./package.json');
+
+// load all files
+const classFiles = ['src/TramLite.js', ...fs.readdirSync('src/processors').map((file) => `src/processors/${file}`)];
+const loadedClassFiles = Object.fromEntries(
+ classFiles.map((filePath) => {
+ console.log('loading', filePath);
+ return [filePath, fs.readFileSync(filePath).toString()];
+ }),
+);
+
+console.log('loading', 'src/import-component.js');
+const importComponent = { 'src/import-component.js': fs.readFileSync('src/import-component.js').toString() };
+
+// Set configurations
+const setConfigs = [
+ {
+ outputFile: 'output/api.js',
+ files: loadedClassFiles,
+ defines: { MODULE: true, INSTALL: false },
+ },
+ {
+ outputFile: 'output/tram-lite.js',
+ files: loadedClassFiles,
+ defines: { MODULE: false, INSTALL: true },
+ },
+ {
+ outputFile: 'output/import-component.js',
+ files: { ...loadedClassFiles, ...importComponent },
+ defines: { MODULE: false, INSTALL: false },
+ enclose: true,
+ },
+];
+
+setConfigs.forEach((config) => {
+ console.log('building', config.outputFile);
+ const options = {
+ compress: {
+ global_defs: {
+ APP_VERSION: version,
+ ...config.defines,
+ },
+ },
+ output: {
+ comments: 'all',
+ beautify: true,
+ wrap_iife: config.enclose,
+ },
+ };
+ const result = UglifyJS.minify(config.files, options);
+ fs.writeFileSync(config.outputFile, result.code);
+});
+
+// Minify
+const minifyConfigs = [
+ { inputFile: 'output/api.js', outputFile: 'output/api.min.js' },
+ { inputFile: 'output/tram-lite.js', outputFile: 'output/tram-lite.min.js' },
+ { inputFile: 'output/import-component.js', outputFile: 'output/import-component.min.js' },
+];
+
+minifyConfigs.forEach((config) => {
+ console.log('minifying', config.outputFile);
+ const result = UglifyJS.minify(fs.readFileSync(config.inputFile, 'utf8'));
+ fs.writeFileSync(config.outputFile, result.code);
+});
+
+console.log('Build complete!');
diff --git a/package.json b/package.json
index 5191f08..1d4fa57 100644
--- a/package.json
+++ b/package.json
@@ -12,16 +12,7 @@
"scripts": {
"prestart": "npm run build",
"start": "serve .",
- "build": "npm run bundle && npm run set && npm run minify",
- "bundle": "uglifyjs src/TramLite.js src/processors/*.js -o output/bundle-step.js --define APP_VERSION=\"'4.1.1'\" -b --comments all",
- "set": "npm run set:api && npm run set:tram-lite && npm run set:import-component",
- "set:api": "uglifyjs output/bundle-step.js -o output/api.js --define MODULE=\"true\" --define INSTALL=\"false\" -b --comments all ",
- "set:tram-lite": "uglifyjs output/bundle-step.js -o output/tram-lite.js --define MODULE=\"false\" --define INSTALL=\"true\" -b --comments all ",
- "set:import-component": "uglifyjs output/bundle-step.js src/import-component.js -o output/import-component.js --define MODULE=\"false\" --define INSTALL=\"false\" -b --comments all -e",
- "minify": "npm run minify:api && npm run minify:tram-lite && npm run minify:import-component",
- "minify:api": "uglifyjs output/api.js -o output/api.min.js -c -m",
- "minify:tram-lite": "uglifyjs output/tram-lite.js -o output/tram-lite.min.js -c -m",
- "minify:import-component": "uglifyjs output/import-component.js -o output/import-component.min.js -c -m",
+ "build": "node build.js",
"prepublishOnly": "npm run build",
"pretest": "npm run build",
"test": "cypress open",
From 6a2a19da64d4b71d30349ce9cfa58a9ff9feb00b Mon Sep 17 00:00:00 2001
From: Jesse Jurman
Date: Sat, 7 Oct 2023 00:56:39 -0400
Subject: [PATCH 07/30] enclose
---
build.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build.js b/build.js
index 4573084..8a97a1c 100644
--- a/build.js
+++ b/build.js
@@ -43,10 +43,10 @@ setConfigs.forEach((config) => {
...config.defines,
},
},
+ enclose: config.enclose,
output: {
comments: 'all',
beautify: true,
- wrap_iife: config.enclose,
},
};
const result = UglifyJS.minify(config.files, options);
From 0d8e7473411e8a0d5f3c3e9ef6b9bb336adb378d Mon Sep 17 00:00:00 2001
From: Jesse Jurman
Date: Sat, 7 Oct 2023 16:10:08 -0400
Subject: [PATCH 08/30] export-script logic
---
build.js | 25 +-
cypress/export.cy.js | 29 ++
cypress/hybrid.cy.js | 25 +-
cypress/import.cy.js | 31 +-
docs/index.html | 4 +-
examples/components/build-test-components.sh | 2 +
examples/components/ex-colorpicker.html | 15 +
examples/components/ex-colorpicker.js | 379 ++++++++++++++
.../ex-container.html} | 4 +-
.../ex-progressbar.html} | 4 +-
.../ex-temperature.html} | 4 +-
examples/components/example-components.js | 474 ++++++++++++++++++
examples/export/index.html | 31 ++
examples/hybrid/index.html | 16 +-
examples/import/index.html | 17 +-
examples/inline/index.html | 28 +-
package.json | 3 +-
src/ImportComponent.js | 27 +
src/import-component.js | 36 --
src/scripts/export-script.js | 54 ++
src/scripts/import-script.js | 18 +
21 files changed, 1135 insertions(+), 91 deletions(-)
create mode 100644 cypress/export.cy.js
create mode 100644 examples/components/build-test-components.sh
create mode 100644 examples/components/ex-colorpicker.html
create mode 100644 examples/components/ex-colorpicker.js
rename examples/{import/example-container.html => components/ex-container.html} (80%)
rename examples/{import/im-progressbar.html => components/ex-progressbar.html} (91%)
rename examples/{import/im-temperature.html => components/ex-temperature.html} (96%)
create mode 100644 examples/components/example-components.js
create mode 100644 examples/export/index.html
create mode 100644 src/ImportComponent.js
delete mode 100644 src/import-component.js
create mode 100644 src/scripts/export-script.js
create mode 100644 src/scripts/import-script.js
diff --git a/build.js b/build.js
index 8a97a1c..28d0f19 100644
--- a/build.js
+++ b/build.js
@@ -12,7 +12,17 @@ const loadedClassFiles = Object.fromEntries(
);
console.log('loading', 'src/import-component.js');
-const importComponent = { 'src/import-component.js': fs.readFileSync('src/import-component.js').toString() };
+const importComponentClass = {
+ 'src/ImportComponent.js': fs.readFileSync('src/ImportComponent.js').toString(),
+};
+console.log('loading', 'src/import-script.js');
+const importScript = {
+ 'src/scripts/import-script.js': fs.readFileSync('src/scripts/import-script.js').toString(),
+};
+console.log('loading', 'src/export-script.js');
+const exportScript = {
+ 'src/scripts/export-script.js': fs.readFileSync('src/scripts/export-script.js').toString(),
+};
// Set configurations
const setConfigs = [
@@ -28,10 +38,20 @@ const setConfigs = [
},
{
outputFile: 'output/import-component.js',
- files: { ...loadedClassFiles, ...importComponent },
+ files: { ...loadedClassFiles, ...importComponentClass, ...importScript },
defines: { MODULE: false, INSTALL: false },
enclose: true,
},
+ {
+ outputFile: 'output/export-dependencies.js',
+ files: { ...loadedClassFiles, ...importComponentClass },
+ defines: { MODULE: false, INSTALL: false },
+ },
+ {
+ outputFile: 'output/export-script.js',
+ files: { ...exportScript },
+ defines: { MODULE: false, INSTALL: false },
+ },
];
setConfigs.forEach((config) => {
@@ -58,6 +78,7 @@ const minifyConfigs = [
{ inputFile: 'output/api.js', outputFile: 'output/api.min.js' },
{ inputFile: 'output/tram-lite.js', outputFile: 'output/tram-lite.min.js' },
{ inputFile: 'output/import-component.js', outputFile: 'output/import-component.min.js' },
+ { inputFile: 'output/export-dependencies.js', outputFile: 'output/export-dependencies.min.js' },
];
minifyConfigs.forEach((config) => {
diff --git a/cypress/export.cy.js b/cypress/export.cy.js
new file mode 100644
index 0000000..33671b7
--- /dev/null
+++ b/cypress/export.cy.js
@@ -0,0 +1,29 @@
+describe('Tram-Lite Example Components (via export)', () => {
+ it('should validate Tram-Lite APIs and Use Cases when exporting components', () => {
+ // visit index.html
+ cy.visit('../examples/export/index.html');
+
+ /* copied from inline tests */
+ cy.get('ex-temperature').get('input#f').type('19');
+ cy.get('ex-temperature').get('input#c').should('have.value', '-7');
+ cy.get('ex-temperature').get('input#f').should('have.value', '19');
+
+ /* copied from inline tests */
+ cy.get('ex-progressbar').should('not.have.attr', 'warning');
+ cy.get('ex-progressbar').get('input#value').clear().type('12');
+ cy.get('ex-progressbar').should('have.attr', 'warning');
+ cy.get('ex-progressbar').get('input#max').clear().type('15');
+ cy.get('ex-progressbar').should('not.have.attr', 'warning');
+
+ /* copied from inline tests */
+ cy.get('ex-colorpicker').invoke('attr', 'hue', '120');
+ cy.get('ex-colorpicker').get('input#hue-range-input').should('have.value', '120');
+ cy.get('ex-colorpicker').get('input#hue-text-input').should('have.value', '120');
+ cy.get('ex-colorpicker')
+ .get('rect')
+ .then(($element) => {
+ const rawColor = window.getComputedStyle($element[0])['fill'];
+ expect(rawColor).to.equal('oklch(0.7 0.1 120)');
+ });
+ });
+});
diff --git a/cypress/hybrid.cy.js b/cypress/hybrid.cy.js
index 6adf513..514df2c 100644
--- a/cypress/hybrid.cy.js
+++ b/cypress/hybrid.cy.js
@@ -3,16 +3,27 @@ describe('Tram-Lite Example Components (via import and inline)', () => {
// visit index.html
cy.visit('../examples/hybrid/index.html');
- /* verify that component effects trigger on dependency updates */
+ /* copied from inline tests, verify inline defined element */
cy.get('in-temperature').get('input#f').type('19');
cy.get('in-temperature').get('input#c').should('have.value', '-7');
cy.get('in-temperature').get('input#f').should('have.value', '19');
- /* verify that an element with multiple dependencies triggers on either dependency */
- cy.get('im-progressbar').should('not.have.attr', 'warning');
- cy.get('im-progressbar').get('input#value').clear().type('12');
- cy.get('im-progressbar').should('have.attr', 'warning');
- cy.get('im-progressbar').get('input#max').clear().type('15');
- cy.get('im-progressbar').should('not.have.attr', 'warning');
+ /* copied from inline tests, verify the external component imported works */
+ cy.get('ex-progressbar').should('not.have.attr', 'warning');
+ cy.get('ex-progressbar').get('input#value').clear().type('12');
+ cy.get('ex-progressbar').should('have.attr', 'warning');
+ cy.get('ex-progressbar').get('input#max').clear().type('15');
+ cy.get('ex-progressbar').should('not.have.attr', 'warning');
+
+ /* copied from inline tests, verify that external component exported works */
+ cy.get('ex-colorpicker').invoke('attr', 'hue', '120');
+ cy.get('ex-colorpicker').get('input#hue-range-input').should('have.value', '120');
+ cy.get('ex-colorpicker').get('input#hue-text-input').should('have.value', '120');
+ cy.get('ex-colorpicker')
+ .get('rect')
+ .then(($element) => {
+ const rawColor = window.getComputedStyle($element[0])['fill'];
+ expect(rawColor).to.equal('oklch(0.7 0.1 120)');
+ });
});
});
diff --git a/cypress/import.cy.js b/cypress/import.cy.js
index 25b56b3..7280325 100644
--- a/cypress/import.cy.js
+++ b/cypress/import.cy.js
@@ -3,16 +3,27 @@ describe('Tram-Lite Example Components (via import)', () => {
// visit index.html
cy.visit('../examples/import/index.html');
- /* verify that component effects trigger on dependency updates */
- cy.get('im-temperature').get('input#f').type('19');
- cy.get('im-temperature').get('input#c').should('have.value', '-7');
- cy.get('im-temperature').get('input#f').should('have.value', '19');
+ /* copied from inline tests */
+ cy.get('ex-temperature').get('input#f').type('19');
+ cy.get('ex-temperature').get('input#c').should('have.value', '-7');
+ cy.get('ex-temperature').get('input#f').should('have.value', '19');
- /* verify that an element with multiple dependencies triggers on either dependency */
- cy.get('im-progressbar').should('not.have.attr', 'warning');
- cy.get('im-progressbar').get('input#value').clear().type('12');
- cy.get('im-progressbar').should('have.attr', 'warning');
- cy.get('im-progressbar').get('input#max').clear().type('15');
- cy.get('im-progressbar').should('not.have.attr', 'warning');
+ /* copied from inline tests */
+ cy.get('ex-progressbar').should('not.have.attr', 'warning');
+ cy.get('ex-progressbar').get('input#value').clear().type('12');
+ cy.get('ex-progressbar').should('have.attr', 'warning');
+ cy.get('ex-progressbar').get('input#max').clear().type('15');
+ cy.get('ex-progressbar').should('not.have.attr', 'warning');
+
+ /* copied from inline tests */
+ cy.get('ex-colorpicker').invoke('attr', 'hue', '120');
+ cy.get('ex-colorpicker').get('input#hue-range-input').should('have.value', '120');
+ cy.get('ex-colorpicker').get('input#hue-text-input').should('have.value', '120');
+ cy.get('ex-colorpicker')
+ .get('rect')
+ .then(($element) => {
+ const rawColor = window.getComputedStyle($element[0])['fill'];
+ expect(rawColor).to.equal('oklch(0.7 0.1 120)');
+ });
});
});
diff --git a/docs/index.html b/docs/index.html
index 36d44db..f94ee86 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -47,8 +47,8 @@
diff --git a/examples/components/build-test-components.sh b/examples/components/build-test-components.sh
new file mode 100644
index 0000000..8f97acb
--- /dev/null
+++ b/examples/components/build-test-components.sh
@@ -0,0 +1,2 @@
+node output/export-script.js examples/components/ex-progressbar.html examples/components/ex-temperature.html examples/components/ex-container.html examples/components/ex-colorpicker.html -o examples/components/example-components.js
+node output/export-script.js examples/components/ex-colorpicker.html -o examples/components/ex-colorpicker.js
diff --git a/examples/components/ex-colorpicker.html b/examples/components/ex-colorpicker.html
new file mode 100644
index 0000000..966ff53
--- /dev/null
+++ b/examples/components/ex-colorpicker.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/examples/components/ex-colorpicker.js b/examples/components/ex-colorpicker.js
new file mode 100644
index 0000000..f7cbf50
--- /dev/null
+++ b/examples/components/ex-colorpicker.js
@@ -0,0 +1,379 @@
+{
+class TramLite {
+ static version = "4.2.0";
+ static installed = !1;
+ /**
+ * utility function to build the component class from the template string
+ * (this is an underlying utility for the define function)
+ */
+ static makeComponentClass(t, ...e) {
+ var a = document.createElement("template"), o = e.map(t => `tl:${t}:`);
+ // tag our templateVariables, so we know how to look for them in the dom
+ a.innerHTML = String.raw({
+ raw: t
+ }, ...o);
+ const i = a.content.firstElementChild, r = {};
+ // any attributes on the root are considered default values
+ [ ...i.attributes ].forEach(t => {
+ r[t.name] = t.value;
+ }),
+ // if there are any component-effects that aren't already on hold, hold them now
+ // (we don't want them triggering before the component has been completely defined)
+ // if there is already a hold, we won't touch these elements
+ // (the developer may want to defer processing until later)
+ i.querySelectorAll("script[tl-effect]:not([tl-hold])").forEach(t => {
+ t.setAttribute("tl-hold", "component-mount");
+ });
+ // Custom element class with tram-lite template support.
+ class n extends HTMLElement {
+ static tramLiteVersion = "4.2.0";
+ static tagName = i.tagName.toLowerCase();
+ static get observedAttributes() {
+ // all of the template variables are attributes that we'll update on
+ return e;
+ }
+ constructor() {
+ super(),
+ // list of attribute and text nodes that have a template value
+ // these are scanned through when templated attributes are updated
+ this.templateValuesAttrNodes = [], this.templateValuesTextNodes = [];
+ // Create a shadow root
+ // and append our HTML to it
+ var t = this.attachShadow({
+ mode: "open"
+ });
+ t.append(...i.cloneNode(!0).childNodes),
+ // save the original template in templateValuesTextNodes
+ ComponentDefinition.getTextNodesWithTramLiteValues(t).forEach(t => {
+ this.templateValuesTextNodes.push({
+ textNode: t,
+ originalTemplate: t.textContent
+ });
+ }),
+ // save the original attribute in the templateValuesAttributes
+ ComponentDefinition.getElementsWithTramLiteValuesInAttributes(t).forEach(e => {
+ [ ...e.attributes ].forEach(t => {
+ t.value.match(/tl:(.+?):/) && this.templateValuesAttrNodes.push({
+ attrNode: t,
+ element: e,
+ originalTemplate: t.value
+ });
+ });
+ });
+ }
+ connectedCallback() {
+ // set all attribute values
+ // - the first default value is whatever is set on DOM creation
+ // - next, we check if there are default values that were part of the define
+ // - lastly, we'll set it to an empty string.
+ var t = Object.keys(r);
+ [ ...e, ...t ].forEach(t => {
+ null === this.getAttribute(t) && this.setAttribute(t, r[t] || "");
+ }),
+ // an initial call to set the default attributes
+ this.attributeChangedCallback(),
+ // if there were any scripts that were waiting to be triggered on component mount, trigger them now
+ this.shadowRoot.querySelectorAll('script[tl-hold="component-mount"]').forEach(t => {
+ t.removeAttribute("tl-hold");
+ });
+ }
+ attributeChangedCallback(t, e, a) {
+ // scan through all text nodes and attributes with template values, and update them
+ this.updateTextNodeTemplates(), this.updateAttrNodeTemplates();
+ }
+ getUpdatedTemplate(t) {
+ let a = t;
+ return e.forEach(t => {
+ // fallback on the default values or an empty string if there is no value for this attribute yet
+ var e = this.getAttribute(t) || r[t] || "";
+ a = a.replaceAll(`tl:${t}:`, e);
+ }), a;
+ }
+ updateTextNodeTemplates() {
+ // go through each text node that has a template variable, and update them
+ this.templateValuesTextNodes.forEach(({
+ textNode: t,
+ originalTemplate: e
+ }) => {
+ t.textContent = this.getUpdatedTemplate(e);
+ });
+ }
+ updateAttrNodeTemplates() {
+ // go through each element with an attribute that has a template variable, and update those attribute values
+ this.templateValuesAttrNodes.forEach(({
+ attrNode: t,
+ element: e,
+ originalTemplate: a
+ }) => {
+ // set the attribute value to the new value (updated with all template variables)
+ t.value = this.getUpdatedTemplate(a),
+ // these attributes are special, in order to update the live value (after a user has interacted with them),
+ // they need to be set on the element as well
+ [ "value", "checked", "selected" ].includes(t.name) && (e[t.name] = this.getUpdatedTemplate(a));
+ });
+ }
+ }
+ return n;
+ }
+ /**
+ * a template tag function used to create new web-components.
+ * {@link https://tram-one.io/tram-lite/#define Read the full docs here.}
+ */
+ static define(t, ...e) {
+ // build the new component class from the template
+ t = TramLite.makeComponentClass(t, ...e);
+ // register this as a new element as a native web-component
+ return customElements.define(t.tagName, t), t;
+ }
+ /**
+ * a helper function to update the root web-component when an input updates
+ * {@link https://tram-one.io/tram-lite/#updateRootAttr Read the full docs here.}
+ * @param {string} attributeName
+ * @param {Event} event
+ * @param {string} [targetAttribute="value"]
+ */
+ static updateRootAttr(t, e, a = "value") {
+ var o = e.target.getRootNode().host;
+ e.target[a] ? o.setAttribute(t, e.target[a]) : o.removeAttribute(t);
+ }
+ /**
+ * helper function to set up a callback for when an element's attribute changes
+ * {@link https://tram-one.io/tram-lite/#addAttributeListener Read the full docs here.}
+ * @param {Element} targetElement - The DOM element to observe.
+ * @param {string[]} attributeNames - The name of the attribute (or list of attributes) to observe for changes.
+ * @param {function(MutationRecord):void} callback - The function to call when the observed attribute changes.
+ * This function takes one argument: the MutationRecord representing the change.
+ */
+ static addAttributeListener(e, t, a) {
+ new MutationObserver(t => {
+ t.forEach(t => {
+ // only call the mutation if an attribute changed
+ t.oldValue !== e.getAttribute(t.attributeName) && a(t);
+ });
+ }).observe(e, {
+ attributes: !0,
+ attributeFilter: t,
+ attributeOldValue: !0
+ });
+ }
+ /**
+ * function to append new behaviors to elements that are attached to the shadowDOM.
+ * {@link https://tram-one.io/tram-lite/#appendShadowRootProcessor Read the full docs here.}
+ * @param {string} matcher
+ * @param {{ connect: function }} componentClass
+ * @param {ShadowRoot} [shadowRoot=ShadowRoot.prototype]
+ */
+ static appendShadowRootProcessor(e, a, t = ShadowRoot.prototype) {
+ // save the original version of shadowRoot.append
+ const o = t.append;
+ t.append = function(...t) {
+ o.call(this, ...t),
+ // if any element in this shadowRoot matches our matcher,
+ // run the `connect` function from this class
+ this.querySelectorAll(e).forEach(t => {
+ t.getRootNode().host && a.connect(t);
+ });
+ };
+ }
+}
+
+/**
+ * ComponentDefinition is a custom element that extends the template element, that allows developers
+ * to build new web-components, using Tram-Lite, all in their HTML templates!
+ *
+ * {@link https://tram-one.io/tram-lite/#tl-definition}
+ */
+class ComponentDefinition {
+ // regex for finding attributes that have been templated in
+ static templateVariableRegex = /tl:(.+?):/;
+ /**
+ * function to test if node has an attribute value with a template variable
+ * e.g.
+ */
+ static nodeHasTramLiteAttr = t => [ ...t.attributes ].some(t => t.value.match(ComponentDefinition.templateVariableRegex)) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
+ /**
+ * function to test if node has an TEXT node with a template variable
+ * e.g. Hello ${'name'}
+ */
+ static nodeHasTextElementWithTramLiteAttr = t => t.textContent.match(ComponentDefinition.templateVariableRegex) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
+ /**
+ * generic function to build a tree walker, and use the filter + tram-lite matcher.
+ * this should return all elements that match the criteria
+ */
+ static buildTreeWalkerTramLiteMatcher(t, e, a) {
+ for (var o, i = [], r = document.createTreeWalker(t, e, {
+ acceptNode: a
+ })
+ // build a tree walker that goes through each element, and each attribute
+ ; o = r.nextNode(); ) i.push(o);
+ return i;
+ }
+ // Returns elements with attributes containing tram-lite template variables.
+ static getElementsWithTramLiteValuesInAttributes(t) {
+ return ComponentDefinition.buildTreeWalkerTramLiteMatcher(t, NodeFilter.SHOW_ELEMENT, ComponentDefinition.nodeHasTramLiteAttr);
+ }
+ // Returns text nodes containing tram-lite template variables.
+ static getTextNodesWithTramLiteValues(t) {
+ return ComponentDefinition.buildTreeWalkerTramLiteMatcher(t, NodeFilter.SHOW_TEXT, ComponentDefinition.nodeHasTextElementWithTramLiteAttr);
+ }
+ /**
+ * static function to process template tags and define components
+ * @param {HTMLTemplateElement} templateTag
+ */
+ static processTemplateDefinition(t) {
+ [ ...t.content.children ].forEach(t => {
+ var t = t.outerHTML.split(/\$\{\'(.*?)\'\}/), e = t.filter((t, e) => e % 2 == 0), t = t.filter((t, e) => e % 2 != 0);
+ // we expect template variables to be in the following pattern, matching "${'...'}"
+ TramLite.define(e, ...t);
+ });
+ }
+ /**
+ * utility function to extract js template strings, so that they can be passed into a template tag function
+ */
+ static extractTemplateVariables(t) {
+ // we expect template variables to be in the following pattern, matching "${'...'}"
+ t = t.split(/\$\{\'(.*?)\'\}/);
+ // Split the string by the above pattern, which lets us get an alternating list of strings and variables
+ return [ t.filter((t, e) => e % 2 == 0), t.filter((t, e) => e % 2 != 0) ];
+ }
+ /**
+ * function to set up an observer to watch for when new templates are added,
+ * and process all the definitions in them
+ * @param {Document} [targetRoot=document]
+ */
+ static setupMutationObserverForTemplates(t = document) {
+ new MutationObserver(t => {
+ t.forEach(t => {
+ t.addedNodes.forEach(t => {
+ // check if the previous element is a definition template
+ // we wait until we are in the next element (most likely a #text node)
+ // because that will confirm that the element has been completely parsed
+ t.previousSibling?.matches?.("[tl-definition]") && ComponentDefinition.processTemplateDefinition(t.previousSibling);
+ });
+ });
+ }).observe(document, {
+ subtree: !0,
+ childList: !0
+ });
+ }
+}
+
+/**
+ * ComponentEffect is a class that can extend the script element, that allows developers
+ * to build side-effects for web-components.
+ *
+ * {@link https://tram-one.io/tram-lite/#tl-effect}
+ */
+class ComponentEffect {
+ /**
+ * function to trigger the javascript in a script tag.
+ * This does not handle the src attribute, only inline javascript.
+ * The `this` keyword will reference the host parent node of this script tag.
+ * @param {HTMLScriptElement} scriptTag
+ */
+ static processScriptTag(t) {
+ // don't do this if we have a hold on the script tag
+ if (!t.hasAttribute("tl-hold")) {
+ const e = t.getRootNode().host;
+ // provide a scoped evaluation of the script tags in this element
+ t = t.innerHTML, Function("document", "window", t).bind(e)(e.shadowRoot, window);
+ }
+ }
+ /**
+ * connect function for ComponentEffect - when this is run on a script,
+ * we trigger that script with the host element as context, and set up an
+ * observer if a set of dependencies are defined and ever update
+ * @param {HTMLScriptElement} newNode
+ */
+ static connect(t) {
+ var e, a = t.getRootNode().host;
+ // run an initial run of the script
+ // (this won't happen if there is a tl-hold on the script)
+ ComponentEffect.processScriptTag(t),
+ // if we have any dependencies, add a listener to trigger them
+ t.hasAttribute("tl-dependencies") && !0 !== t.hasSetupListener && (e = t.getAttribute("tl-dependencies").split(" "),
+ TramLite.addAttributeListener(a, e, () => {
+ // check if the inline script is being held
+ ComponentEffect.processScriptTag(t);
+ }), t.hasSetupListener = !0),
+ // if we ever set (or remove) the hold on this, trigger the inline script
+ // (this allows developers to delay triggering inline scripts)
+ TramLite.addAttributeListener(t, [ "tl-hold" ], () => {
+ ComponentEffect.processScriptTag(t);
+ });
+ }
+}
+
+/**
+ * ControlledInput is a class that can extend the input or script element, that allows developers
+ * to build 2-way data-binding for web-components.
+ *
+ * {@link https://tram-one.io/tram-lite/#tl-effect}
+ */
+class ControlledInput {
+ /**
+ * connect function for ControlledInput - when this is run on an input (or other similar control),
+ * we set up a 2-way data binding from the input to the host element.
+ * @param {HTMLInputElement} newNode
+ */
+ static connect(e) {
+ // attributes that control the behavior of the controlled input
+ var t = (e.getAttribute("tl-trigger") || "change").split(" ");
+ const a = e.getAttribute("tl-hostattr") || "value", o = e.getAttribute("tl-targetattr") || "value", i = e.getRootNode().host;
+ e[o] = i.getAttribute(a),
+ // update this input whenever the host attribute updates
+ TramLite.addAttributeListener(i, [ a ], () => {
+ e[o] = i.getAttribute(a);
+ }),
+ // update the root component attribute whenever the value changes for this node updates
+ t.forEach(t => {
+ e.addEventListener(t, t => {
+ TramLite.updateRootAttr(a, t, o);
+ });
+ });
+ }
+}
+
+class ImportComponent {
+ /**
+ * utility function for importing and defining new components (outside of Tram-Lite being installed)
+ * @param {string} componentContent
+ */
+ static importNewComponent(t) {
+ var [ t, e ] = ComponentDefinition.extractTemplateVariables(t), t = TramLite.makeComponentClass(t, ...e);
+ // make a component class based on the template tag pieces
+ // (this is done, over define, so we can attach shadow root processors)
+ // override attachShadow so that we can add shadowRootProcessors
+ const a = t.prototype.attachShadow;
+ t.prototype.attachShadow = function(...t) {
+ t = a.call(this, ...t);
+ return TramLite.appendShadowRootProcessor("[tl-controlled]", ControlledInput, t),
+ TramLite.appendShadowRootProcessor("[tl-effect]", ComponentEffect, t),
+ t;
+ },
+ // define the component in the DOM
+ customElements.define(t.tagName, t);
+ }
+}
+
+{
+ const componentTemplate = `
+
+
+
+
+
+`;
+ ImportComponent.importNewComponent(componentTemplate)
+}
+
+}
\ No newline at end of file
diff --git a/examples/import/example-container.html b/examples/components/ex-container.html
similarity index 80%
rename from examples/import/example-container.html
rename to examples/components/ex-container.html
index 566f319..d9dde76 100644
--- a/examples/import/example-container.html
+++ b/examples/components/ex-container.html
@@ -1,4 +1,4 @@
-
+
+
+
+`;
+ ImportComponent.importNewComponent(componentTemplate)
+}
+
+
+{
+ const componentTemplate = `
+
+
+
+
+
+`;
+ ImportComponent.importNewComponent(componentTemplate)
+}
+
+}
\ No newline at end of file
diff --git a/examples/export/index.html b/examples/export/index.html
new file mode 100644
index 0000000..1272936
--- /dev/null
+++ b/examples/export/index.html
@@ -0,0 +1,31 @@
+
+
+
+ Tram-Lite Example Components
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/hybrid/index.html b/examples/hybrid/index.html
index 02da9e0..ad5e53f 100644
--- a/examples/hybrid/index.html
+++ b/examples/hybrid/index.html
@@ -18,8 +18,9 @@
+
@@ -71,11 +72,14 @@
-
+
-
-
-
-
+
+
+
+
+
+
+