Home > OS >  Single HTTP request for HTML template file for multiple instances of the same Web Component
Single HTTP request for HTML template file for multiple instances of the same Web Component

Time:01-11

Say I have a Web Component defined like this:

// web-component.js
export class WebComponent extends HTMLElement {
  template = '/path/to/template.html';  
  tmpl = {};

  constructor() {
    super();
  }
    
  async connectedCallback() {
    const html = fetch(this.template).then(response => response.text());
    this.doSomething(await html);
  }
  
  doSomething(html) {
    console.log(html);
  }
}

document.addEventListener('DOMContentLoaded', customElements.define('web-component', WebComponent));

A template file like this:

//template.html
<template id="web-component">
    <h1>Text Goes Here</h1>
</template>

And a web page like this:

//index.html
    ....
    <head>
    <script type="module" src="/path/to/web-component.js"></script>
    </head>
    <body>
    <web-component>Foo</web-component>
    <web-component>Bar</web-component>
    <web-component>Baz</web-component>
    </body>
    ....

The web browser is making three http requests to fetch the same template file. I want to store the html from the template file somewhere on the client so I'm only making a single http request.

I tried this:

async connectedCallback() {
  const existing_template = document.body.querySelector('#web-component');
  console.log(existing_template);
  if (existing_template) {
    this.doSomething(existing_template);
  } else {
    const html = fetch(this.template).then(response => response.text());
    this.addTemplateToWebPage(await html);
    this.doSomething(await html);
}

addTemplateToWebPage(html) {
    const tag = document.createElement('body');
    tag.innerHTML = html;
    document.body.appendChild(tag.querySelector('template'));
}

But existing_template is always null, so I'm still making unnecessary http requests. I also tried this, with the same result:

connectedCallback() {
    this.doSomething();
  }

  async doSomething() {
    const existing_template = document.body.querySelector('#web-component');
    console.log(existing_template);
    if (existing_template) {
      this.doSomethingElse(existing_template);
    } else {
      const html = fetch(this.template).then(response => response.text());
      this.addTemplateToWebPage(await html);
      this.doSomethingElse(await html);
    }
  }

  doSomethingElse(html) {
    console.log('doing something else');
  }

How can I do this so I only have a single http request when calling the same template?

CodePudding user response:

What you're doing to the HTML isn't clear, but an easy way is to create a custom fetch wrapper for your HTML template. Something like this:

const fetchTemplate = (() => {
  const cache = new Map();
  return async (path, options) => {
    if(cache.has(path)) return cache.get(path);
    const res = await fetch(path, options);
    const data = await res.text();
    cache.set(path, data);
    return data;
  };
})();

This presents a much more streamlined approach and can be easily adapted for other templates.

Usage:

async connectedCallback() {
  const html = await fetchTemplate(this.template);
  this.doSomething(html);
}

CodePudding user response:

Since the original question refers to an "HTML" template, I'm not sure if, technically, this really answers it, but it has been a few days so I will relate how I solved my problem. Basically I just changed template.html to template.js, turned the template html code into a javascript literal, and export/imported it into my class. I guess the browser automatically caches the http request because it's only making a single http call. I didn't transpile or use any build tools ... it just worked in the most recent versions of Firefox, Chrome and Edge (which is all I'm concerned about). It has been awhile since I have played with this stuff and I'm kinda blown away at the current browser support for vanilla javasript modules. When we finally get HTML imports that play nice with javascript modules, I'm hoping this approach makes it easy to refactor the code to accommodate the new technology.

My file structure is:

/www
- index.html
- /components
-- /my-component
--- component.js
--- template.js

index.html

<html>
    <head>
        <script type="module" src="/components/my-component/component.js">  </script>
    </head>
    <body>
        <my-component>One</my-component>
        <my-component>Two</my-component>
        <my-component>Three</my-component>
    </body>
</html>

template.js

export const template = `
<template id="my-template">
    <style>
        h1 { color: dimgray }
    </style>

    <div id="wrapper">
        <h1>Need Text</h1>
    </div>
</template>
`;

component.js

import { template } from './template.js';
export class MyComponent extends HTMLElement {
  shadow = {};
  tmpl = {};
  
  constructor() {
    super();
    this.shadow = this.attachShadow({mode:'open'});
   }
   
   connectedCallback() {
    this.setVars();
    this.build();
    this.render();
   }
   
   setVars() {
    const tmpl = this.setTemplate(template);
    this.tmpl = tmpl.querySelector('template').content;
   }
   
   setTemplate(html) {
     const range = document.createRange();
     return range.createContextualFragment(html);
   }
   
   build() {
    // do something 
   }
   
   render() {
    this.innerHTML = "";
    this.shadow.append(this.tmpl);
   }
  }
   
  document.addEventListener('DOMContentLoaded', () => customElements.define('my-component', MyComponent));
  • Related