Web Components, pass data to and from - javascript

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?

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;
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,[{"a":9},{"a":8},{"a":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":"<b>Hi!</b>"},{"c":"</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.

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>

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>

Using a tiny lib 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>

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>

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>

Related

Can you use a backdraftjs watchable to make a component completely re-render?

This is a contrived example but it is similar to real-life situations where, for example, you might have a list of links built from data that you are AJAXing in from a server.
import {Component, e, render} from './node_modules/bd-core/lib.js';
// a list of strings that for alert when you click them
class AlertLinkList extends Component.withWatchables('data') {
handleClick(event){
alert(event.target.innerHTML);
}
bdElements() {
return e.div(
{
// bdReflect: ?????
},
['yes', 'no', 'maybe'].map(
txt => e.div({bdAdvise: {click: 'handleClick'}}, txt)
)
)
}
}
var linkList = render(AlertLinkList, {}, document.body);
// I would like to change the strings but this (obviously) does nothing
linkList.data = ['soup', 'nuts', 'fish', 'dessert'];
I can't think of a straightforward way to solve this.
bdReflect only works on writable DOM attributes, I think, so for example I could use it to replace the innerHTML of the component but then I think I lose the bdAdvise assignments on the links (and it also seems kinda kludgey).
Any ideas?
OK here's one pattern that works for this...
get rid of the watchables in AlertLinkList
instead, use kwargs to populate the list
wrap the list in another component that simply re-renders the list with new content whenever the content changes (e.g. after fetching new content from the server)
// a list of strings that alert when you click them
class AlertLinkList extends Component {
handleClick(event){
alert(event.target.innerHTML);
}
bdElements() {
return e.div(
this.kwargs.items.map(
txt => e.div({bdAdvise: {click: 'handleClick'}}, txt)
)
)
}
}
// a wrapper that provides/retrieves data for AlertLinkList
class LinkListWrapper extends Component {
bdElements() {
return e.div(
{},
e.a(
{bdAdvise: {click: 'updateList'}},
'Click to Update List',
),
e.div({bdAttach: 'listGoesHere'}),
);
}
updateList(event) {
// the data below would have been retrieved from the server...
const resultRetrievedFromServer = ['soup', 'nuts', 'fish', 'dessert'];
this.renderList(resultRetrievedFromServer)
}
renderList(items) {
render(AlertLinkList, {items}, this.listGoesHere, 'only')
}
postRender() {
const initialData = ['yes', 'no', 'maybe']
this.renderList(initialData);
}
}
var linkList = render(LinkListWrapper, {}, document.body);
The only issue I see here is that it may be suboptimal to re-render the entire wrapped component if only one small part of the data changed, though I suppose you could design around that.
Let's begin solving this problem by describing the public interface of AlertLinkList:
A component that contains a homogeneous list of children.
The state of each child is initialized by a pair of [text, url].
The list is mutated en masse.
Given this, your start is almost perfect. Here it is a again with a few minor modifications:
class AlertLinkList extends Component.withWatchables('data') {
handleClick(event) {
// do something when one of the children is clicked
}
bdElements() {
return e.div({}, this.data && this.data.map(item => e(AlertLink, { data: item })));
}
onMutateData(newValue) {
if (this.rendered) {
this.delChildren();
newValue && newValue.forEach(item => this.insChild(AlertLink, { data: item }));
}
}
}
See https://backdraftjs.org/tutorial.html#bd-tutorial.watchableProperties for an explanation of onMutateData.
Next we need to define the AlertLink component type; this is trivial:
class AlertLink extends Component {
bdElements() {
return e.a({
href: this.kwargs.data[1],
bdAdvise: { click: e => this.parent.handleClick(e) }
}, this.kwargs.data[0]);
}
}
The outline above will solve your problem. I've written the pen https://codepen.io/rcgill/pen/ExWrLbg to demonstrate.
You can also solve the problem with the backdraft Collection component https://backdraftjs.org/docs.html#bd-core.classes.Collection
I've written a pen https://codepen.io/rcgill/pen/WNpmeyx to demonstrate.
Lastly, if you're interested in writing the fewest lines of code possible and want a fairly immutable design, you don't have to factor out a child type. Here's a pen to demonstrate that: https://codepen.io/rcgill/pen/bGqZGgW
Which is best!?!? Well, it depends on your aims.
The first solution is simple and general and the children can be wrangled to do whatever you want them to do.
The second solution is very terse and includes a lot of additional capabilities not demonstrated. For example, with the backdraft Collection component
mutating the collection does not destroy/create new children, but rather alters the state of existing children. This is much more efficient and useful when implementing things like large grids.
you can mutate an individual elements in the collection
The third solution is very terse and very fixed. But sometimes that is all you need.

how to pass objects in web component javascript [duplicate]

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?
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;
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,[{"a":9},{"a":8},{"a":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":"<b>Hi!</b>"},{"c":"</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.
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>
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>
Using a tiny lib 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>
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>
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>

How to pass js Object and functions to a web component

Hi All I am a beginner in javaScript and currently exploring JS Web-component and I got stuck due to some use cases
1 ) I want to pass a JS Object into my component like
<my-component data=obj ></my-component>
And require to use inside my component code Like
connectedCallback () {
console.log(this.data) // it should print {"name":"xyz" , "role" : "dev"}
}
2 ) I also need to pass some functions or maybe call back functions like.
function myFunction(e){
console.log(e)
}
<my-component click=myFunction ></my-component>
please try to add code snippet also in ans that will help me to learn more JS.
Thanks
You should pass large object by Javascript.
Via a custom element method:
let comp = document.querySelector( 'my-component' )
comp.myMethod( obj )
Or setting a property:
comp.data = obj
It is best to pass in complex data using a property and not an attribute.
myEl.data = {a:1,b:'two'};
The standard on events work fine on a custom element:
function myFunction(e){
alert(JSON.stringify(e.target.data));
e.target.data = {a:1,b:"two"};
}
class MyComponent extends HTMLElement {
constructor() {
super();
this._data = 0;
this.attachShadow({mode:'open'}).innerHTML="Click Me";
}
static get observedAttributes() {
return ['data'];
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (oldVal !== newVal) {
}
}
get data() {
return this._data;
}
set data(newVal) {
this._data = newVal;
}
}
customElements.define('my-component', MyComponent);
<my-component onclick="myFunction(event)"></my-component>
If your component dispatches a custom event then it is best to access it through code:
function specialEventHandler(evt) {
// do something
}
myEl.addEventListener('special-event;', specialEventHandler);
I did a Udemy course with Andreas Galster and the tutor passed in a JSON object via attribute.
As you can see it needs encodeURIComponent and decodeURIComponent as well to
attributeChangedCallback (name, oldValue, newValue) {
if (newValue && name === 'profile-data') {
this.profileData = JSON.parse(decodeURIComponent(newValue));
this.removeAttribute('profile-data');
}
this.render();
}
Pass in:
<profile-card profile-data=${encodeURIComponent(JSON.stringify(profile))}>
</profile-card>
The code worked fine for me.
Ad 1) You need to use JSON.stringify(obj)
Ad 2) As far as I know All attributes need to be defined as strings. You can pass the function that is global and inside component try to eval(fn)

