Home > Software design >  Run javascript from external html file
Run javascript from external html file

Time:10-03

So I want to create and use native web components and describe them as HTML files with markup, CSS and Javascript bundled together in one file like how Vue does .vue files. The components would be loaded on a page from an external components.html file, for example, via fetch().

So far I can load the HTML and CSS without a problem, however the Javascript is "dead", the browser doesn't run it or recognize it. As I understand Vue requires a build step in order to 'compile' .vue files. There is no live loading of .vue. Well, I want to do live loading. Is that silly?

All the native web component 'frameworks' I see out there define their components entirely in Javascript but I want to do it more declaratively with HTML and without template literal HTML definitions. I basically want to have methods and possibly data attached to custom elements when they are instantiated. Also, eval() is not a viable option, right?

I guess it is good that the Javascript initially comes in dead, so it doesn't pollute the global scope. But then how can I read it and basically inject it into the custom element class?

Here is an example of my code, which works fine except for loading the Javascript in the component, since the methods object does not exist anywhere.

components.html

<template id="my-dog">
    <style>
        .fur {color: brown}
    </style>
    
    <img src="dog.gif" onclick="speak">
    <p class="fur">This is a dog</p>
    
    <script>
        methods = {
            speak(){alert("Woof!");}
        }
    </script>
</template>

template creation script

//(skipping the part where I fetch components.html)
//(and inject them into the page)
//(and then iterate over each template until...)
templates.forEach(x=>{createTemplate(x)}) //ie. createTemplate('my-dog')

function createTemplate(elementName){
    
    /*various code*/

    let elemClass =  class extends HTMLElement {
        constructor() {
            super(); 
                
            this.attachShadow({mode: 'open'})
                .appendChild(templateContent.cloneNode(true));

        }
    }
    // THIS WORKS! But how can I do the same for speak() function
    // and other functions and variables defined in the template?
    elemClass.prototype['test'] = ()=>{console.log("This is a test")}

    customElements.define(elementName, elemClass);
}

CodePudding user response:

Loading external HTML/CSS

See Dev.To blogpost: https://dev.to/dannyengelman/load-file-web-component-add-external-content-to-the-dom-1nd

Loading external <script>

<script> inside a <template> runs in Global scope once you clone its contents to the DOM

I have not tried Vue; Angular bluntly removes all <script> content from Templates.

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

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

This then can call a GlobalFunction, or run this.getRootNode().host immediately.

console.log showing scope when executing Custom Elements:

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

<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(); // see JSFiddle
    }
  </script>

  <img src onerror="(()=>{
    // 'this' is the IMG scope, this.getRootNode().host is the Custom Element
    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, "clone Template");
        this.attachShadow({ mode: 'open' })
            .append(scriptContainer.content.cloneNode(true));
      }
    });
</script>

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

it is al about getting your scope right

Since Scripts run in Global scope you can get variables clashes.

let foo=42 in a template script will fail (in most browsers) if there is a GLOBAL let foo="bar";

So use an esoteric trigger "GlobalFunction" name and do not create global variables.

advanced

  • Yes, the same template script runs for every connected <my-element>

  • <img src one rror="GlobalFunction.bind(this.getRootNode().host)"> is ignored

  • This will run; but do not forget to reset that error function, or you will run into endless JS loops

    <template id=scriptContainer>
    <script>
    console.log("script runs in Global scope!!");
    
    function GlobalFunction() {
      console.log(this); // proper Custom Element scope
    }
    </script>
    
    <img src onerror="GlobalFunction.bind(this.getRootNode().host)(this.error=null)">
    

````

  • Less DOM invasive than an <IMG> is to use <style onl oad="...">

CodePudding user response:

So I have managed to get something working using the method described here, which I discovered after following the link in Danny Engelman's comment. The key is basically to use an iframe, rather than AJAX until the day that we have HTML imports in browsers. By using the iframe method the code inside <script> tags stays alive. Then it gets pulled into your page and the iframe is deleted. So long as the code is still wrapped in a <template> tag it does not touch the global scope or actually do anything, until it is instantiated in a custom element.

Questions still remain for me about how to best handle global scope issues, but for now it works to just use a predetermined global variable or variables that are defined inside templates, and in the connectedCallback I check for those variables. If they exist I transfer their information onto the custom element, then wipe the global variables for the next connected element. There is probably some better way to do it.

Working example here: https://codepen.io/lomacar/project/editor/ZPQJgg

Now what would be nice is if I could read the scripts in the templates before they are used and store their code in one universal location that each custom element points to, rather than processing their code every time a custom element is connected and without ever spilling their guts into global namespace.

  • Related