Web Components, pass data to and from
Asked Answered
E

6

61

My understanding is that data is passed to a custom html element via its attributes and sent out by dispatching a CustomEvent.

JavaScript objects can obviously be sent out in the event's detail field, but what if the element needs a lot of data passed into it. Is there a way to provide it with an object in JavaScript.

What if the element for instance contains a variable number of parts that needs to be initialized or changed dynamically (e.g. a table with a variable number of rows)? I can imagine setting and modifying an attribute consisting of a JSON string that is parsed inside the component, but it does not feel like an elegant way to proceed:

<my-element tableRowProperties="[{p1:'v1', p2:'v2'}, {p1:'v1',p2:'v2'}, {p1:'v1',p2:'v2'}]"></my-element>

Or can you make the element listen to events from the outside that contains a payload of data?

Effy answered 18/5, 2018 at 6:22 Comment(4)
You can pass data to or get data from a custom element by calling ione of its methods or attributes.Upbear
Fantastic! Of course! That was the missing piece (although obvious) I needed to grasp the usefulness of web components. I had the feeling that they could completely replace the need for using jQuery widget factory, but couldn't really see how until now. Thanks!Effy
You're welcome :-) example here https://mcmap.net/q/324341/-how-do-you-decouple-web-components/4600982Upbear
you can use e-form element from EHTML: github.com/Guseyn/EHTMLBarbellate
S
68

Passing Data In

If you really want/need to pass large amounts of data into your component then you can do it four different ways:

  1. Use a property. This is the simplest since you just pass in the Object by giving the value to the element like this: el.data = myObj; This is often best done using getters and setters. Then you code, in the setter, can process the values being passed in.

  2. Use an attribute. Personally I hate this way of doing it this way, but some frameworks require data to be passed in through attributes. This is similar to how you show in your question. <my-el data="[{a:1},{a:2}....]"></my-el>. Be careful to follow the rules related to escaping attribute values. If you use this method you will need to use JSON.parse on your attribute and that may fail. It can also get very ugly in the HTML to have the massive amount of data showing in a attribute.

3 Pass it in through child elements. Think of the <select> element with the <option> child elements. You can use any element type as children and they don't even need to be real elements. In your connectedCallback function your code just grabs all of the children and convert the elements, their attributes or their content into the data your component needs.

4 Use Fetch. Provide a URL for your element to go get its own data. Think of <img src="imageUrl.png"/>. If your already has the data for your component then this might seem like a poor option. But the browser provides a cool feature of embedding data that is similar to option 2, above, but is handled automatically by the browser.

Here is an example of using embedded data in an image:

img {
  height: 32px;
  width: 32px;
}
<img src="data:image/svg+xml;charset=utf8,%3C?xml version='1.0' encoding='utf-8'?%3E%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 314.7 314.7'%3E%3Cstyle type='text/css'%3E .st0{fill:transparent;stroke:%23231F20;stroke-width:12;} .st1{fill:%23231F20;stroke:%23231F20;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:10;} %3C/style%3E%3Cg%3E%3Ccircle class='st0' cx='157.3' cy='157.3' r='150.4'/%3E%3Cpolygon class='st1' points='108,76.1 248.7,157.3 108,238.6'/%3E%3C/g%3E%3C/svg%3E">

And here is an example of using embedded data in a web component:

function readSrc(el, url) {
    var fetchHeaders = new Headers({
      Accept: 'application/json'
    });

    var fetchOptions = {
      cache: 'default',
      headers: fetchHeaders,
      method: 'GET',
      mode: 'cors'
    };

    return fetch(url, fetchOptions).then(
      (resp) => {
        if (resp.ok) {
          return resp.json();
        }
        else {
          return {
            error: true,
            status: resp.status
          }
        }
      }
    ).catch(
      (err) => {
        console.error(err);
      }
    );
  }

  class MyEl extends HTMLElement {
    static get observedAttributes() {
      return ['src'];
    }

    attributeChangedCallback(attrName, oldVal, newVal) {
      if (oldVal !== newVal) {
        this.innerHtml = '';
        readSrc(this, newVal).then(
          data => {
            this.innerHTML = `<pre>
${JSON.stringify(data,0,2)}
            </pre>`;
          }
        );
      }
    }
  }

  // Define our web component
  customElements.define('my-el', MyEl);
<!--
This component would go load its own data from "data.json"
<my-el src="data.json"></my-el>
<hr/>
The next component uses embedded data but still calls fetch as if it were a URL.
-->
<my-el src="data:json,[{&quot;a&quot;:9},{&quot;a&quot;:8},{&quot;a&quot;:7}]"></my-el>