How to access content text in HTML Custom Element?

When creating an HTML custom element with a JSON string embedded by the user (though the type of string is not relevant here) ...
<my-elem>
{ "some":"content" }
</my-elem>
I would like to JSON.parse it like this ...
class MyElement extends HTMLElement {
constructor() {
super();
this.root = this.attachShadow({ mode:'open' });
this.root.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
JSON.parse(this.innerHTML);
}
}
customElements.define('my-elem', MyElement);
const template = document.createElement('template');
template.innerHTML = `irrelevant`;
... and get a perfect result with Firefox v.63.
But running this with Chrome v.71 I get
Uncaught SyntaxError: Unexpected end of JSON input
due to this.innerHTML returning an empty string.
I also tried other DOM methods to access the textual content, but all of them failed also.
Now I'm rather clueless, how to get this to work with Chrome.
Btw: Using <slot> is of no help, since I do not want to render the textual content ... only access it for parsing.
Resolved:
Put the template definition before the class definition.
Ensure having the script defining the custom element inserted behind all custom element instances in the HTML document.
Why not make it more flexible and support both a src attribute and a data property?
class MyElement extends HTMLElement {
static get observedAttributes() {
return ['src'];
}
constructor() {
super();
this.attachShadow({ mode:'open' });
this._data = {
name: '',
address: ''
};
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (oldVal !== newVal) {
const el = this;
fetch(newVal).then(resp => resp.json()).then(
data => {
el._data = data;
el.render();
}
);
}
}
render() {
this.shadowRoot.innerHTML = `
<div>
<div>${this._data.name}</div>
<div>${this._data.address}</div>
</div>`;
}
set address(val) {
this._data.address = val;
this.render();
}
set name(val) {
this._data.name = val;
this.render();
}
}
customElements.define('my-elem', MyElement);
setTimeout(() => {
let el = document.querySelector('my-elem');
el.name = 'The Joker';
el.address = 'Gothem';
setTimeout(() => {
el.setAttribute('src', 'data:application/json,%7B%22name%22:%22Thanos%22,%22address%22:%22Titan%22%7D')
}, 1500);
}, 1500);
<my-elem src="data:application/json,%7B%22name%22:%22Darth%20Vader%22,%22address%22:%22Death%20Star%22%7D"></my-elem>
The advantage to using a src attribute is that you can pass in JSON or you can pass in a URL that will return the JSON.
The properties allow you to change individual values in your DOM.
Changing the entire innerHTML may not be the right thing to do, but with small amounts of DOM it could be. You can also change individual values on the DOM or use something like LitHtml.
You should wait for the content to be present.
In most cases a simple delay could resolve the problem:
connectedCallback() {
setTimeout( () => JSON.parse(this.innerHTML) )
}
Alternatly, actually <slot> could help with the slotchange event. You can hide the rendering or remove the content if you don't want it.

