TransWikia.com

How will I access the components in the script inside a template?

Stack Overflow Asked by PerduGames on November 29, 2020

I would like reuse my html components that contains some javascript code, so for simplify I bring one simple example:

index.html:

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <my-component></my-component>
    <script src="index.js"></script>
  </body>
</html>

my-component.html:

<template>
  <div id="something"></div>
  <script>
    // It doesn't work, this here is "window"
    document.getElementById("something").innerHTML = "Something"
  </script>
</template>

index.js:

window.makeComponent = (function () {
    function fetchAndParse(url) {
    return fetch(url, {mode: "no-cors"})
        .then(res => res.text())
        .then(html => {
        const parser = new DOMParser()
        const document = parser.parseFromString(html, 'text/html')
        const head = document.head
        const template = head.querySelector('template')
        return template
        })
    }
    function defineComponent(name, template) {
    class UnityComponent extends HTMLElement {
        connectedCallback() {
        const shadow = this.attachShadow({mode: 'open'})
        shadow.appendChild(document.importNode(template.content, true))
        }
    }
    return customElements.define(name, UnityComponent)
    }
    function loadComponent (name, url) {
    fetchAndParse(url).then((template) => defineComponent(name, template))
    }

    return {loadComponent}
}())
makeComponent.loadComponent("my-component", "my-component.html")

I can with this code, but it copy all variables of the script to window:

<template>
  <div id="something"></div>
  <style onload="templFunc.apply(this.getRootNode())"></style>
  <script>
    function templFunc() {
    // It works
    let text = "Something"
    this.querySelector('#something').innerHTML = text
    // but...
    console.log(window.text) // => "Something"
    }
  </script>
</template>

It doesn’t make a sense, if the script is inside the template at least should can access the elements inside the template, else the template is almost not util for the javascript, so, I can’t understand the intention of use script inside the template or how to reuse the web components that use javascript, Is it wrong do this?

So, How to I access the components in the script inside a template without copy all script variables to window?

2 Answers

As you found out <script> inside a <template> runs in Global scope

If you use Angular, note Angular bluntly removes all <script> content from Templates.

One workaround is to add an HTML element that triggers code within Element scope.

<img src onerror="[CODE]"> is the most likely candidate:

This then can call a Global function, or run this.getRootNode().host immediatly.

<template id=scriptContainer>
  <script>
    console.log("script runs in Global scope!!");

    function GlobalFunction(scope, marker) {
      scope = scope.getRootNode().host || scope;
      console.log('run', marker, 'scope:', scope);
      scope.elementMethod && scope.elementMethod();
    }
  </script>

  <img src onerror="(()=>{
    this.onerror = null;// prevent endless loop if function generates an error
    GlobalFunction(this,'fromIMGonerror');
  })()">

</template>

<my-element id=ONE></my-element>
<my-element id=TWO></my-element>

<script>
  console.log('START SCRIPT');
  customElements.define('my-element',
    class extends HTMLElement {
      connectedCallback() {
        console.log('connectedCallback', this.id);
        this.attachShadow({ mode: 'open' })
            .append(scriptContainer.content.cloneNode(true));
      }
    });

</script>

More detailed playground, including injecting SCRIPTs, at: https://jsfiddle.net/CustomElementsExamples/g134yp7v/

Answered by Danny '365CSI' Engelman on November 29, 2020

Here is the solution,

my-component:

<template>
  <div id="something"></div>
  <script>
    makeComponent.getComponent("my-component", "something").innerHTML = "Something"
  </script>
</template>

index.js:

window.makeComponent = (function () {
  function fetchAndParse(url) {
    return fetch(url, { mode: "no-cors" })
      .then((res) => res.text())
      .then((html) => {
        const parser = new DOMParser();
        const document = parser.parseFromString(html, "text/html");
        const head = document.head;
        const template = head.querySelector("template");
        return template;
      });
  }
  function defineComponent(name, template) {
    class UnityComponent extends HTMLElement {
      connectedCallback() {
        const shadow = this.attachShadow({ mode: "open" });
        this.setAttribute("id", name);
        shadow.appendChild(document.importNode(template.content, true));
      }
    }
    return customElements.define(name, UnityComponent);
  }
  function getComponent(host, query) {
    return document.getElementById(host).shadowRoot.querySelector(query);
  }
  function loadComponent(name, url) {
    fetchAndParse(url).then((template) => defineComponent(name, template));
  }

  return { getComponent, loadComponent };
})();
makeComponent.loadComponent("my-component", "my-component.html");

However I think that this is not the better way, maybe I need use the events here, and pass the shadow scope to a listener that is called in the script tag in the template, but I don't know how to pass the scope to the event yet.

Up:

With events:

my-component:

<template>
  <div id="something"></div>
  <script>
     document.addEventListener("custom-event", (e) => {
         console.log(e.detail.target.shadowRoot.getElementById("date-year"));
     })
  </script>
</template>

index.js:

window.makeComponent = (function () {
  function fetchAndParse(url) {
    return fetch(url, { mode: "no-cors" })
      .then((res) => res.text())
      .then((html) => {
        const parser = new DOMParser();
        const document = parser.parseFromString(html, "text/html");
        const head = document.head;
        const template = head.querySelector("template");
        return template;
      });
  }
  function defineComponent(name, template) {
    class UnityComponent extends HTMLElement {
      connectedCallback() {
        const shadow = this.attachShadow({ mode: "open" });
        shadow.appendChild(document.importNode(template.content, true));
        const event = new CustomEvent("custom-event", {'detail': {
            target: this
        }});
        document.dispatchEvent(event);
      }
    }
    return customElements.define(name, UnityComponent);
  }
  function loadComponent(name, url) {
    fetchAndParse(url).then((template) => defineComponent(name, template));
  }

  return { loadComponent };
})();
makeComponent.loadComponent("my-component", "my-component.html");

However, I prefer the first solution even. But if you need of nested components the first doesn't work, you need of the second.

Answered by PerduGames on November 29, 2020

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP