A minimal, zero-dependency boilerplate for building single-page applications using Web Components.
Current version: 0.1 alpha.
⚠️ Early version. Use with caution.
- Concept
- Getting started
- Boilerplate Architecture
- State management
- Navigation management
- Bundling the app
- Serving the app
- Misc
This boilerplate proposes a structure for building front-end web applications projects using Custom Elements, a feature that is part of the Web Components specification.
Custom Elements are extremely powerful, in that they allow the creation of HTML elements with self-contained logic and styling. They are well supported by modern browsers, simple to use, and since they are available out of the box, using them generally results in much smaller JavaScript bundles, compared to JavaScript frameworks.
The architecture proposed in this boilerplate is one of the many, many possible ways a single-page web application could be built using Web Components.
This is not in any way meant to be "the optimal way" or even "the right way" of doing so. Personal opinions and likings were most definitely a part of some of the choices made here, and this is clearly acknowledged.
This boilerplate was put together, nonetheless, with a few principles in mind:
- Simplicity: It has to be easy to understand, use and edit.
- Standard: It must rely almost exclusively on features that are part of web standards specifications.
- Independence: It must not require any third-party software package to be used, besides a JavaScript runtime. No package.json, no Pipfile.
Although this boilerplate does not have software-level dependencies, you will need the following software on your machine:
We also recommend using a UNIX-like system such as Mac OS or Linux, or the Windows Subsystem for Linux on Windows.
In a terminal, browse to the folder you want to set your project in, and run the following command to download and execute the setup script:
curl https://raw.githubusercontent.com/matteocargnelutti/this-is-not-a-framework/main/setup.sh > setup.sh && chmod a+x setup.sh && ./setup.shStart the project by using the following command:
node bundler.js && ./serve.sh
# ☝️ Re-build the app and serve it locally over HTTPThen open your browser and go to http://localhost:8080.
If all went well, the default demo app should be accessible:
See this demo live.
We encourage you to start editing files of the default demo app to see how the boilerplate functions. Understanding a bit of JavaScript, and how Custom Elements work is recommended to help in that process.
At the core of the boilerplate is <app-root>, a custom element whose role is to be the parent of the entire application, handle a global state and basic navigation.
index.html includes an <app-root> tag, which triggers the creation of said element and executes the app's logic.
The global HTML structure of the app is handled by <app-root>, and can be edited directly in AppRoot.renderInnerHTML().
There should be only one instance of <app-root> per application: this behavior will be enforced by default.
Components are independent Custom Elements that live under the src/components folder. They are the building blocks of your app.
The structure that was chosen for this boilerplate's elements is to generate the components' HTML within a renderInnerHTML() method, called at least once when the component is added to the page, and to store the CSS for this component in a separate, dedicated file.
- MyComponent.js # Component structure and logic
- MyComponent.css # Component styling
Explore example of components:
In the context of this boilerplate, screens are a specialized type of component:
- Their name must start with
screen- - They must live in the
src/screensfolder (or a subfolder) <app-root>lists and uses those components for navigation management<app-root>will render the currently loaded screen withinapp-root > main
Explore example of screens:
This boilerplate comes with a simple state manager that can be used to allow custom elements to hold and share data, but also react to changes in state.
StateManager takes and holds and object that is considered a state.
Access and edits to this object are regulated through the use of an object Proxy, whose role it so fire a StateManagerUpdate event, attached to the state holder.
See list of parameters and logic rules in StateManager's intro comment.
Let's consider that our custom element my-element holds a state as follows:
this.state = new StateManager(
this, // Parent of the state manager. In this case, this instance of <my-element>
'MyElementState', // Name of the state manager instance.
{ someRandomCounter: 0 } // Data to be held by the state manager. Could be a nested object.
);Upon write access on someRandomCounter, a StateManagerUpdate event will be fired from our instance of <my-element>, containing the following data in its detail field, allowing to track changes.
{
'stateManagerName': 'MyElementState'
'updatedPropertyPath': 'data' // Example of more complex path: data.object1.object2.object3
'updatedProperty': 'someRandomCounter',
'newValue': 1,
// The two following properties are only added if the `StateManager.provideStateCopy` is set to `true`
'previousState': {someRandomCounter: 0}, // Copy of the state before the update
'newState': {someRandomCounter: 1} // Copy of the state after the update
}The fact that StateManager fires events when data changes allows other parts of the component or the app to react to changes in state:
let myElement = this.querySelector('parent > my-element');
myElement.addEventListener('StateManagerUpdate', this.doSomethingOnGlobalStateUpdate.bind(this));<app-root> contains, by default, an instance of StateManager that can be used for global state management. From anywhere within the app, it is possible, via document, to access the state property of <app-root> to access shared data.
See:
- Default, global state definition in
<app-root> - Example of a component listening to changes in
<app-root>'s state: [1] [2]
It is possible to create one or multiple StateManager instances attached to a given custom element. This is useful for component-level state management, when it does not make sense to share state across components.
See:
StateManager is provided for simplicity and out-of-the-box convenience. You are encouraged to replace it with more complex state management solutions if needed.
This boilerplate offers a simplistic hash-based navigation system out of the box: Every screen element defined under src/screens, is accessible via a /#!/ URI.
For example:
If current URL contains /#!/contact-us, <app-root> will make sure that the <screen-contact-us> element exists and, if so, clear app-root > main and inject a <screen-contact-us> tag there.
This logic is defined in <app-root> and can be extended / edited / removed at will.
As for the other out-of-the-box features of this boilerplate, this will likely not cover all uses cases, and you are encouraged to use a more sturdy custom navigation system to cover specific needs.
The changeScreen(screenName) method of <app-root> can be called directly to switch to a specific screen without using hash-based navigation:
let appRoot = document.querySelector('app-root');
appRoot.changeScreen('screen-test');In the spirit of providing a dependency-free boilerplate, This is not a framework comes with an extremely basic JavaScript and CSS bundler, bundler.js.
- Lists all
*.jsand*.cssfiles under/src - Concatenate them as two files:
dist/bundle.js,dist/bundle.css - Allow you to change input and output paths
- CSS and JS minification
- Transpiling to ES5
- Injecting polyfills
- Offer a "watch" mode so bundles are re-built on the fly whenever a file is edited
As usual, you are encouraged to use a dedicated bundler as required to match your needs.
In some cases you might not want to use a bundler at all and directly import various sources, either via <script> tags in index.html
In the default setup of the boilerplate, the bundler has to be re-run after each edit to /src.
To generate new bundles, simply run bundler.js with node:
node bundler.jsDepending on your operating system and setup, it is possible to instruct your system to run this command whenever a file is edited under src/.
Example: watchdog's watchmedo.
The boilerplate comes with serve.sh, a quick shortcut serve the app locally via HTTP, with the help of Python's http.server package.
Make sure your app bundle is built and run the following.
./serve.sh
# index.html is now served on http://localhost:8080- This is an extremely basic HTTP server and should only be used for development purposes.
- You are encouraged to upgrade to a third-party package with live-reload and local HTTPS support as required.
- Please use GitHub issues for bug reports and features requests.
- VSCode extension to enable syntax highlighting for HTML in JavaScript template strings
- Web Components Tutorials on JavaScript.info
- Follow me on Twitter, @macargnelutti
- Live demo of the default app.


