Skip to content

A simple, zero-dependency boilerplate for building single-page applications using Web Components.

License

Notifications You must be signed in to change notification settings

matteocargnelutti/this-is-not-a-framework

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🙅 This is not a framework

This is not a framework's logo.

A minimal, zero-dependency boilerplate for building single-page applications using Web Components.

Current version: 0.1 alpha.

⚠️ Early version. Use with caution.


Summary


Concept

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.

☝️ Back to summary


Getting Started

Pre-requisites

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.

Setting up the boilerplate

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

Checking that the boilerplate works

Start the project by using the following command:

node bundler.js && ./serve.sh 
# ☝️ Re-build the app and serve it locally over HTTP

Then open your browser and go to http://localhost:8080.

If all went well, the default demo app should be accessible:

DEMO app working

See this demo live.

Learning

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.

☝️ Back to summary


Boilerplate architecture

🌲 The Application Root

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.

Structure of <app-root> with nested components

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

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:

📺 Screens

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/screens folder (or a subfolder)
  • <app-root> lists and uses those components for navigation management
  • <app-root> will render the currently loaded screen within app-root > main

Explore example of screens:

☝️ Back to summary


State Management

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.

How it works

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));

Example of an app-wide state management

<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:

Example of component-level state management

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:

Using a different state manager

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.

☝️ Back to summary


Navigation Management

Default behavior

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.

Taking control

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');

☝️ Back to summary


Bundling the app

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.

What this bundler does

  • Lists all *.js and *.css files under /src
  • Concatenate them as two files: dist/bundle.js, dist/bundle.css
  • Allow you to change input and output paths

What this bundle does not do

  • 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

Usage

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

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

☝️ Back to summary


Serving the app

The boilerplate comes with serve.sh, a quick shortcut serve the app locally via HTTP, with the help of Python's http.server package.

Usage

Make sure your app bundle is built and run the following.

./serve.sh
# index.html is now served on http://localhost:8080

Limitations

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

☝️ Back to summary


Misc

☝️ Back to summary

About

A simple, zero-dependency boilerplate for building single-page applications using Web Components.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published