The Magic of Component-based Frontend Development

Written by giwyni | Published 2021/12/25
Tech Story Tags: object-oriented | javascript | web-development | front-end-development | software-engineering | html | dom | single-page-web-applications

TLDRAt the end of 2021, digital transformation and the need to modernize business applications is hardly new. Legacy applications are still in use by a huge number of organizations. Many CFOs are reluctant about modernizing and don’t want to rush into it. The spread of technological innovations is hampered by the human factor also known as chain of command latency. So what does it take to get a CFO on board with an app modernization project? Here are a few tips for “selling” an. app modernization initiative to your CFO.via the TL;DR App

For the last few years, web page construction is done using web components. The webpage seen by a user is a collection of many pieces of HTML and associated code. The browser renders these components on the screen as soon as it can, which means concurrently. Different parts of the page may appear on the screen at different times, however since the entire process is fast, the user sees a quick-loading web page which is good.

However, keeping all these components working coherently as required by a web application involves configuring each piece. This can be tricky since configuring each component can be done only when the component is ready and this is detected using ‘callbacks’ from the browser to the application. Here we suggest a solution to this problem:

First:

Problem Description:

Web front-end software is complex and nowadays written using web components, an object-oriented programming paradigm for User interface objects. Very very briefly each component is a web page (HTML) or part of a web page, along with a class (javascript) that handles all callbacks from the HTML. Often the component has to configure or initialized in order for it render as per the application. Such configuration information may be Titles, data for dropdowns, URLs for API calls to servers etc. This config data is needed when the component is initialized and can vary.

A simple example would be a component that could be used in two environments: test and production. In a test environment, where the URLs used for API calls by the component point to the test server. The same component in the production environment would point to the production server. Using diagrams this is shown below.

Solution Overview:

The configuration information can be obtained in a number of ways: hard-coded, from a javascript file which is part of the deployment package, storing and accessing the config information from the browser's local storage, from an API call to an end-point that returns the config information. Of these, the API call is quite elegant and versatile, since it allows all config information to be kept in one place and accessible to applications via the ubiquitous http/s API calls.

When the web application is first invoked, the browser starts loading all components declared in the HTML (HTML-dom) concurrently. The javascript class behind each component handles the callback events from the browser. Each component needs the configuration information, and the code to obtain the configuration can be placed in the handler that handles the event indicating that the component is available in the dom. An alternative to having each component fetching the configuration is to have one component fetching the configuration, and calling a method on the other components passing the configuration via parameters.

Solution Implementation:

We now follow the usual practice in showing that this method works, which is to show me the code that works!

When creating web-component applications, the easier way is to use a web-component library, which makes available a variety of pre-built components, and contains base classes that have needed boiler-plate code, and provides life-cycle events making the process of creating applications much easier. In this solution, we use one such library .. the popular LitElement library (open-sourced by Google: see LitElement ).

The example application is to show information on 4 different Electric cars in four different tabs on a single web page. The components are designed as follows:

  1. A 'main' component whose HTML has 4 tabs. Each tab has a specific child component (EV-comp)
  2. A component (EV-comp) for each tab - i.e. a specific Electric car model. These load the information for that car model (a picture of the car) into the tab

A structural view of this is:

The configuration information for each of the tab component is the URL from which to fetch the information for that car model.

The process of distributing the config information is coded in the callback when the main component is loaded. This process is:

  1. The main component gets the config information (In this article, this is 'hard-coded' within the main component for simplicity)
  2. The main component then waits until the child components(the tab components) are ready.
  3. The main component then calls the method 'passConfigInfoToChildren' on each child component, with the config information as the parameter.
  4. The 'passConfigInfoToChildren' method of the child component, uses the config information and updates its dom/HTML.

A diagram of this config data flow is:

The numbers on the arrows indicate the order of that event. Where two arrows have the same number, then between them the order could be either way.

The actual code:

The main component:

