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 @@ Install Building Components + Importing & Exporting HTML 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. + + + +

diff --git a/docs/pages/install.html b/docs/pages/install.html index b596efe..49eff00 100644 --- a/docs/pages/install.html +++ b/docs/pages/install.html @@ -29,6 +29,13 @@

script tag

+

Importing & Exporting Components

+

+ 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. +

+

Return Value

None

diff --git a/examples/components/build-test-components.sh b/examples/components/build-test-components.sh new file mode 100644 index 0000000..bef97a8 --- /dev/null +++ b/examples/components/build-test-components.sh @@ -0,0 +1,3 @@ +node output/export-components.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-components.js examples/components/ex-colorpicker.html -o examples/components/ex-colorpicker.js +node output/export-components.js examples/components/ex-todolist.html -o examples/components/ex-todolist.js -m 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-container.html b/examples/components/ex-container.html new file mode 100644 index 0000000..d9dde76 --- /dev/null +++ b/examples/components/ex-container.html @@ -0,0 +1,14 @@ + + +
+ ${'name'} + +
+
diff --git a/examples/components/ex-progressbar.html b/examples/components/ex-progressbar.html new file mode 100644 index 0000000..6521ee1 --- /dev/null +++ b/examples/components/ex-progressbar.html @@ -0,0 +1,17 @@ + +
+ + +
+ +
${'warning'}
+ +
diff --git a/examples/components/ex-temperature.html b/examples/components/ex-temperature.html new file mode 100644 index 0000000..ab0e08f --- /dev/null +++ b/examples/components/ex-temperature.html @@ -0,0 +1,46 @@ + + + = + + + + + + diff --git a/examples/components/ex-todolist.html b/examples/components/ex-todolist.html new file mode 100644 index 0000000..54c4912 --- /dev/null +++ b/examples/components/ex-todolist.html @@ -0,0 +1,49 @@ + + + + + + + + To Do List (${'completed'}/${'total'}) +
+ +
+ + +
diff --git a/examples/export/export.cy.js b/examples/export/export.cy.js new file mode 100644 index 0000000..75703e6 --- /dev/null +++ b/examples/export/export.cy.js @@ -0,0 +1,44 @@ +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)'); + }); + + /* copied from inline tests - specifically verifies multi component definition in export */ + cy.get('ex-todoitem').contains('Example Initial Item'); + cy.get('ex-todoitem').contains('Learning Tram-Lite'); + + 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)'); + + cy.get('ex-todoitem').contains('Cypress Test').click(); + cy.get('ex-todolist').get('span').contains('(0/3)'); + }); +}); diff --git a/examples/export/index.html b/examples/export/index.html new file mode 100644 index 0000000..298bdd2 --- /dev/null +++ b/examples/export/index.html @@ -0,0 +1,35 @@ + + + + Tram-Lite Exported Components + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/hybrid/hybrid.cy.js b/examples/hybrid/hybrid.cy.js new file mode 100644 index 0000000..514df2c --- /dev/null +++ b/examples/hybrid/hybrid.cy.js @@ -0,0 +1,29 @@ +describe('Tram-Lite Example Components (via import and inline)', () => { + it('should validate Tram-Lite APIs and Use Cases when importing and inline defining components', () => { + // visit index.html + cy.visit('../examples/hybrid/index.html'); + + /* 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'); + + /* 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/examples/hybrid/index.html b/examples/hybrid/index.html new file mode 100644 index 0000000..2c0eae8 --- /dev/null +++ b/examples/hybrid/index.html @@ -0,0 +1,85 @@ + + + + Tram-Lite Hybrid Components + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/import/import.cy.js b/examples/import/import.cy.js new file mode 100644 index 0000000..a1686b8 --- /dev/null +++ b/examples/import/import.cy.js @@ -0,0 +1,44 @@ +describe('Tram-Lite Example Components (via import)', () => { + it('should validate Tram-Lite APIs and Use Cases when importing components', () => { + // visit index.html + cy.visit('../examples/import/index.html'); + + /* copied from inline tests - verifies that multiple components in a single import works */ + 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 - verifies that multiple components in a single import 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 - verifies that multiple call of import-component 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)'); + }); + + /* copied from inline tests - specifically verifies multi component definition in import */ + cy.get('ex-todoitem').contains('Example Initial Item'); + cy.get('ex-todoitem').contains('Learning Tram-Lite'); + + 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)'); + + cy.get('ex-todoitem').contains('Cypress Test').click(); + cy.get('ex-todolist').get('span').contains('(0/3)'); + }); +}); diff --git a/examples/import/index.html b/examples/import/index.html new file mode 100644 index 0000000..119c35a --- /dev/null +++ b/examples/import/index.html @@ -0,0 +1,39 @@ + + + + Tram-Lite Imported Components + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/index.html b/examples/index.html deleted file mode 100644 index 7f40718..0000000 --- a/examples/index.html +++ /dev/null @@ -1,211 +0,0 @@ - - - - Tram-Lite Example Components - - - - - - - - - - - - - Tram-Lite Components! - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/inline/index.html b/examples/inline/index.html new file mode 100644 index 0000000..6d81c21 --- /dev/null +++ b/examples/inline/index.html @@ -0,0 +1,211 @@ + + + + Tram-Lite Inline Components + + + + + + + + + + + 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.