How to access DOM event-handlers for JSX-components in server-sided React

I am building a simple static view-engine using React with the goal of rendering static HTML-markup and generating a js-file filled with that components DOM-events (onClick, etc).
The way I'm doing the first part is to require a specified JSX-file which, for example, looks like this:
import React from 'React';
export default class Test extends React.Component {
clicked() {
alert('Clicked the header!');
}
render() {
return (
<html>
<head>
<title>{this.props.title}</title>
</head>
<body>
<h1 onClick={this.clicked}>click me!!!</h1>
</body>
</html>
);
}
}
I am then rendering the JSX-file via a NodeJS-backend like this:
let view = require('path-to-the-jsx-file');
view = view.default || view;
const ViewElement = React.createFactory(view);
let output = ReactDOMServer.renderToStaticMarkup(ViewElement(props));
It works great for serving static HTML. But I am wondering if there is a way to access all components used in the JSX-file in an array or something, which I then could use to check what events are bound and to which handlers.
So in this example, be able to get that the <h1>-tag's onClick-handler? Is this even possible to do somehow?
To be able to get the function as a string from the onClick event, we want the following:
The DOM of the element
We can obtain this by attaching a ref attribute on our h1 element
The name of the function being passed into the onClick event (clicked)
The function itself from a string containing the name of the function
Since we're conveniently using methods within a React component, we can use this['functionName'] within our component to obtain the function.
A stringified version of the function
import React from 'React';
export default class Test extends React.Component {
componentDidMount() {
// Gets the name of the function passed inside onClick()
const nameBound = this.element.props.onClick.name;
// Removes 'bound ' from the function name (-> clicked)
const nameString = nameBound.replace('bound ', '');
// Gets the function from the function name as a string
const convertedFunction = this[nameString];
// Converts the function into string
const stringifiedFunction = convertedFunction.toString();
console.log(functionString);
}
clicked() {
alert('Clicked the header!');
}
render() {
return (
<html>
<head>
<title>{this.props.title}</title>
</head>
<body>
<h1 ref={(element) => { this.element = element; }} onClick={this.clicked}>click me!!!</h1>
</body>
</html>
);
}
}
After a lot of messing around I came up with a solution that works quite well.
If I create my own instance of the ReactElement I want to render (in the example ViewElement(props)), I can then render the element using it's standard render-function:
let element = ViewElement(props);
let instance = new element.type();
let render = instance.render();
From here I can go through all the props for this element, so, say, onClick-handlers would be in render.props.
So what I do is to check each prop if the key matches a react-event-name (ex. onClick, onDragEnd, onDragEnter etc). If it does, and the value of this property is of type function - I have the event-name and it's handler-function:
Object.keys(render.props).map((key) => {
if (bigArrayOfAllTheEventNames.indexOf(key) !== -1) {
item.events[key] = render.props[key];//check if type is function.
}
});
Then I also iterate through the render.props.children recursivly to reach all it's child components and add every component which has events to an array.
The only problem left was that I needed a way to bind the rendered DOM-string to the javascript handlers I now have. For this I added a need to use a custom DOM-attribute, which then can be used to ID the component with something like this
$("[data-id=value]").on({event-name}, {it's JS-handler}).
It might not be perfect yet, but I think that this is the best solution out there.

Categories