diff --git a/.gitignore b/.gitignore
index d97ed1b..c9654a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,8 @@ output
# Dependency directory
node_modules
+
+# components that are generated as part of build-test-components.sh
+examples/components/ex-colorpicker.js
+examples/components/ex-todolist.js
+examples/components/example-components.js
diff --git a/build.js b/build.js
new file mode 100644
index 0000000..a845024
--- /dev/null
+++ b/build.js
@@ -0,0 +1,109 @@
+// This script contains all the steps for generating the output results.
+// This can be triggered by running `npm run build`
+//
+// A majority of this file uses the UglifyJS API
+// https://www.npmjs.com/package/uglify-js
+//
+const fs = require('fs');
+const path = require('path');
+const UglifyJS = require('uglify-js');
+const { version } = require('./package.json');
+
+// before we start anything, make sure the output directory exists and is empty
+if (!fs.existsSync('output')) {
+ // if it doesn't exist make it
+ fs.mkdirSync('output');
+} else {
+ // if it does, remove all files in the directory
+ const files = fs.readdirSync('output');
+ for (const file of files) {
+ const filePath = path.join('output', file);
+ fs.unlinkSync(filePath);
+ }
+}
+
+// load all source class files (these will be included in all builds)
+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()];
+ }),
+);
+
+// load all import/export scripts separately (these are only included in some builds)
+console.log('loading', 'src/import-components.js');
+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(),
+};
+
+// uglify parameters to change the result of each bundle.
+// `MODULE` and `INSTALL` are variables that can be found in the class files and
+// determine if we should attach listeners for a window or export the class for a JS API.
+// `enclose` determines if the code should be wrapped in an IIFE (which prevents
+// prevents class definitions from colliding).
+const buildConfigs = [
+ {
+ 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-components.js',
+ files: { ...loadedClassFiles, ...importComponentClass, ...importScript },
+ defines: { MODULE: false, INSTALL: false },
+ enclose: true,
+ },
+ {
+ outputFile: 'output/export-dependencies.js',
+ files: { ...loadedClassFiles, ...importComponentClass },
+ defines: { MODULE: false, INSTALL: false },
+ },
+];
+
+buildConfigs.forEach((config) => {
+ console.log('building', config.outputFile);
+ const options = {
+ compress: {
+ global_defs: {
+ APP_VERSION: version,
+ ...config.defines,
+ },
+ },
+ enclose: config.enclose,
+ output: {
+ comments: 'all',
+ beautify: true,
+ },
+ };
+ const result = UglifyJS.minify(config.files, options);
+ fs.writeFileSync(config.outputFile, result.code);
+});
+
+// for each of these, create a minified version
+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-components.js', outputFile: 'output/import-components.min.js' },
+ { inputFile: 'output/export-dependencies.js', outputFile: 'output/export-dependencies.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);
+});
+
+// do a simple copy for the export-script (needs no minification)
+fs.copyFileSync('src/scripts/export-script.js', 'output/export-components.js');
+
+console.log('Tram-Lite build complete!');
diff --git a/cypress/spec.cy.js b/cypress/spec.cy.js
deleted file mode 100644
index f8a1f48..0000000
--- a/cypress/spec.cy.js
+++ /dev/null
@@ -1,65 +0,0 @@
-describe('Tram-Lite Example Components', () => {
- // per Cypress best practices (https://docs.cypress.io/guides/references/best-practices#Creating-Tiny-Tests-With-A-Single-Assertion)
- // it is often better to run all tests together, rather than having unit-like tests... so we'll comment the intent of each test,
- // rather than doing a reset between each test. The results should still be just as obvious in the cypress runner!
- it('should validate all Tram-Lite APIs and Use Cases', () => {
- // visit index.html (this works because the test page doesn't need to be hosted to work!)
- cy.visit('../examples/index.html');
-
- /* validate that slot elements are rendered as expected in Tram-Lite */
- cy.get('ex-title').contains('Title');
- cy.get('ex-title').contains('Tram-Lite Components!');
-
- /* validate that the counter elements work as expected */
- cy.get('ex-counter#default').contains(/Green: 0/); // default values should populate
- cy.get('ex-counter#red').contains(/Red: 0/); // passed in values should populate
- cy.get('ex-counter#red').click(); // clicking a counter should increment
- cy.get('ex-counter#red').contains(/Red: 1/);
-
- /* verify that updating inputs updates attributes as expected (tl-controlled) */
- cy.get('ex-mirror').get('input#source').type('Hello, World');
- cy.get('ex-mirror').get('input#reflection').should('have.value', 'Hello, World');
- cy.get('ex-mirror').should('have.attr', 'value', 'Hello, World');
- cy.get('ex-mirror').should('have.attr', 'is-mirrored', '');
-
- /* verify that updating an attribute copies to multiple elements and attributes */
- 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)');
- });
-
- /* verify that startup scripts in component definitions trigger as expected */
- cy.get('ex-todoitem').contains('Example Initial Item');
- cy.get('ex-todoitem').contains('Learning Tram-Lite');
-
- /* verify that creating elements works as expected */
- cy.get('ex-todolist').get('form input').type('Cypress Test'); // create new todo item
- cy.get('ex-todolist').get('form').submit();
-
- cy.get('ex-todoitem').contains('Cypress Test'); // verify it exists
-
- cy.get('ex-todoitem').contains('Cypress Test').click(); // click it, and verify that the top label updates
- cy.get('ex-todolist').get('span').contains('(1/3)');
-
- /* verify that updating an input with a false value unsets the attribute value */
- cy.get('ex-todoitem').contains('Cypress Test').click();
- cy.get('ex-todolist').get('span').contains('(0/3)');
-
- /* verify that component effects trigger on dependency updates */
- 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('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');
- });
-});
diff --git a/docs/index.html b/docs/index.html
index cbd2ab7..292f0c0 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -44,6 +44,13 @@
+
+
@@ -78,6 +85,7 @@
InstallBuilding Components
+ Importing & ExportingHTML API
@@ -110,6 +118,7 @@
>
+
diff --git a/docs/pages/importing-and-exporting.html b/docs/pages/importing-and-exporting.html
new file mode 100644
index 0000000..d19522d
--- /dev/null
+++ b/docs/pages/importing-and-exporting.html
@@ -0,0 +1,120 @@
+
Importing And Exporting Components
+
+ If you are building a library, there are a few different options when it comes to sharing your components with other
+ developers. On this page, we document a few different options, and the reasons why you may choose one.
+
+
+ Regardless of how you choose to share your components, doing so is a great way to make components easily consumable
+ among other developers, without requiring Tram-Lite as a core dependency. All methods also allow you to have different
+ versions of Tram-Lite components in the same project.
+
+
+
Using import-components script
+
+ The import-components script is a great way to share raw HTML templates without requiring a build step as
+ part of your project. It also allows developers to selectively choose which elements they would like to import.
+
+
+
+
+
+
+
+ The script depends on an attribute tl-components, which is a space separated list of paths to the
+ components you'd like to import.
+
+
Parameters
+
+ The only parameter for import-components.js is the tl-components attribute. It is a space
+ delimited list of component definition paths. The components should be HTML, and just the definition of the
+ components, the same as the content inside of a tl-definition template tag.
+
+
Example Importing an HTML Template
+
+ For example, we might have the following x-button.html.
+
+
+
+
+
+
+
+
+ We could then import and immediately use this component in our HTML page using the
+ import-components.js script.
+
+
+
+
+
+
+
+
+
This script is also available as a minified script - simply point to import-components.min.js.
+
+
Using export-components command
+
+ The export-components CLI tool is a great way to build native javascript if consumers of the library are
+ using tools to bundle their code, or if you (or your consumers) have a build step. It's also great because it works
+ with the native script import on the consumer's side.
+
+
+
+
+
+
+
+ You can pass in any html files you'd like to be bundled, and it will create a javascript file that people can natively
+ import with a script tag.
+
+
Parameters
+
+
+
+
+
+
+ Aside from the required components, the command has two optional flags, --output and
+ --minified.
+
+
+ --output (or -o) can be used to set the file name and directory of the resulting javascript.
+ If this flag is missing, the command will place the file in the current directory, named based on the component files
+ passed in.
+
+
+ --minified (or -m) can be used to import the minified Tram-Lite code as part of your export.
+ This should reduce the total size of the exported components.
+
+
Example Exporting an HTML Template to Javascript
+
+ Similar to the example above, we start with a component definition in an x-button.html.
+
+
+
+
+
+
+
+
+ Then we can run the command to export this component to javascript.
+
+
+
+
+
+
+ This will create an x-button.js file locally. We can then import that file using a normal script tag.
+
+
+
+
+
+
+
+ If you want to import components from an external source, or would like to break up your component definitions into
+ separate files, check out the documentation on
+ Importing & Exporting Components.
+
+
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/docs/pages/js-appendShadowRootProcessor.html b/docs/pages/js-appendShadowRootProcessor.html
index bd5a3d3..aa056f7 100644
--- a/docs/pages/js-appendShadowRootProcessor.html
+++ b/docs/pages/js-appendShadowRootProcessor.html
@@ -46,7 +46,8 @@
Syntax
@@ -61,6 +62,13 @@
Parameters
class with a static connect function, which are associated with newly attached nodes.
+
shadowRootoptional
+
+
+ a specific shadowRoot instance to update the behavior for - by default this is the shadowRoot prototype for all
+ components, but by passing a specific shadowRoot, you only update the behavior of that component's shadowRoot.
+
+
+
+
+
+ Tram-Lite Components!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/inline/inline.cy.js b/examples/inline/inline.cy.js
new file mode 100644
index 0000000..2f232df
--- /dev/null
+++ b/examples/inline/inline.cy.js
@@ -0,0 +1,65 @@
+describe('Tram-Lite Example Components', () => {
+ // per Cypress best practices (https://docs.cypress.io/guides/references/best-practices#Creating-Tiny-Tests-With-A-Single-Assertion)
+ // it is often better to run all tests together, rather than having unit-like tests... so we'll comment the intent of each test,
+ // rather than doing a reset between each test. The results should still be just as obvious in the cypress runner!
+ it('should validate all Tram-Lite APIs and Use Cases', () => {
+ // visit index.html (this works because the test page doesn't need to be hosted to work!)
+ cy.visit('../examples/inline/index.html');
+
+ /* validate that slot elements are rendered as expected in Tram-Lite */
+ cy.get('in-title').contains('Title');
+ cy.get('in-title').contains('Tram-Lite Components!');
+
+ /* validate that the counter elements work as expected */
+ cy.get('in-counter#default').contains(/Green: 0/); // default values should populate
+ cy.get('in-counter#red').contains(/Red: 0/); // passed in values should populate
+ cy.get('in-counter#red').click(); // clicking a counter should increment
+ cy.get('in-counter#red').contains(/Red: 1/);
+
+ /* verify that updating inputs updates attributes as expected (tl-controlled) */
+ cy.get('in-mirror').get('input#source').type('Hello, World');
+ cy.get('in-mirror').get('input#reflection').should('have.value', 'Hello, World');
+ cy.get('in-mirror').should('have.attr', 'value', 'Hello, World');
+ cy.get('in-mirror').should('have.attr', 'is-mirrored', '');
+
+ /* verify that updating an attribute copies to multiple elements and attributes */
+ cy.get('in-colorpicker').invoke('attr', 'hue', '120');
+ cy.get('in-colorpicker').get('input#hue-range-input').should('have.value', '120');
+ cy.get('in-colorpicker').get('input#hue-text-input').should('have.value', '120');
+ cy.get('in-colorpicker')
+ .get('rect')
+ .then(($element) => {
+ const rawColor = window.getComputedStyle($element[0])['fill'];
+ expect(rawColor).to.equal('oklch(0.7 0.1 120)');
+ });
+
+ /* verify that startup scripts in component definitions trigger as expected */
+ cy.get('in-todoitem').contains('Example Initial Item');
+ cy.get('in-todoitem').contains('Learning Tram-Lite');
+
+ /* verify that creating elements works as expected */
+ cy.get('in-todolist').get('form input').type('Cypress Test'); // create new todo item
+ cy.get('in-todolist').get('form').submit();
+
+ cy.get('in-todoitem').contains('Cypress Test'); // verify it exists
+
+ cy.get('in-todoitem').contains('Cypress Test').click(); // click it, and verify that the top label updates
+ cy.get('in-todolist').get('span').contains('(1/3)');
+
+ /* verify that updating an input with a false value unsets the attribute value */
+ cy.get('in-todoitem').contains('Cypress Test').click();
+ cy.get('in-todolist').get('span').contains('(0/3)');
+
+ /* verify that component effects trigger on dependency updates */
+ 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('in-progressbar').should('not.have.attr', 'warning');
+ cy.get('in-progressbar').get('input#value').clear().type('12');
+ cy.get('in-progressbar').should('have.attr', 'warning');
+ cy.get('in-progressbar').get('input#max').clear().type('15');
+ cy.get('in-progressbar').should('not.have.attr', 'warning');
+ });
+});
diff --git a/package-lock.json b/package-lock.json
index 8c29283..ff2f47c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "tram-lite",
- "version": "4.1.1",
+ "version": "4.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "tram-lite",
- "version": "4.1.1",
+ "version": "4.2.0",
"license": "MIT",
"devDependencies": {
"cypress": "^12.14.0",
diff --git a/package.json b/package.json
index 77eae6b..86e7043 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "tram-lite",
- "version": "4.1.1",
+ "version": "4.2.0",
"description": "💡 HTML library for building and enhancing web-components",
"homepage": "https://tram-one.io/tram-lite/",
"repository": "https://github.com/Tram-One/tram-lite",
@@ -9,17 +9,16 @@
"files": [
"output"
],
+ "bin": {
+ "export-components": "./output/export-components.js"
+ },
"scripts": {
"prestart": "npm run build",
"start": "serve .",
- "build": "npm run bundle && npm run build:api && npm run build:tram-lite",
- "bundle": "uglifyjs src/TramLite.js src/processors/*.js -o output/bundle-step.js --define APP_VERSION=\"'4.1.1'\" -b --comments all",
- "build:api": "uglifyjs output/bundle-step.js -o output/api.js --define MODULE=\"true\" -b --comments all ",
- "build:tram-lite": "uglifyjs output/bundle-step.js -o output/tram-lite.js --define MODULE=\"false\" -b --comments all ",
- "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",
+ "build": "node build.js",
"prepublishOnly": "npm run build",
- "pretest": "npm run build",
+ "build-test-components": "bash ./examples/components/build-test-components.sh",
+ "pretest": "npm run build && npm run build-test-components",
"test": "cypress open",
"docs": "serve ./docs"
},
diff --git a/src/ImportComponent.js b/src/ImportComponent.js
new file mode 100644
index 0000000..3764266
--- /dev/null
+++ b/src/ImportComponent.js
@@ -0,0 +1,44 @@
+class ImportComponent {
+ /**
+ * utility function for processing a list of component definitions.
+ * @param {string} definitionTemplate
+ */
+ static processDefinitionTemplate(definitionTemplate) {
+ // container for all our component definitions
+ const templateContainer = document.createElement('template');
+
+ templateContainer.innerHTML = definitionTemplate;
+
+ // for each child element, process the new definition (this is similar to processTemplateDefinition)
+ const allChildElements = templateContainer.content.children;
+ [...allChildElements].forEach((elementToDefine) => {
+ ImportComponent.importNewComponent(elementToDefine.outerHTML);
+ });
+ }
+
+ /**
+ * utility function for importing and defining new components (outside of Tram-Lite being installed)
+ * @param {string} componentTemplate
+ */
+ static importNewComponent(componentTemplate) {
+ const [componentRawStrings, componentTemplateVariables] =
+ ComponentDefinition.extractTemplateVariables(componentTemplate);
+
+ // make a component class based on the template tag pieces
+ // (this is done, over define, so we can attach shadow root processors)
+ const componentClass = TramLite.makeComponentClass(componentRawStrings, ...componentTemplateVariables);
+
+ // override attachShadow so that we can add shadowRootProcessors
+ const attachShadow = componentClass.prototype.attachShadow;
+ componentClass.prototype.attachShadow = function (...options) {
+ const shadowRoot = attachShadow.call(this, ...options);
+ TramLite.appendShadowRootProcessor('[tl-controlled]', ControlledInput, shadowRoot);
+ TramLite.appendShadowRootProcessor('[tl-effect]', ComponentEffect, shadowRoot);
+
+ return shadowRoot;
+ };
+
+ // define the component in the DOM
+ customElements.define(componentClass.tagName, componentClass);
+ }
+}
diff --git a/src/TramLite.js b/src/TramLite.js
index 0874ebe..4a1c804 100644
--- a/src/TramLite.js
+++ b/src/TramLite.js
@@ -194,18 +194,18 @@ class TramLite {
* {@link https://tram-one.io/tram-lite/#appendShadowRootProcessor Read the full docs here.}
* @param {string} matcher
* @param {{ connect: function }} componentClass
- * @param {(node: Node) => boolean} [rootNodeTest=() => true]
+ * @param {ShadowRoot} [shadowRoot=ShadowRoot.prototype]
*/
- static appendShadowRootProcessor(matcher, componentClass, rootNodeTest = () => true) {
+ static appendShadowRootProcessor(matcher, componentClass, shadowRoot = ShadowRoot.prototype) {
// save the original version of shadowRoot.append
- const shAppend = ShadowRoot.prototype.append;
+ const shAppend = shadowRoot.append;
- ShadowRoot.prototype.append = function (...nodes) {
+ shadowRoot.append = function (...nodes) {
shAppend.call(this, ...nodes);
// if any element in this shadowRoot matches our matcher,
// run the `connect` function from this class
this.querySelectorAll(matcher).forEach((matchingElement) => {
- if (rootNodeTest(matchingElement.getRootNode().host)) {
+ if (matchingElement.getRootNode().host) {
componentClass.connect(matchingElement);
}
});
@@ -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 20fbd55..1bba6a3 100644
--- a/src/processors/ComponentDefinition.js
+++ b/src/processors/ComponentDefinition.js
@@ -73,19 +73,28 @@ class ComponentDefinition {
[...allChildElements].forEach((elementToDefine) => {
const definitionString = elementToDefine.outerHTML;
- // we expect template variables to be in the following pattern, matching "${'...'}"
- const variablePattern = /\$\{\'(.*?)\'\}/;
- // Split the string by the above pattern, which lets us get an alternating list of strings and variables
- const parts = definitionString.split(variablePattern);
+ const [rawStrings, templateVariables] = ComponentDefinition.extractTemplateVariables(definitionString);
- // Extract the strings and the variables
- const rawStrings = parts.filter((_, index) => index % 2 === 0);
- const templateVaraibles = parts.filter((_, index) => index % 2 !== 0);
-
- TramLite.define(rawStrings, ...templateVaraibles);
+ TramLite.define(rawStrings, ...templateVariables);
});
}
+ /**
+ * utility function to extract js template strings, so that they can be passed into a template tag function
+ */
+ static extractTemplateVariables(templateString) {
+ // we expect template variables to be in the following pattern, matching "${'...'}"
+ const variablePattern = /\$\{\'(.*?)\'\}/;
+ // Split the string by the above pattern, which lets us get an alternating list of strings and variables
+ const parts = templateString.split(variablePattern);
+
+ // Extract the strings and the variables
+ const rawStrings = parts.filter((_, index) => index % 2 === 0);
+ const templateVariables = parts.filter((_, index) => index % 2 !== 0);
+
+ return [rawStrings, templateVariables];
+ }
+
/**
* function to set up an observer to watch for when new templates are added,
* and process all the definitions in them
@@ -118,7 +127,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);
}
diff --git a/src/scripts/export-script.js b/src/scripts/export-script.js
new file mode 100644
index 0000000..9ae81e9
--- /dev/null
+++ b/src/scripts/export-script.js
@@ -0,0 +1,55 @@
+#!/usr/bin/env node
+
+// usage - use this script to create a javascript version of a component definition
+// e.g. npx tram-lite export-components your-component-definition.html
+// see more usage details here: https://tram-one.io/tram-lite/#importing-and-exporting
+
+const fs = require('fs');
+const path = require('path');
+
+// check to see if we should use the minified flag for external dependencies
+const useMinified = process.argv.includes('-m') || process.argv.includes('--minified');
+
+// check to see if there is a predefined output filename (otherwise we will try to generate one)
+const outputFlagIndex = process.argv.findIndex((arg) => arg === '-o' || arg === '--output');
+const customOutputFile = outputFlagIndex !== -1 ? process.argv[outputFlagIndex + 1] : null;
+
+const filePaths = process.argv.filter((arg) => {
+ return arg.match(/\.html$/);
+});
+
+if (filePaths.length === 0) {
+ console.error('Please provide at least one file path as an argument.');
+ process.exit(1);
+}
+
+console.log('processing', filePaths, 'for export');
+const componentDefinitions = filePaths.map((filePath) => fs.readFileSync(filePath, 'utf8'));
+
+const tramLiteExportDependenciesPath = useMinified
+ ? path.join(__dirname, './export-dependencies.min.js')
+ : path.join(__dirname, './export-dependencies.js');
+
+const tramLiteExportDepenedencies = fs.readFileSync(tramLiteExportDependenciesPath).toString();
+const templateAndLoadCode = componentDefinitions
+ .map((componentCode) => {
+ // update the component code, in case it also uses ``, we'll need to escape them
+ const formattedComponentCode = componentCode.replaceAll('`', '\\`').replaceAll('${', '\\${');
+ return `
+{
+ const componentTemplate = \`${formattedComponentCode}\`;
+ ImportComponent.processDefinitionTemplate(componentTemplate);
+}
+`;
+ })
+ .join('\n');
+
+const isSingleFile = filePaths.length === 1;
+// if we are processing a single file, use that as the file name
+// if we are processing more than one file, use the directory name
+const generatedOutputFileName = isSingleFile
+ ? path.basename(filePaths[0], '.html') + '.js'
+ : path.basename(process.cwd()) + '.js';
+
+const result = '{\n' + tramLiteExportDepenedencies + '\n' + templateAndLoadCode + '\n}';
+fs.writeFileSync(customOutputFile || generatedOutputFileName, result);
diff --git a/src/scripts/import-script.js b/src/scripts/import-script.js
new file mode 100644
index 0000000..06e0529
--- /dev/null
+++ b/src/scripts/import-script.js
@@ -0,0 +1,26 @@
+// usage - this script is intended to be used as part of a script tag
+// e.g.