@scalable.software/web.component
v1.2.1
Published
Web Component Template
Downloads
5
Readme
Web.Component
This repository contains a comprehensive boilerplate template for creating Web Components with unit testing and api document generation. The template is designed to demonstrate the use of modern web technologies such as HTML, CSS, and TypeScript to create modular, reusable, and testable web components. The template includes the following features:
- Separation of Concerns: Demonstrates the principle of Separation of Concerns (SoC) by using HTML for structure, CSS for presentation, and TypeScript for behavior.
- State Management: Implements state management using private properties and public getters and setters.
- Imperative and Declarative API: Provides both imperative and declarative APIs for interacting with the web component.
- Event Handling: Demonstrates event handling by dispatching custom events when the state of the component changes.
- Import Maps: Demonstrates the use of import maps to resolve JavaScript module specifiers to their corresponding files or URLs.
- Unit Testing: Includes unit tests for the web component using the Jasmine assertion library and Karma test runner.
- API Documentation: Generates API documentation for the web component using the Typedoc documentation generator.
Before we dive into the details of all the features, let's first go through the installation and demonstration of the web component.
Installation
Create a new repository using this template
Clone the repository to your local machine
Install the dependencies
npm install- Run the unit tests to ensure everything is working as expected
npm test- Build the web component
npm run buildUsage
The below steps will guide you through all the unique feature associated with the demo web component, a pin-button. As the name implies, the pin-button is a simple button that can be switched on and off.
- After successful build, run the following command:
npm run serveObserve the
pin-buttonweb component in the browserOpen the chrome developer tools and select the console
Get a reference to the web component in the browser console
const pin = document.querySelector("pin-button");- Create an onon event listener for the web component
const onon = () => {
console.log("onon triggered");
};- Add the event listener to the web component
pin.onon = onon;- Switch the pin on using the imperative API
pin.on();Observe the visual changes in the browser and the console output
Select the elements tab in the chrome developer tools and inspect the html
<pin-button state="on"></pin-button>manually change the state attribute value to
offObserve the visual changes in the browser
Return to the console tab and get the current state of the web component
pin.state;Click the web component with the mouse
Again observe the event which is triggered in the console
Lastly, get the current state of the web component again
pin.state;See API Documentation for details of all state, operations and events implemented.
Note: See the section of Typedoc how to generate the API documentation.
Now that you have successfully interacted with the pin-button web component, let's dive into the details of the features that make this boilerplate template unique.
Separation of Concerns
In modern web development, the principle of Separation of Concerns (SoC) is fundamental to creating maintainable, scalable, and easily understandable code. This principle is elegantly demonstrated through the use of HTML, CSS, and TypeScript, each serving a distinct purpose while seamlessly working together to create robust web components. Below, we outline how each technology contributes to this principle:
HTML Template: Structure
The technology used to define the layout of the web component is HTML templates. HTML templates are HTML fragments that are loaded into the DOM but not rendered immediately. Web component typically clones the HTML template and insert that inside the shadow DOM of a web component after which it is rendered. This allows developers to define the structure of the web component without cluttering the main HTML file with complex markup.
With reference to this boilerplate:
- The
Pin.template.htmlin thesrcfolder contains the layout of thePincomponent. - This template is loaded into the dom using a public
loadTemplate(filename)static method in the baseComponentclass, where the filename in this case isPin.template.html. (See theindex.jsfile in thedemofolder which illustrate how this method is used to load the template into the DOM.) - This keep things simple, by default the template id is the same as the web component tag, see the public
templategetter in thePinclass. This id is then used by the protected_loadTemplateinstance method on the baseComponentclass to clone and inserted the template into the shadow DOM of the web component.
CSS: Presentation
CSS (Cascading Style Sheets) takes charge of the presentation aspect, dictating how the structured HTML elements should appear on the screen. This includes layouts, colors, fonts, and animations. CSS is also used to update visual changes based on changes to web component attributes. CSS's role in Separation of Concerns is to decouple the visual styling from the content structure, allowing for design changes without altering the underlying HTML. This separation not only makes the styling more manageable but also enhances the flexibility and reusability of the components.
With reference to this boilerplate:
- The
Pin.style.cssin thesrcfolder contains the styling for thePincomponent. - The public
cssgetter in thePinclass contains thecssfilename and is used by the protectedloadStylemethod on the baseComponentclass to insert alinkelement into the shadow DOM of the web component which points to thecssfile.
TypeScript: Behavior
TypeScript, a superset of JavaScript, adds type safety and advanced object-oriented features to the component's behavior. It controls how the component reacts to user interactions, fetches data, and manipulates the DOM. By isolating the behavioral logic in TypeScript, developers can create more predictable and maintainable codebase. TypeScript's static typing and compile-time checks further contribute to the robustness and scalability of the component.
Fundamentally, the behavior of a web component can be grouped into three parts:
- State: Implements state management using private properties and public getters and setters.
- Operations: Provides both imperative and declarative APIs for interacting with the web component.
- Events: Demonstrates event handling by dispatching custom events when the state of the component changes.
With reference to this boilerplate, see the Pin class in the src folder which contains three sections described above.
Development Workflow
This boilerplate was developed using a specific workflow that ensures the web component is modular, testable, and well-documented. As a first step, a business analyst defined the needed specifications for the web component. These specifications were then used to guide the development of the web component. The development process followed the steps outlined below:
Web Component Specifications
A component is built according to a set of specifications, see the content in the specifications folder for an example. The specifications are divided into the following categories:
- Metadata
- State
- Operations
- Events
- Composition
Below in an overview of each section and a reference to the pin-button web component:
Component Metadata
This section defines the name of the component, the tag name, and namespace to use when applicable
{
"Component": "Pin",
"Tag": "pin-button",
"Namespace": "Pin",
"Description": "The Pin component is a button that can be turned on or off."
}Component State
Component state is implemented using a combination of private properties and public getters and setter methods. Some properties are also defined as attributes to enable declarative API.
For example, the specifications refer to a visible state:
{
"state": {
"visible": {
"type": "string",
"description": "Indicates whether the pin is visible.",
"values": ["yes", "no", null],
"default": "yes",
"isAttribute": true
}
}
}Note the null value in the values array. This is used to indicate that the attribute can be removed from the html element.
here is the extract of the implementation of the visible state:
private _visible: Visibility = Visible.YES;
public get visible(): Visibility {
return <Visibility>this.getAttribute(Attribute.VISIBLE) ?? this._visible;
}
public set visible(visible: Visibility) {
visible = visible || Visible.YES;
if (this.visible !== visible) {
this._visible = visible;
visible == Visible.YES && this.removeAttribute(Attribute.VISIBLE);
visible == Visible.YES &&
this.dispatchEvent(new CustomEvent(Event.ONSHOW, { detail: visible }));
visible == Visible.NO && this.setAttribute(Attribute.VISIBLE, visible);
visible == Visible.NO &&
this.dispatchEvent(new CustomEvent(Event.ONHIDE, { detail: visible }));
}
}Component Operations
Component operations are implemented using public methods and used to enable the imperative API. In essence these public methods are thin wrappers which use the available setters to change the state of the component.
For example, the specifications refer to a on operation:
{
"operations": {
"on": {
"description": "Turns the pin on.",
"parameters": [],
"returns": "void"
}
}
}Here is the extract of the implementation of the on operation:
public on = () => (this.state = State.ON);Component Events
Component events are implemented using the CustomEvent constructor. The CustomEvent constructor is used to create a new event object which can be dispatched using the dispatchEvent method. Events are dispatched when the state of the component changes inside the setter methods.
For example, the specifications refer to a onon event:
{
"events": {
"onon": {
"description": "Triggered when the pin is turned on.",
"parameters": []
}
}
}Here is the extract of the implementation of the onon event:
// Event Registration
public set onon(handler: Handler) {
this.addEventListener(Event.ONON, handler);
}The onon event is dispatched in the setter of the state property:
state === State.ON &&
this.dispatchEvent(new CustomEvent(Event.ONON, { detail: { state } }));
state === State.OFF &&
this.dispatchEvent(new CustomEvent(Event.ONOFF, { detail: { state } }));Component Composition
Component composition is implemented using the HTMLTemplateElement and ShadowRoot API. The HTMLTemplateElement is used to define the layout of the component and the ShadowRoot is used to attach the template to the component.
For example, the specifications refer to the Pin.template.html file:
{
"composition": {
"on": {
"description": "The SVG icon used for on state.",
"contains": ["path"],
"type": "svg"
}
}
}Here is the extract of the implementation of the on element in the composition:
<svg class="on" height="24px" width="24px" viewBox="0 0 20 20" fill="#212121">
<path
d="M2.85355 2.14645C2.65829 1.95118 2.34171 1.95118 2.14645 2.14645C1.95118 2.34171 1.95118 2.65829 2.14645 2.85355L6.896 7.60309L4.01834 8.75415C3.35177 9.02078 3.17498 9.88209 3.68262 10.3897L6.29289 13L3 16.2929V17H3.70711L7 13.7071L9.61027 16.3174C10.1179 16.825 10.9792 16.6482 11.2459 15.9817L12.3969 13.104L17.1464 17.8536C17.3417 18.0488 17.6583 18.0488 17.8536 17.8536C18.0488 17.6583 18.0488 17.3417 17.8536 17.1464L2.85355 2.14645ZM11.6276 12.3347L10.3174 15.6103L4.38973 9.68263L7.66531 8.3724L11.6276 12.3347ZM12.9565 10.7127C12.9294 10.7263 12.9026 10.7403 12.8761 10.7548L13.6202 11.4989L16.8622 9.87793C18.0832 9.26743 18.3473 7.64015 17.382 6.67486L13.3251 2.61804C12.3599 1.65275 10.7326 1.91683 10.1221 3.13783L8.5011 6.37977L9.24523 7.1239C9.25971 7.09739 9.27373 7.07059 9.28728 7.04349L11.0165 3.58504C11.3218 2.97454 12.1354 2.8425 12.618 3.32514L16.6749 7.38197C17.1575 7.86461 17.0255 8.67826 16.415 8.98351L12.9565 10.7127Z"
/>
</svg>Typescript Compiler Options
Two different typescript configuration are defined: tsconfig.build.json and tsconfig.test.json. The typescript complier options of the two files are largely the same. Below is an overview of the compiler options used:
{
"compilerOptions": {
"module": "es2022",
"target": "es2022",
"moduleResolution": "node",
"declaration": true,
"rootDir": "./src",
"outDir": "./dist/",
"baseUrl": "./",
"paths": {
"pin": ["./src/Index.js"]
}
},
"include": ["./src/**/*"]
}The important options to note are:
module
Thees2022module system is used to enable the use of ESM in the browser.paths
Thepathsoption is used to define module aliases and used in conjunction with importmaps to avoid the use of a bundler. See the section on importmaps for more details.
Unit Testing
Unit test are designed to test the web component based on the defined specifications.
This template enables both realtime and manual unit testing. Realtime unit testing is achieved using wallaby and manual unit testing is done using the karma test runner with jasmine assertion library. Both wallaby and karma configuration file is located at the root of the project: wallaby.js, karma.conf.js.
To manually run the unit tests, and generate coverage reports in the coverage folder, use the following command:
npm testTypedoc Configuration
In addition the typescript compiler options, the tsconfig.test.json configuration file also contains Typedoc configuration details. Typedoc is a documentation generator for typescript projects.
To generate the API documentation, in the docs folder, use the following command:
npm run documentImportmaps
Import maps are a browser feature that enables developers to define how JavaScript module specifiers are resolved to their corresponding files or URLs, allowing for custom mapping of module names to paths and facilitating easier management of dependencies. By using import maps, developers can therefore easily switch between a local file, an NPM installed package or even a CDN hosted file. Also, Import maps is what enables the use of ESM compiled TypeScript to work in the browser without the use of any bundler.
Lastly, the keep the mail HTML file as lean as possible, an injection script is used to inject import maps defined as json objects into the html document. Once, external Import maps have been added to the HTML spec, the injection script will not longer be needed. See the ./importmap folder, which contains the following files:
inject.js- The script that injects the import map into the HTML documentimportmap.build.js- The import map used by the demo pageimportmap.test.js- The import map used by the test page
Publishing
The beauty of combining HTML, CSS, and TypeScript lies in their ability to be bundled together into a single, installable package. This approach not only adheres to the Separation of Concerns but also simplifies the distribution and reuse of components across different projects.
With reference to this boilerplate, the web component is bundled and published as a NPM package. The package.json file contains the necessary scripts and configurations to build, test, and publish the web component. The dist folder contains the compiled JavaScript files as well as a copy of the HTML Template and CSS Files.
A github workflow is setup to automatically publish the web component to NPM when a new release is created. The workflow is defined in the .github/workflows/publish.yml file.
Before creating a new release in GitHub, ensure the following steps are completed:
- Determine the type of version bump (major, minor, patch)
- Run one of the following commands to bump the version in the
package.jsonfile:
npm version major
npm version minor
npm version patch- Given you have used the above command, a tag will automatically be created in the repository, else you can create a tag manually using the following command:
git tag -a v1.0.0 -m "Version 1.0.0"- Push the tag to the remote repository:
git push origin v1.0.0- Create a new release in GitHub and the workflow will automatically publish the web component to NPM.
Note: a manual publication to NPM is required for the first release when using a scoped package on the free tier of NPM:
npm publish --access public.
License
his software and its documentation are released under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License (CC BY-NC-SA 4.0). This means you are free to share, copy, distribute, and transmit the work, and to adapt it, but only under the following conditions:
Attribution: You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work).
NonCommercial: You may not use this material for commercial purposes.
ShareAlike: If you alter, transform, or build upon this work, you may distribute the resulting work only under the same or similar license to this one.
For more details, please visit the full license agreement.
