Web Components using Lit
Web Component Example
When considering a good use case for a web component, I decided to create a stamp duty calculator. It’s a simple component with a few inputs and outputs, and it’s a great example of how to use web components in a real-world application.
Creating new component using Lit
Creating a new component using Lit is straightforward. Inside your project, create a new JavaScript or TypeScript file (e.g., StampDuty.js):
Most likley you will have to add styles or style library to your component. In this example I am using Tailwind CSS.
Next you can define your component properties. In this case, I have a single property called price.
firstUpdated()
is a lifecycle method that is called after the component's first update. This is a good place to initialize any state or perform any other setup that requires the component's properties to be defined.
render()
is a required method that returns a template literal that defines the component's HTML structure. In this case, the component renders a form with a few inputs and outputs. In mu example every time the component inputs change new value is calculated and displayed.
import { LitElement, html, css } from 'lit';
import { TWStyles } from '../../../static/twlit.js';
export class StampDuty extends LitElement {
static get styles() {
return [TWStyles];
}
static get properties() {
return {
price: { type: String },
};
}
firstUpdated() {
this.calculateStampDuty();
}
calculateStampDuty() {
...
}
updateStampDuty(event) {
...
}
render() {
return html` <h3 class="mb-6">Stamp Duty Calculator</h3>
<form
@change=${this.updateStampDuty}
@input=${this.updateStampDuty}
class="mb-6 md:flex"
>
<div class="w-full md:w-1/3 mb-4 pr-4">
<label
for="type"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
>I am:
</label>
<select
name="type"
id="type"
@change=${event => this.updateStampDuty(event)}
class="select w-full rounded shadow-sm select-bordered border-gray-300 outline-none p-2.5 focus:ring-gray-500 border-0 bg-white text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-5"
>
<option value="ftb">First Time Buyer</option>
<option value="rtb">Next Home</option>
<option value="shb">Additional property</option>
</select>
</div>
<div class="w-full md:w-1/3 mb-4 pr-4">
<label for="property_price"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
>Property price
</label>
<div class="relative mb-4">
<div
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
>
<span class="text-gray-500 sm:text-sm">£</span>
</div>
<input
type="text"
name="price"
id="property_price"
class="block w-full rounded-md border-0 bg-white p-2.5 pl-7 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-5"
placeholder=${this.price}
value=${this.price}
@input=${event => this.updatePrice(event)}
/>
</div>
</div>
<div class="w-full md:w-1/3 mb-4">
<label
for="stampduty"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
>Stamp duty:
</label>
<div name="stampduty" class="ml-2 flex align-middle items-center h-10"><span id="stamp-duty"></div>
</div>
</form>
`;
}
customElements.define('stampduty', StampDuty);
Usage
Easiest way to use this component is to add it to your html file.
<html>
<head>
...
<script type="module" src="./StampDuty.js"></script>
</head>
<body>
<stampDuty price="100000"></stampDuty>
</body>
</html>
Stamp Duty Calculator Example 1
Stamp Duty Calculator Example 2
Testing
I decided to use Playwright for my component library. Testing web componets can be tricky as in most cases we need to access shadow dom.
Playwright 1.30 introduced a new method elementHandle.evaluateHandle()
which allows us to access shadow dom.
import { test, expect } from '@playwright/test';
import { html } from 'lit';
test.describe('first time buyers', () => {
const testCases = [
{ price: '100000', expected: '0' },
{ price: '260000', expected: '0' },
{ price: '420000', expected: '0' },
{ price: '426000', expected: '50' },
{ price: '625000', expected: '10,000' },
{ price: '626000', expected: '18,800' },
{ price: '925000', expected: '33,750' },
{ price: '926000', expected: '33,850' },
{ price: '1000000', expected: '41,250' },
{ price: '1500000', expected: '91,250' },
{ price: '1501000', expected: '91,370' },
];
for (const { price, expected } of testCases) {
test(`${price}`, async ({ page }) => {
await page.setContent(`
<html>
<head>
<link rel="stylesheet" href="../static/tailwind.css">
</head>
<body>
<ds-tw-stampduty price="${price}"></ds-tw-stampduty>
</body>
</html>
`);
const shadowContent = await page.locator('ds-tw-stampduty');
});
}
});
test.describe('moving house', () => {
const testCases = [
{ price: '100000', expected: '0' },
...
{ price: '1501000', expected: '91,370' },
];
...
});