You can do that same this using XHR, but if your browser supports Web Components then it probably supports fetch. And there are several good fetch polyfills if you really need one.

The best advantage to using option 4 is that you can get your data from a URL and you can directly embed your data. And this is exactly how most pre-defined HTML elements, like <img> work.

UPDATE

I did think of a 5th way to get JSON data into an object. That is by using a <template> tag within your component. This still required you to call JSON.parse but it can clean up your code because you don't need to escape the JSON as much.

class MyEl extends HTMLElement {
  connectedCallback() {
    var data;
    
    try {
      data = JSON.parse(this.children[0].content.textContent);
    }
    
    catch(ex) {
      console.error(ex);
    }

    this.innerHTML = '';
    var pre = document.createElement('pre');
    pre.textContent = JSON.stringify(data,0,2);
    this.appendChild(pre);
  }
}

// Define our web component
customElements.define('my-el', MyEl);
<my-el>
  <template>[{"a":1},{"b":"&lt;b>Hi!&lt;/b>"},{"c":"&lt;/template>"}]</template>
</my-el>

Passing Data Out

There are three ways to get data out of the component:

  1. Read the value from a property. This is ideal since a property can be anything and would normally be in the format of the data you want. A property can return a string, an object, a number, etc.

  2. Read an attribute. This requires the component to keep the attribute up to date and may not be optimal since all attributes are strings. So your user would need to know if they need to call JSON.parse on your value or not.

  3. Events. This is probably the most important thing to add to a component. Events should trigger when state changes in the component. Events should trigger based on user interactions and just to alert the user that something has happened or that something is available. Traditionally you would include the relevant data in your event. This reduces the amount of code the user of your component needs to write. Yes, they can still read properties or attributes, but if your events include all relevant data then they probably won't need to do anything extra.

Sludge answered 18/5, 2018 at 17:52 Comment(2)
Why not adding methods (getters or setters) to the class that can be called on the element (that is also an instance of the class) : document.querySelector('my-element').myMethod(heavyData)Sacking
getters and setters is what I meant by "#1 Use a property". I will clean up the response to make that more obvious.Sludge
B
16

There is a 6th way that is really similar to @Intervalia's answer above but uses a <script> tag instead of a <template> tag.

This is the same approach used by a Markdown Element.

class MyEl extends HTMLElement {
  connectedCallback() {
    var data;
    
    try {
      data = JSON.parse(this.children[0].innerHTML);
    }
    
    catch(ex) {
      console.error(ex);
    }

    this.innerHTML = '';
    var pre = document.createElement('pre');
    pre.textContent = JSON.stringify(data,0,2);
    this.appendChild(pre);
  }
}

// Define our web component
customElements.define('my-el', MyEl);
<my-el>
  <script type="application/json">[{"a":1},{"b":"<b>Hi!</b>"},{"c":"</template>"}]</script>
</my-el>
Bergius answered 29/5, 2019 at 18:58 Comment(0)
G
0

If you are using Polymer based web components, the passing of data could be done by data binding. Data could be stored as JSON string within attribute of and passed via context variable.

<p>JSON Data passed via HTML attribute into context variable of  and populating the variable into combobox.</p> 
<dom-bind><template>
<iron-ajax url='data:text/json;charset=utf-8,
                [{"label": "Hydrogen", "value": "H"}
                ,{"label": "Oxygen"  , "value": "O"}
                ,{"label": "Carbon"  , "value": "C"}
                ]'
           last-response="{{lifeElements}}" auto handle-as="json"></iron-ajax>
<vaadin-combo-box id="cbDemo"
        label="Label, value:[[cbDemoValue]]"
        placeholder="Placeholder"
        items="[[lifeElements]]"
        value="{{ cbDemoValue }}"
>
    <template>
        [[index]]: [[item.label]] <b>[[item.value]]</b>
    </template>
</vaadin-combo-box>
<vaadin-combo-box label="Disabled" disabled value="H" items="[[lifeElements]]"></vaadin-combo-box>
<vaadin-combo-box label="Read-only" readonly value="O" items="[[lifeElements]]"></vaadin-combo-box>   



<web-elemens-loader selection="
@polymer/iron-ajax,
@vaadin/vaadin-element-mixin/vaadin-element-mixin,
@vaadin/vaadin-combo-box,
"></web-elemens-loader>
</template></dom-bind>
<script src="https://cdn.xml4jquery.com/web-elements-loader/build/esm-unbundled/node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script><script type="module" src="https://cdn.xml4jquery.com/web-elements-loader/build/esm-unbundled/src/web-elemens-loader.js"></script>
Giagiacamo answered 6/4, 2020 at 1:7 Comment(0)
M
0

