1

I have the following code, which creates a custom element, encapsulated with Shadow DOM:

'use strict'
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {

    var root = this.createShadowRoot();
    var divEl = document.createElement('div');
    divEl.setAttribute("id", "container");
    divEl.innerHTML =
        "<input id='input' type='text'>"
      + "<br>"
      + "Result: <span id='result'></span>"
      + "<br><button onclick='performTask()'>Run</button>";
    root.appendChild(divEl);

};
document.registerElement('custom-ele', {
    prototype: proto
});

The idea is, when 'Run' is clicked, the input would be taken from the input element and processed (in performTask()), then the output placed into '#result'. My two questions are:

  1. How would I grab the value from the input field in the Shadow DOM?
  2. How would I place the output into #result?

This previous stack overflow post looks like it would have answered my question, but all the suggested links are no longer valid so am wondering if anyone could point me in the right direction :)

P.S. I'd rather not use templates since HTML Imports are not being supported by all browsers and I want all of my custom element code contained in one file.

Community
  • 1
  • 1
Nodeocrat
  • 816
  • 2
  • 14
  • 29

2 Answers2

2

Turns out you can add functions to the shadow root itself, then you can just call this.parentNode.fn() on the shadow roots direct children to access the shadowRoot...

proto.createdCallback = function() {

    let root = this.createShadowRoot();

    root.innerHTML = "<input id='input' type='text'>"
        + "<br>"
        + "Result: <span id='result'></span>"
        + "<br><button onclick='this.parentNode.process()'>Run</button>";

    this.shadowRoot.process = function() {
        let spanEle = this.querySelector('span');
        let inputEle = this.querySelector('input');
        spanEle.textContent = performAlgorithm(inputEle.value.split(','));
    };
};
document.registerElement('custom-ele', { prototype: proto });

(Thanks to MarcG for giving me the initial insight)

Nodeocrat
  • 816
  • 2
  • 14
  • 29
  • I see this as a rather unorthodox way of doing things. You are adding methods to the `shadowRoot` object instead of adding them to the custom element where they belong. What for? Also, how do you add the event listener to the button, and where do you define the `performAlgorithm` function? – Marcelo Glasberg Jan 07 '16 at 18:32
  • performAlgorithm is in the same file, and the reason I added process() to the shadow root was so I can use markup instead of mashing the whole thing together with javascript which is very tedious. There's no event listener added in this case, it is called via the onclick attribute 'this.parentNode.process()' I can't add it to the element because it is not accessible from elements in the shadow root (shadow roots parentNode is null) – Nodeocrat Jan 08 '16 at 10:45
  • 1
    @Ashley, you shoud avoid using inline script (the `onclick` expression) for security reasons. Instead you can define it in the `createdCallback` (or `attachedCallback`) directly: `this.shadowRoot.querySelector( "button" ).onclick = function () { this.parentNode.process() }` – Supersharp Jan 08 '16 at 11:33
  • For security reasons? Lol. @Ashley: I'm glad I was of help. Bye. – Marcelo Glasberg Jan 08 '16 at 19:35
2

WITH CLOSURE

You can use the method querySelector on your Shadow DOM root to get inside elements:

'use strict'
var proto = Object.create( HTMLElement.prototype )
proto.createdCallback = function ()
{
    //HTML ROOT
    var root = this.createShadowRoot()
    root.innerHTML = "<input id='input' type='text'>"
        + "<br>"
        + "Result: <span id='result'></span>"
        + "<br><button>Run</button>"

    //UI
    var buttonEle = root.querySelector( "button" )
    var inputEle = root.querySelector( "input" )
    var spanEle = root.querySelector( "#result" )
    buttonEle.onclick = function ()
    {
        var input = inputEle.value
        // do some processing...
        spanEle.textContent = input
    } 
}
document.registerElement( 'custom-ele', { prototype: proto } )

NB: you can use template without HTML Imports, in the same page. See the following snippet:

<html>

<body>
  <custom-ele></custom-ele>

  <template id="custelem">
    <input id='input' type='text'>
    <br>Result:
    <span id='result'></span>
    <br>
    <button>Run</button>
  </template>

  <script>
    var proto = Object.create(HTMLElement.prototype)
    proto.createdCallback = function() {
      //HTML ROOT
      var root = this.createShadowRoot()
      root.innerHTML = custelem.innerHTML

      //UI
      var buttonEle = root.querySelector("button")
      var inputEle = root.querySelector("input")
      var spanEle = root.querySelector("#result")
      buttonEle.onclick = function() {
        var input = inputEle.value
          // do some processing...
        spanEle.textContent = input
      }
    }
    document.registerElement('custom-ele', {
      prototype: proto
    })
  </script>


</body>

</html>

WITHOUT CLOSURE

If you don't want to use closure, you can declare a method called handleEvent on your custom element, and add an Event Listener that will redirect on it:

proto.createdCallback = function ()
{
    //HTML ROOT
    var root = this.createShadowRoot()
    root.innerHTML = custelem.innerHTML

    //EVENT
    var buttonEle = root.querySelector( "button" )
    buttonEle.addEventListener( "click", this )
}

proto.handleEvent = function ( ev )
{
    var inputEle = this.shadowRoot.querySelector( "input" )
    var spanEle = this.shadowRoot.querySelector( "#result" )
    // do some processing...
    spanEle.textContent = inputEle.value
}
Supersharp
  • 29,002
  • 9
  • 92
  • 134
  • Well, not exactly. My solution will let createdCallback be garbage collected, while yours will not. – Marcelo Glasberg Jan 05 '16 at 19:03
  • 1
    Thanks for the reply. In your code snippet, buttonEle.onclick has a reference to inputEle which is part of createdCallback's scope... won't that prevent createdCallback from being garbage collected in the same way as my solution? As for templates, I'd prefer all my custom element code to be confined to one file so that all I have to do is import a single file, then be able to use the new element with minimal effort. – Nodeocrat Jan 05 '16 at 22:47
  • @CodeMedic Yes my first example is using the closure context because I consider it is the best and fastest way. I will post a second example without closure context use. Snippet is also modifie to merge javascript and html code. – Supersharp Jan 07 '16 at 12:54
  • @MarcG. You're wrong. Your `bind` call will generate a local context much bigger than a simple closure. `Bind` relies on a closure itself so in the GC point of view it will no help, sorry... Also your answer was incorrect and uncomplete when I posted. Actually, it is still buggy, unprecise, and... dozens times slower than mine. EXACT?! – Supersharp Jan 07 '16 at 13:21
  • @Supersharp: First you copied the `querySelector` from my answer. Then now you updated your answer to copy my declared method `proto.process` which you called `process.handleEvent`. But you are not even calling your `proto.handleEvent` function. Could you please correct your answer by at least copying ALL of my answer? – Marcelo Glasberg Jan 07 '16 at 18:05
  • when you add the HTML via innerHTML, you can't use querySelector to grab the elements in createdCallback, I tried this in an earlier attempt and the querySelector always returned null – Nodeocrat Jan 08 '16 at 10:49
  • @CodeMedic : yes you can. Look and run the snippet. It works. – Supersharp Jan 08 '16 at 10:53
  • @MarcG : You're wrong again. I called nothing `process.handleEvent` and I don't need to call `proto.handleEvent` because it's a standard behaviour from DOM v2. Please stop polluting posts with your ignorance. – Supersharp Jan 08 '16 at 11:05