DEV Community

Cover image for Getting Started with PolymerJS and LitElement
mukeshkarna
mukeshkarna

Posted on

Getting Started with PolymerJS and LitElement

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Add the post-app component tag into the body tag.

<body>
  <post-app></post-app>
</body>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Now we can pass the image as children in this way

<fruit_component>
  <img src=/images/img.jpeg />
</fruit_component>
Enter fullscreen mode Exit fullscreen mode

Whatever we pass between the components displays into the slot.

Let’s open the terminal and run

npm run serve
Enter fullscreen mode Exit fullscreen mode

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');
          },
        },
      ],
    },
  ]);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 },
    };
}
Enter fullscreen mode Exit fullscreen mode

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',
        },
    ];
}
Enter fullscreen mode Exit fullscreen mode

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>
    `;
}
Enter fullscreen mode Exit fullscreen mode

Now we can see our page as displayed below.

Screen Shot 2021-08-15 at 19.22.14

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>`;
  }
Enter fullscreen mode Exit fullscreen mode

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 },
    };
  }
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Now we can use these properties in the toggleDialog function.

toggleDialog(item) {
    if (item) {
        this.operation = 'Edit';
        this.selectedItem = item;
    } else {
        this.operation = 'Add';
    }
}
Enter fullscreen mode Exit fullscreen mode

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>`;
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode


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 = {};
  }
Enter fullscreen mode Exit fullscreen mode

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 },
Enter fullscreen mode Exit fullscreen mode

Also initialize it into the constructor.

this.item = {};
Enter fullscreen mode Exit fullscreen mode

Now create a function named setItemValue below the openAddEditDialog function.

setItemValue(key, value) {
    this.item = {
        ...this.item,
        [key]: value,
    };
}
Enter fullscreen mode Exit fullscreen mode

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)}"
Enter fullscreen mode Exit fullscreen mode

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];
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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,
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
    });
  }
Enter fullscreen mode Exit fullscreen mode
this.handleOnDelete = this.handleOnDelete.bind(this);
Enter fullscreen mode Exit fullscreen mode

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.

Oldest comments (0)