Using a tiny lib/build tool such as Lego would allow you to write the following:

<my-element :tableRowProperties="[{p1:'v1', p2:'v2'}, {p1:'v1',p2:'v2'}, {p1:'v1',p2:'v2'}]"></my-element>

and within your my-element.html web-component:

<template>
  <table>
    <tr :for="row in state.tableRowProperties">
      <td>${row.p1}</td>
      <td>${row.p2}</td>
    </tr>
</template>

<script>
  this.init() {
    this.state = { tableRowPropoerties: [] }
  }
</script>
Mediatorial answered 13/4, 2020 at 15:7 Comment(3)
This would introduce a non-standard dependency. Furthermore, colons aren't legal symbols in html attribute names.Alemannic
The content you are reading is very user friendly. Once you run it with the lego-cli it transforms this to a very low level standard component. (example here)Mediatorial
Coming from vuejs and running from the 16KB base component size when converting Vue to Web Components , I have been looking for a vueish way to do this without that 16KB overhead, This kinda fits the narrativeStroh
M
0

I know this has been answered, but here is an approach I took. I know it's not rocket science and there are probably reasons not to do it this way; however, for me, this worked great.

This is an indirect approach to pass in data where an attribute called wc_data is passed in the custom element which is a 'key' that can be used one time.

You can obviously do whatever with the wc-data like callbacks and "callins" into the custom-tag.

link to codesandbox

files:

wc_data.ts

export const wc_data: {
    [name: string]: any,
    get(key: string): any,
    set(key: string, wc_data: any): any
} = {

    get(key: string): any {
        const wc_data = this[key];
        delete this[key];
        return wc_data;
    },

    set(p_key: string, wc_data: any) {
        this[p_key] = wc_data;
    }

}

CustomTag.ts

import { wc_data } from './wc_data';

const template = document.createElement('template');

template.innerHTML = `

    <style>
        .custom-tag {
            font-size: 1.6em;
        }
    </style>

    <button class="custom-tag">Hello <span name="name"></span>, I am your <span name="relation"></span></button>
`;

class CustomTag extends HTMLElement {

    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.appendChild(template.content.cloneNode(true));
    }

    callin() {
        console.log('callin called');
    }

    connectedCallback() {
        const v_wc_data = wc_data.get(this.getAttribute('wc-data'));
        console.log('wc_data', v_wc_data);

        const v_name = this.shadowRoot.querySelector('[name="name"]');
        const v_relation = this.shadowRoot.querySelector('[name="relation"]');

        v_name.innerHTML = v_wc_data.name;
        v_relation.innerHTML = v_wc_data.relation;

        const v_button = this.shadowRoot.querySelector('button');

        v_button.style.color = v_wc_data.color;

        v_wc_data.element = this;

        v_button.addEventListener('click', () => v_wc_data.callback?.());

    }

    disconnectedCallback() {
    }
}

window.customElements.define('custom-tag', CustomTag);

console.log('created custom-tag element');

export default {};

SomeTsFile.ts

wc_data.set('tq', {
    name: 'Luke',
    relation: 'father',
    color: 'blue',
    element: undefined,
    callback() {
        console.log('the callback worked');
        const v_tq_element = this.element;
        console.log(this.element);
        v_tq_element.callin();
    },
});

some html..

<div>stuff before..</div>

<custom-tag wc_data="tq" />

<div>stuff after...</div>
Mistrust answered 27/3, 2022 at 18:28 Comment(1)
Your solution doesn't have anything at all to do with the question. As a reminder, typescript is a cancer on open web standards. The question was about html, css and javascript.Alemannic
A
0

Thanks to the other contributors, I came up with this solution which seems somewhat simpler. No json parsing. I use this example to wrap the entire component in a-href to make the block clickable:


  customElements.define('ish-marker', class extends HTMLElement {
    constructor() {
      super()
      const template = document.getElementById('ish-marker-tmpl').content

      const wrapper = document.createElement("a")
      wrapper.appendChild( template.cloneNode(true) )
      wrapper.setAttribute('href', this.getAttribute('href'))

      const shadowRoot = this.attachShadow({mode: 'open'}).appendChild( wrapper )
    }
  })


<ish-marker href="https://go-here.com">
  ...
  // other things, images, buttons.
  <span slot='label'>Click here to go-here</span>
</ish-marker>

Alemannic answered 1/2, 2023 at 20:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.