An web application using only native browser technology and vite build tool. HTML-first,template-agnostic,web 2.0.
MIT License
An web application using only browser native technologies and no frameworks.
fast
web components
browser native
HTML template based
vite
This application demonstrate that the bowser's have evolved to support complex application without external dependecies.
The entire source code a component is in a single file (e.g. home-page.component.html
).
<template id="home-page">
<h2>Home Page</h2>
</template>
<script>
class HomePage extends CustomElement {
static component = Object.freeze({
selector: 'home-page'
});
};
HomePage.componentInit();
</script>
Use data attributes to bind and attach events.
Example:
<button data-on="click:onClick('RED')">RED</button>
<span data-bind="color"></span>
<span data-css="color:color"></span>
<output-color color="BLACK" data-bind-color="color" />
<template id="test-page">
<button data-on="click:onClick('RED')">RED</button>
<button data-on="click:onClick('BLUE')">BLUE</button>
<output-color color="BLACK" data-bind-color="color" data-on="colorReset:onReset($event)"></output-color>
</template>
<script>
class TestPage extends CustomElement {
static component = Object.freeze({
selector: 'test-page'
});
onClick(color) {
this.state.setState({ color });
}
onReset(color) {
this.state.setState({ color });
}
};
TestPage.componentInit();
</script>
All html files will be bundled into the index.html
file.
<!-- index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SPA WC Vite</title>
<script>window.DEBUG_CUSTOM_ELEMENTS=true</script>
<!-- core.js will be injected here-->
</head>
<body>
<!-- components will be injected here -->
<app-root></app-root>
</body>
</html>
The core.js
creates the CustomElement
class that provide abstraction for easier working with Web Components
.
window.core = {
isObject: function (obj) { /** check is object. */ },
toArray: function (obj) { /** covert list to array */ },
registerComponent: function (selector, element, dependencies, extend) {
/** abstraction that first register dependencies, than register the component */
window.customElements.define(selector, element, { extends: extend });
},
/** other methods */
};
class StateManager {
constructor(onStateChanged) {
}
setState(newState) {
}
set(key, value) {
}
}
class CustomElement extends HTMLElement {
static componentInit() {
this.prototype.template = document.getElementById(this.component.templateId || this.component.selector);
core.registerComponent(/** */);
}
state = new StateManager(() => {
/* update all [data-bind], [data-bind-attributeName], [data-css], etc. */
});
constructor() {
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(this.template.content.cloneNode(true));
// attach to all event listeners specified by [data-on]
this.shadowRoot.querySelectorAll("[data-on]").forEach((node) => {
node.addEventListener(/** */);
});
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.state.set(name, newValue);
}
}
}
Vite is used only for bundeling files. In reality vite
is not needed and can be done with a simple NodeJs script.
package.json
{
"scripts": {
"start": "vite",
"build": "vite build"
},
"devDependencies": {
"vite": "5.1.7"
}
}
// vite.config.js
/** @param options {{ path: string, at: 'head' | 'body' | 'body-pre' }} */
function injectFilesInIndexHtml(options) {
return {
name: 'inject-files-in-index-html',
transformIndexHtml: {
transform(html) {
/** ... */
const isDirectory = options.path...;
const files = isDirectory ? fs.readdirSync(basePath) : [options.path];
const filesContent = files.map((file) => {
const pathToFile = path.resolve(basePath, file);
const txt = fs.readFileSync(pathToFile);
switch (file.split('.').pop()) {
/** ... */
case 'html':
return txt.toString();
}
});
const data = filesContent.join('');
switch (options.at) {
case 'head':
return html.replace('</head>', `${data}\n</head>`);
case 'body':
return html.replace('</body>', `${data}\n</body>`);
case 'body-pre':
return html.replace('<body>', `<body>\n${data}`);
}
},
},
};
}
export default defineConfig({
plugins: [
injectFilesInIndexHtml({ path: 'core.js', at: 'head' }),
injectFilesInIndexHtml({ path: 'components/', at: 'body-pre' }),
],
});