Introduction to Web Components.
We all have built web pages using HTML, CSS, and JavaScript. Generally, we write the HTML tags and render them on the page. Sometimes, we have to repeat tags to render the same type of UI elements. It makes the page messy. And also, adding styles to the elements make an impact on multiple tags and elements. We have to override the style for each different element. Developers always try to work more in less time.
We try to follow “Don’t Repeat Yourself (DRY)” but just using HTML, CSS, and JavaScript, is not possible. Web components make it possible.
Web components are a set of web platform APIs that allow us to make new custom HTML tags or elements with encapsulated functionality that can be reused multiple times and utilized on our web pages. It helps us share data between components and saves our time and energy.
<user-avatar
class="mr-2x"
name="${name}"
shape="${this.shape}"
.imageURL="${imageURL}"
.withBorder="${this.withBorder}"
>
</user-avatar>
This is the simple example of custom component. Properties such as name, shape, imageURL, withBorder are pass into the component in the form of component attributes.
If this looks confusing, don’t worry, you will be able to build one web application where we can Add, Edit, Delete and List the posts, by the end of this article.
Things you need to know before diving into the tutorial.
Custom Elements
Custom Elements help developers build their customizable element or HTML tags with encapsulated functionality which can be helpful for them in their web applications. Let’s say we have to create a component that displays user details with images. You can create a element where you can structure it as you want.Shadow DOM
Shadow DOM is a way to encapsulate the styling and markup of your components. It prevents overriding of styles. It is the concept of scoped style. It does not replace the styling of parent or child components. It behaves separately which allows us to write the styling of the same class or id in a separate component.ES Modules
ES Modules defines the inclusion and reuse of JS documents in a standard-based, modular, performant way. Web Components follow the ES Modules pattern.HTML Templates
HTML Templates are ways to insert HTML structures that only get rendered when the main template is rendered. Whatever we write inside tag will get rendered.
What is Polymer?
It is an open-source JavaScript library based upon Web Components. It is developed by Google. Polymer helps us to create custom elements for building web applications. It's much easier and faster to create custom elements that work like DOM elements.
What is LitElement?
It is a simple base class that helps us to create a web component. It uses lit-html to create the web components using Shadow DOM and manage the properties and attributes. The element is updated whenever the properties of the element are changed.
This is the basic structure of LitElement to create a new component.
import { LitElement, html, css } from 'lit-element';
// Creating MyElement component extending the LitElement Class.
class MyElement extends LitElement {
// Add Styles for the component
static get styles() {
return [
css `
:host {
display:block;
}
`];
}
// Add Properties which will be used into the components.
static get properties() {
return {
myString: { type: String },
};
}
// Initialize all the properties and bind the function into the constructor.
constructor() {
// Always call super first in constructor
super();
this.myString = 'Hello World';
}
// Add the html structure for the component you want to build.
render() {
return html`
<p>${this.myString}</p>
`;
}
}
// register custom element on the CustomElementRegistry using the define() method
customElements.define('my-element', MyElement);
Now, Let’s Dive into CRUD operations using Polymer and LitElement. We are going to develop an application to Add, Edit, Delete, and listing the Post.
The GitHub Repo for this tutorial is available here. I recommend checking it out, because its got this whole tutorial.
Okay, so lets get started!
Download the starter file from here
Setup
Clone the repo and open it with the Text Editor. Delete the docs, docs-src, and test. Go to the dev folder and move the index.html into the root folder. Afterward, you can delete the dev folder too.
Install dependencies:
npm i
After that install @vaadin/router. It is a client-side router library developed in JavaScript. It is mostly used in Web Components based web applications. It is a lightweight router library. It has different features such as child routes, async routes resolution, and many more.
npm install --save @vaadin/route
Create an src folder. After that create components folder inside it. Then create a file named post-app.js inside it. Add the below given code into the post-app.js file.
import {LitElement, html} from 'lit';
class PostApp extends LitElement {
firstUpdated() {
const el = this.shadowRoot.querySelector('main');
}
render() {
return html` <main></main> `;
}
}
customElements.define('post-app', PostApp);
Here main is the DOM where every other component gets rendered.
Create a folder named router inside the src folder and also router.js into the newly created folder.
import { Router } from '@vaadin/router';
/**
* Initializes the router.
*
* @param {Object} outlet
*/
function initRouter(outlet) {
const router = new Router(outlet);
router.setRoutes([
{
path: '/',
component: 'landing-page',
action: () => {
import('../components/landing-page/landing-page');
},
},
]);
}
export default initRouter;
Now import the initRouter into the post-app.js
import initRouter from '../router/router';
Call the initRouter function inside the firstUpdated.
firstUpdated() {
const el = this.shadowRoot.querySelector('main');
initRouter(el);
}
Open index.html of the root folder.
Add the script tag inside the head tag.
<script type="module" src="./src/components/post-app.js"></script>
Add the post-app component tag into the body tag.
<body>
<post-app></post-app>
</body>
We will be using the paper elements which are a collection of custom UI components. We can simply install it and import it in the file we want to use and add the tag of that element in the form of an HTML Tag. We are going to use a paper-card element to set the background container for the page. So, let’s install the paper-card package.
npm install @polymer/paper-card --save
Create the landing-page folder Inside the components folder and also create landing-page.js into the newly created folder.
import { css, html, LitElement } from 'lit';
import '@polymer/paper-card/paper-card';
class LandingPage extends LitElement {
static get properties() {
return {};
}
static get styles() {
return [
css`
.main-wrapper,
paper-card {
height: 100vh;
display: flex;
flex-direction: column;
}
`,
];
}
constructor() {
super();
}
render() {
return html` <div class="main-wrapper">
<paper-card>
<div class="menu-wrapper">
<a href="/home">Home</a>
<a href="/post">Posts</a>
</div>
<div>
<slot></slot>
</div>
</paper-card>
</div>`;
}
}
customElements.define('landing-page', LandingPage);
We have added the URL for the Home and Posts page which is rendered into all the pages because we have added /home and /post as children of a home directory inside the router. Now the remaining page DOM is rendered inside the slot. A slot is a place where we can pass anything we want to render into the component.
Let’s say we have a fruit component with the title fruit and we want to pass the image into the component as a children's DOM.
fruit_component.js
<div>
${this.title}
<slot></slot>
</div>
Now we can pass the image as children in this way
<fruit_component>
<img src=”/images/img.jpeg” />
</fruit_component>
Whatever we pass between the components displays into the slot.
Let’s open the terminal and run
npm run serve
Copy the local URL and paste it into the browser and open it.
It shows the menu list which we have added into the landing page component.
It will not work now. As we have not set up to display its content.
router.setRoutes([
{
path: '/',
component: 'landing-page',
action: () => {
import('../components/landing-page/landing-page');
},
},
{
path: '/',
component: 'landing-page',
children: [
{
path: '/',
redirect: '/post',
},
{
path: '/post',
component: 'post-list',
action: async () => {
await import('../components/posts/post-list.js');
},
},
{
path: '/home',
component: 'home-page',
action: () => {
import('../components/home-page/home-page');
},
},
{
path: '(.*)+',
component: 'page-not-found',
action: () => {
import('../components/page-not-found');
},
},
],
},
]);
Now create a home-page folder inside the components folder and create the home-page.js file inside it.
import { LitElement, css, html } from 'lit';
class HomePage extends LitElement {
static get styles() {
return [css``];
}
render() {
return html`
<div>
Home Page
</div>
`;
}
}
customElements.define('home-page', HomePage);
Create a posts folder inside the components folder and create the post-list.js file inside it.
import { css, html, LitElement } from 'lit';
class PostList extends LitElement {
static get properties() {
return {};
}
static get styles() {
return [css``];
}
constructor() {
super();
}
render() {
return html`
<div>
Post List
</div>
`;
}
}
customElements.define('post-list', PostList);
Now refreshing the page, we can see the text ‘Home page’ while clicking on Home and ‘Post List’ while clicking on Posts.
Retrieve Operation
Now let’s create a new component named ‘table-view’ to display the table. Let’s create a folder named common inside the src folder. And create a file named index.js and table-view.js
Inside index.js let’s import the table-view.js
import ‘./table-view.js’;
Before opening the table-view.js, let’s install these packages which we will be using later in our new component.
npm install --save @polymer/paper-input
npm install --save @polymer/paper-dialog
npm install --save @polymer/paper-button
Open table-view.js and add the following code.
import { LitElement, html, css } from 'lit';
import '@polymer/paper-input/paper-input';
import '@polymer/paper-dialog/paper-dialog';
import '@polymer/paper-button/paper-button';
export class TableView extends LitElement {
static get properties() {
return {
posts: { type: Array },
};
}
static get styles() {
return [
css`
:host {
display: block;
}
table {
border: 1px solid black;
}
thead td {
font-weight: 600;
}
tbody tr td:last-child {
display: flex;
flex-direction: row;
margin: 0px 12px;
}
.mr {
margin-right: 12px;
}
.dflex {
display: flex;
flex-direction: column;
}
.input-container {
margin: 4px 4px;
}
paper-dialog {
width: 500px;
}
.edit-button {
background-color: green;
color: white;
}
.delete-button {
background-color: red;
color: white;
}
.add-button {
background-color: blue;
color: white;
}
.ml-auto {
margin-left: auto;
}
`,
];
}
constructor() {
super();
}
renderAddButton() {
return html`<div class="ml-auto">
<paper-button raised class="add-button">Add</paper-button>
</div>`;
}
render() {
return html`
<div class="dflex">
${this.renderAddButton()}
<div>
<table>
<thead>
<tr>
<td>S.No.</td>
<td>Title</td>
<td>Description</td>
<td>Action</td>
</tr>
</thead>
<tbody>
${this.posts.map((item, index) => {
return html`
<tr>
<td>${index + 1}</td>
<td>${item.title}</td>
<td>${item.description}</td>
<td>
<div class="mr">
<paper-button raised class="edit-button">
Edit
</paper-button>
</div>
<div>
<paper-button raised class="delete-button">
Delete
</paper-button>
</div>
</td>
</tr>
`;
})}
</tbody>
</table>
</div>
</div>
`;
}
}
customElements.define('table-view', TableView);
We have to add a table-view component into post-list.js so that when we click on a post, we can see the table on that page. We have to pass the posts data into the table-view component. For that, we have to create a property to store the data of posts. Open post-list.js and add new property into the property section.
static get properties() {
return {
posts: { type: Array },
};
}
After creating the property let’s initialize it into a constructor. Since we have not used any API, we can simply add dummy data into it.
constructor() {
super();
this.posts = [
{
id: 1,
title: 'Title 1',
description: 'This is description of post',
},
{
id: 2,
title: 'Title 2',
description: 'This is description of post',
},
{
id: 3,
title: 'Title 3',
description: 'This is description of post',
},
];
}
Inside the render function, let’s call the table-view component and pass the posts as a property of the table-view component.
render() {
return html`
<div>
<h2>Post Lists</h2>
<div>
<table-view .posts="${this.posts}"></table-view>
</div>
</div>
`;
}
Now we can see our page as displayed below.
Add Operation
Now let’s work on adding an item. We have already added an Add button into our component.
Now let’s update the renderAddButton function by adding the click action into it.
renderAddButton() {
return html`<div class="ml-auto" @click="${() => this.toggleDialog()}">
<paper-button raised class="add-button">Add</paper-button>
</div>`;
}
To make the button actionable let’s create a toggleDialog function below this function. Before creating the function let’s add operation and selectedItem properties into the properties section.
static get properties() {
return {
posts: { type: Array },
operation: { type: String },
selectedItem: { type: Object },
};
}
We will have these lists of properties after adding those properties. Also, we have to initialize the newly added properties into the constructor.
this.operation = 'Add';
this.selectedItem = {};
this.toggleDialog = this.toggleDialog.bind(this);
Now we can use these properties in the toggleDialog function.
toggleDialog(item) {
if (item) {
this.operation = 'Edit';
this.selectedItem = item;
} else {
this.operation = 'Add';
}
}
Toggle dialog will try to open the dialog, so let’s add a dialog component. We will use paper-dialog.
openAddEditDialog() {
return html`<paper-dialog>
<h2>${this.operation} Post</h2>
<div class="input-container">
<paper-input
label="Title"
@input="${(event) => this.setItemValue('title', event.target.value)}"
value="${this.selectedItem.title || ''}"
></paper-input>
<paper-input
label="Description"
value="${this.selectedItem.description || ''}"
@input="${(event) =>
this.setItemValue('description', event.target.value)}"
></paper-input>
</div>
<div class="buttons">
<paper-button dialog-confirm autofocus @click="${this.onAcceptBtnClick}"
>${this.operation === 'Add' ? 'Save' : 'Update'}</paper-button
>
<paper-button dialog-dismiss @click="${this.closeDialog}"
>Cancel</paper-button
>
</div>
</paper-dialog>`;
}
Paper card components need to open when clicked on the Add button. To open the dialog box lets add
this.shadowRoot.querySelector('paper-dialog').open();
at the end of the toggleDialog
Function and add ${this.openAddEditDialog()}
before the last div inside render function. This function will open the dialog box. And after opening the dialog we have to close the dialog box. For this let’s add the closeDialog
function.
closeDialog() {
this.shadowRoot.querySelector('paper-dialog').close();
this.selectedItem = {};
}
Here, if we have selected any item to edit, then we have to clear it because it will store the data of the currently selected post item.
Here we have a Title and Description field to add posts. We have made a common dialog box for Add and Edit of posts. This will help us not to create the same component repeatedly.
When opening the dialog, we have to set the button name as Save while adding and Update while editing the post. That’s why we have added the condition on the accept button and also the title to display when the dialog box is opened. Add Post is shown when the Add button is clicked and Edit Post is shown when the Edit button is clicked.
Now we have to get the value of Title and Description when typed into the input field. To do so, we have to add the new property named item in the properties section.
item: { type: Object },
Also initialize it into the constructor
.
this.item = {};
Now create a function named setItemValue
below the openAddEditDialog
function.
setItemValue(key, value) {
this.item = {
...this.item,
[key]: value,
};
}
Paper input has the @input
property which calls a function to add the item into the variable.
@input="${(event) => this.setItemValue('title', event.target.value)}"
This will pass the key and the value to the setItemValue
function and will create the object.
We have added the @click
action in one of the paper buttons inside the paper-dialog component.
@click="${this.onAcceptBtnClick}"
Whenever clicked the onAcceptBtnClick
function is called. So, we have to create that function and also bind it inside the constructor
.
onAcceptBtnClick() {
if (this.operation === 'Add') {
this.item = {
id: this.posts.length + 1,
...this.item
};
this.posts = [...this.posts, this.item];
}
}
this.onAcceptBtnClick = this.onAcceptBtnClick.bind(this);
When the operation value is ‘Add’, the new item will be added to posts.
Now the Add function is completed. We can add new data to the post.
Edit Operation
It’s time to Edit the Post.
To edit the post, we have to add the @click
action into the edit button. So, let’s update the edit button inside the table.
<div class="mr" @click="${() => this.toggleDialog(item)}">
<paper-button raised class="edit-button">
Edit
</paper-button>
</div>
we have to update the setItemValue
function. We have set selected items that we have chosen to edit on the selectedItem
property on the toggleDialog
function. We can now update the setItemValue
function. When the operation
is set to Edit, it will update on this.selectedItem
property when we update the value.
setItemValue(key, value) {
if (this.operation === 'Edit') {
this.selectedItem = {
...this.selectedItem,
[key]: value,
};
} else {
this.item = {
...this.item,
[key]: value,
};
}
}
Now we have to update the onAcceptBtnClick
function where the updated post is replaced with the new one.
onAcceptBtnClick() {
if (this.operation === 'Add') {
this.item = {
id: this.posts.length + 1,
...this.item
};
this.posts = [...this.posts, this.item];
} else {
this.posts = this.posts.map((post) => {
if (post.id === this.selectedItem.id) {
return this.selectedItem;
}
return post;
});
}
}
This will end up in the Edit function of the post.
Delete Operation
Now let’s move on to the Delete function of the post.
Firstly, we have to add @click
action into the delete button.
<div @click="${() => this.handleOnDelete(item)}">
<paper-button raised class="delete-button">
Delete
</paper-button>
</div>
Now we have to create the handleOnDelete
function and bind it into the constructor.
handleOnDelete(item) {
this.posts = this.posts.filter((post) => {
return post.id !== item.id;
});
}
this.handleOnDelete = this.handleOnDelete.bind(this);
Here, the post item which we want to delete is passed into the function and we compare its ID with the post inside the Posts array. After that, the post is deleted from the posts array.
In this way, we can do a simple CRUD operation using PolymerJS and LitElement.
Top comments (0)