import {LitElement, html,css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import '@polymer/paper-item/paper-item';
import '@polymer/paper-tabs/paper-tab';
import '@polymer/paper-tabs';
import './evComp.js'


class Main extends LitElement {
    //define styles separttely
      static styles = css`
      .tabcontent {
        color: green;
        display: none;
      }
      paper-tabs{
        --paper-tabs-selection-bar-color : #4286f4;
        background-color:green;
      }
      paper-tab {
        border: 1px solid yellow;
      }
      paper-tab.iron-selected {
        color: blue;
        background-color: lightblue
      }
    `;

    // Define a template
    myTemplate = (title)=> html`
    <div id="header">
    <div style="width:10em;margin:auto;background-color:lightblue;font-weight:bold">
    <paper-item class="hdr">EV Car models</paper-item>
    </div>
    <!-- These are tabs (a horizontal row of buttons) -->
    <paper-tabs>
      <paper-tab class="tablinks" @click="${this.openTab}">Tesla EV</paper-tab>
      <paper-tab class="tablinks" @click="${this.openTab}">Toyota EV</paper-tab>
      <paper-tab class="tablinks" @click="${this.openTab}">Solo EV</paper-tab>
      <paper-tab class="tablinks" @click="${this.openTab}">Hyundai EV</paper-tab>
    </paper-tabs>
    </div>
    <!-- The tab content area -->
    <div style="padding-top: 6em;">
    <!-- Tab content -->
    <div class="tabcontent">
      <ev-comp title="Tesla EV"></ev-comp>
    </div>

    <div class="tabcontent">
      <ev-comp title="Toyota EV"></ev-comp>
    </div>

    <div class="tabcontent">
    <ev-comp title="Solo EV"></ev-comp>
    </div>

    <div class="tabcontent">
    <ev-comp title="Hyundai EV"></ev-comp>
    </div>

    </div>
    `;
    //property changes will cause render to be called again
    static get properties() {
      return {
        loadStatus: { type: String }
      };
    }
    constructor() {
      super();
      this.loadStatus='loaded'  //This is used to trigger rendering of page after the response from config service
    }

  async passConfigInfoToChildren(passConfigTo,config){
    //wait for child components to be available and pass on the config info
    for (const childComp of passConfigTo) {
      await childComp.updateComplete
      if (typeof(childComp.receiveConfigInfo)==='function') await childComp.receiveConfigInfo(config)
    }
  }

  async firstUpdated(changeProperties){
    let config =  {
      'Toyota EV':{
        'imgUrl':'https://toyota.scene7.com/is/image/toyota/BZ4_MY23_0018_V001-1?fmt=jpg&fit=crop&resMode=bisharp&qlt=90&wid=1696&hei=952'
      },
      'Tesla EV':{
        'imgUrl':'https://www.thedrive.com/content/2021/12/0x0-Model3_26.jpg?quality=85&width=1440&quality=70'
      },
      'Solo EV':{
        'imgUrl':'https://electrek.co/wp-content/uploads/sites/3/2016/08/solo-2.jpg?quality=82&strip=all'
      },
      'Hyundai EV':{
        'imgUrl':'https://cdn.vox-cdn.com/thumbor/_YuMe2L1LW7RNi-f8eHCKDdu_zs=/0x0:3395x2147/920x613/filters:focal(1427x803:1969x1345):format(webp)/cdn.vox-cdn.com/uploads/chorus_image/image/70155884/48639_HyundaiMotorUnveilsSEVENConceptSegment_bustingSUEVfortheIONIQBrand.0.jpg'
      }
    }
    //get the children to whom we want to pass the config-info
    let passConfigTo = this.shadowRoot.querySelectorAll('ev-comp')
    //let config="first content"
    this.passConfigInfoToChildren(passConfigTo,config)
  }

  async openTab(e) {
    let activeTabNm = e.target.innerText //name of the tab clicked
    console.log(`Clicked on ${activeTabNm}`);
    let activeTabDiv=this.shadowRoot.querySelector('ev-comp[title="'+activeTabNm+'"'+']').closest(".tabcontent")
    // Get all tab content divs and hide them
    let tabcontent = this.shadowRoot.querySelectorAll(".tabcontent");
    for (let i = 0; i < tabcontent.length; i++) {
      tabcontent[i].style.display = "none";
    }
    // Show the current tab, and add an "active" class to the button that opened the tab 
    activeTabDiv.style.display = "block";
  }

  render() {
      if (this.loadStatus == 'loading') return html`loading configs..`
      else {
        return this.myTemplate("Sms 0.0");
    }
  }
}
customElements.define('main-comp',Main)

The child component:

import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';


class EvComp extends LitElement {
    myTemplate = ()=> html`
    <div id="config">
    </div>
    `;

    constructor() {
        super();
    }

    async receiveConfigInfo(config) {
        this.shadowRoot.querySelector("#config").innerHTML=config
        let imgUrl=config[this.title].imgUrl
        this.shadowRoot.querySelector("#config").innerHTML=`<img src="${imgUrl}" />`
    }
    render() {
          return this.myTemplate();
    }
}
customElements.define('ev-comp',EvComp)

The index.html:

This is the html page that renders the main component:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
    <title>HackerNoonArticle</title>
    <script type="module" src= "src/main.js"></script>
  </head>
  <body>
     <main-comp></main-comp>
  </body>
</html>

Usage in brief:

To execute this on the browser the tools used were the transpiler snowpack (with minimal config), and the chrome browser.

Here is a screenshot of the sample app running on localhost:

Reference info:

The callback and convenience methods from LitElement used are: firstupdated - this callback indicates that the component is ready. This is used in the main component to fetch the config information and distribute it to the other components. updatecomplete - this is an event that is set when the component is ready. By awaiting this event for each child component, 'main' ensures that child components are ready to accept the configuration information.

The entire list of lit-element callbacks are here: https://lit.dev/docs/components/lifecycle/


Published by HackerNoon on 2021/12/25