In previous posts, I explained How to setup a TypeScript project using Rollup.js from scratch. Also, I covered How to Serve a Single Page Application(SPA) using Rollup.js and Web Dev Server step by step.
In this article, I'll take the result of both tutorials as a starting point to integrate LitElement into the game to write the first Web Components through the latest web standards and powerful TypeScript features.
Project Setup
Prerequisites
You'll need to have installed the following tools in your local environment:
- Node.js. Preferably the latest LTS version.
- A package manager. You can use either NPM or Yarn. This tutorial will use NPM.
Initialize the Project
Let's create a clone or download the project seed before adding the new configurations and tools:
git clone https://github.com/luixaviles/typescript-rollup.git
cd typescript-rollup/
git checkout tags/02-serve-spa -b 03-litelement-app
The previous commands will download the project and create a new branch 03-litelement-app
to get started.
Source Code Files
The previous project already contains a set of files and configurations ready to go with TypeScript and Rollup. Open it with your favorite code editor and take a look at the project structure:
|- typescript-rollup
|- src/
|- math/
|- math.ts
|- index.ts
|- string/
|- string.ts
|- index.ts
|- app.ts
|- index.html
|- package.json
|- rollup.config.js
|- tsconfig.json
|- web-dev-server.config.js
Installing LitElement and lit-html
The Single-Page Application will be based on LitElement, hence we'll need to install some development dependencies to the project:
npm install --save lit-element lit-html
- LitElement will be the main tool for writing the class-based web components for the application.
- lit-html is an efficient and extensible HTML templating library.
Your package.json
file should have the new dependencies listed as follows:
{
...
"dependencies": {
"lit-element": "^2.4.0",
"lit-html": "^1.3.0"
}
}
Rollup Configuration
The project is already configured and it uses Rollup as the module bundler. Verify the content of rollup.config.js
as follows:
// rollup.config.js
import merge from 'deepmerge';
import { createSpaConfig } from '@open-wc/building-rollup';
const baseConfig = createSpaConfig({
developmentMode: process.env.ROLLUP_WATCH === 'true',
injectServiceWorker: false
});
export default merge(baseConfig, {
// any <script type="module"> inside will be bundled by rollup
input: './index.html'
});
The baseConfig
content will be generated from createSpaConfig
function, which is defined in @open-wc/building-rollup package.
When createSpaConfig
is used, a service worker is generated using Workbox. However, the service worker is injected into the index.html
file when the injectServiceWorker
flag is enabled.
The Serve and Build Scripts
Now it's time to pay attention to the package.json
file. It already defines a set of scripts to both serve and build the project:
{
...
"scripts": {
"start": "concurrently --kill-others --names tsc,web-dev-server \"npm run tsc:watch\" \"web-dev-server --config web-dev-server.config.js\"",
"build": "rimraf dist && tsc && rollup -c rollup.config.js"
},
}
Let's explain what's going to happen with those scripts:
- "tsc:watch" is the script to start with the compilation process through
tsc
, which is the TypeScript compiler. The--watch
param stands for a compiler option to run the compiler in watch mode(it trigger recompilation on file changes). - "start" is the script to add the execution of some commands in parallel:
npm run tsc:watch
andweb-dev-server
that includes some CLI flags through a configuration file.- You already noted that
concurrently
command is called first! - The
--kill-others
parameter will kill all the invoked commands if one dies(eithertsc
orweb-dev-server
).
- You already noted that
It's important to note that any time you need to configure or customize the web-dev-server
behavior, the web-dev-server.config.js
file should be updated.
TypeScript Configuration
The TypeScript configuration is defined in tsconfig.json
file. It already contains some relevant content for the compilation process:
{
"compilerOptions": {
"target": "es2018",
"module": "esnext",
"moduleResolution": "node",
"noEmitOnError": true,
"lib": ["es2017"],
"strict": true,
"esModuleInterop": false,
"outDir": "out-tsc",
"rootDir": "./"
}
,
"include": ["./src/**/*.ts"]
}
However, it would be good to add some capabilities to start working with LitElement. To do that, let's add the experimentalDecorators
support along with the dom
library(to be included in the compilation process). The final state of this fill would be:
{
"compilerOptions": {
"target": "es2018",
"module": "esnext",
"moduleResolution": "node",
"noEmitOnError": true,
"lib": ["es2017", "dom"],
"strict": true,
"esModuleInterop": false,
"outDir": "out-tsc",
"rootDir": "./",
"experimentalDecorators": true,
}
,
"include": ["./src/**/*.ts"]
}
- "experimentalDecorators": true provides the Decorators support for TypeScript. It involves the use of annotations and meta-programming syntax for class declarations and attributes. LitElement support them too. For example:
import {LitElement, html, customElement, property} from 'lit-element';
@customElement('my-custom-element')
class MyCustomElement extends LitElement {
@property()
propName = 'value';
render() {
return html`Hello world`;
}
}
Both @customElement()
and @property()
decorators will make the web components definition compact.
- "lib": ["es2017", "dom"] as the name states, provides a configuration with a list of library files to be included in the compilation. Take a look at the list of possible values for this parameter in the official documentation.
Just to clarify, without adding dom
into lib
configuration, you may find several errors at compilation time. For example:
[tsc] node_modules/lit-element/lib/css-tag.d.ts(16,19): error TS2304: Cannot find name 'CSSStyleSheet'.
[tsc] node_modules/lit-element/lib/decorators.d.ts(46,108): error TS2304: Cannot find name 'HTMLElement'.
[tsc] node_modules/lit-element/lib/decorators.d.ts(203,47): error TS2304: Cannot find name 'AddEventListenerOptions'.
[tsc] node_modules/lit-element/lit-element.d.ts(105,49): error TS2304: Cannot find name 'Element'.
[tsc] node_modules/lit-element/lit-element.d.ts(105,59): error TS2304: Cannot find name 'DocumentFragment'.
[tsc] node_modules/lit-element/lit-element.d.ts(140,45): error TS2304: Cannot find name 'ShadowRoot'.
[tsc] node_modules/lit-html/lib/dom.d.ts(23,49): error TS2304: Cannot find name 'Node'.
[tsc] node_modules/lit-html/lib/parts.d.ts(137,29): error TS2304: Cannot find name 'EventTarget'.
[tsc] node_modules/lit-html/lib/render.d.ts(32,69): error TS2304: Cannot find name 'DocumentFragment'.
[tsc] node_modules/lit-html/lib/template-result.d.ts(29,27): error TS2304: Cannot find name 'HTMLTemplateElement'.
[tsc] node_modules/lit-html/lib/template.d.ts(64,42): error TS2304: Cannot find name 'Comment'.
Using LitElement
Adding the Web Component Definition
It's time to add some TypeScript code with the first Web Component through LitElement. Let's create a new file src/main.ts
with the following content:
import { LitElement, html, customElement, css, property } from 'lit-element';
@customElement('comp-main')
export class CompMain extends LitElement {
static styles = css`
:host {
display: flex;
}
`;
@property({ type: String }) message: string = 'Welcome to LitElement';
render() {
return html`
<div>
<h1>${this.message}</h1>
<span>This App uses:</span>
<ul>
<li>TypeScript</li>
<li>Rollup.js</li>
<li>es-dev-server</li>
</ul>
</div>
`;
}
}
The previous code defines a new component for your project. You can use it in your template files as if it were a new member of the HTML vocabulary:
<comp-main></comp-main>
Also, it supports a custom attribute too!
<comp-main message="Welcome to the TypeScript and LitElement world"></comp-main>
Update the index.html File
At this point, the index.html
file of the project defines almost the same content as the Web Component. Let's change the <body>
content a little bit:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Single Page Application</title>
</head>
<body>
<comp-main></comp-main>
<script type="module" src="./out-tsc/src/main.js"></script>
</body>
</html>
Two important things are happening now:
- The
<comp-main></comp-main>
element will render the brand new component - The
<script>
tag will load the content of the file that contains the component definition.
That's it! You already have a Single-Page Application based on LitElement and TypeScript.
Running the Application
Run the App in Development mode
Serve the application (development mode) using the following command:
npm run start
You should see an output with the host and port:
Web Dev Server started...
[web-dev-server]
[web-dev-server] Root dir: /Users/luixaviles/projects/typescript-rollup
[web-dev-server] Local: http://localhost:8000/
[web-dev-server] Network: http://192.168.1.2:8000/
[web-dev-server]
The default browser will be opened to display the index.html
content.
Run the Build Version
First, run the build
command:
npm run build
This will generate a dist
folder with the following content:
|- dist/
|- b8c5e46a.js
|- index.html
|- ... other scripts
A simple way to serve these files locally would be to use the http-server tool (a command-line http server):
http-server dist/ -o
The output of this command will show you the host and the port you need to use to access the build version of the app:
Starting up http-server, serving dist/
Available on:
http://127.0.0.1:8080
http://192.168.1.2:8080
Hit CTRL-C to stop the server
open: http://127.0.0.1:8080
Source Code of the Project
Find the complete project in this GitHub repository: typescript-rollup. Do not forget to give it a star ⭐️ and play around with the code.
Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work.