How to pass attributes to help implement a custom element? - html

When we use web component techniques to create a custom element, sometimes the implementation of a custom element involves the use of attributes present on the resulting element in the main document. As in the following example:
main doc:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="import" href="web-components/my-portrait.html">
</head>
<body>
<my-portrait src="images/pic1.jpg" />
</body>
</html>
my-portrait.html:
<template id="my-portrait">
<img src="" alt="portait">
</template>
<script>
(function() {
var importDoc = document.currentScript.ownerDocument;
var proto = Object.create(HTMLElement.prototype, {
createdCallback: {
value: function() {
var t = importDoc.querySelector("#my-portrait");
var clone = document.importNode(t.content, true);
var img = clone.querySelector("img");
img.src = this.getAttribute("src");
this.createShadowRoot().appendChild(clone);
}
}
});
document.registerElement("my-portrait", {prototype: proto});
})();
</script>
In the createdCallback, we use this.getAttribute("src") to get the src attribute defined on the portrait element.
However, this way of obtaining the attribute can be only used when the element is instantiated by element tag declaration. But what if the element is created using JavaScript: document.createElement("my-portrait")? When this statement is done being executed, the createdCallback has already been called and this.getAttribute("src") will return null since the element has no src attribute instantly when it is created.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="import" href="web-components/my-portrait.html">
</head>
<body>
<!--<my-portrait src="images/pic1.jpg" />-->
<script>
var myPortrait = document.createElement("my-portrait");
myPortrait.src = "images/pic2.jpg"; // too late
document.getElementsByTagName("body")[0].appendChild(myPortrait);
</script>
</body>
</html>
So how do we pass attribute to createdCallback when we instantiate a custom element using JavaScript? If there were a beforeAttach callback, we can set attributes there, but there are no such callback.

You may implement the lifecycle callback called attributeChangedCallback:
my-portrait.html:
proto.attributeChangedCallback = function ( name, old, value )
{
if ( name == "src" )
this.querySelector( "img" ).src = value
//...
}
name is the name of the modified (or added) attribute,
old is the old value of the attribute, or undefined if it was just created,
value is the new value of the attribute (of type string).
For the callback to be called, use the setAttribute method against your custom element.
main doc:
<script>
var myPortrait = document.createElement( "my-portrait" )
myPortrait.setAttribute( "src", "images/pic2.jpg" ) // OK
document.body.appendChild( myPortrait )
</script>
example:
var proto = Object.create(HTMLElement.prototype, {
createdCallback: {
value: function() {
this.innerHTML = document.querySelector("template").innerHTML
var path = this.getAttribute("src")
if (path)
this.load(path)
}
},
attributeChangedCallback: {
value: function(name, old, value) {
if (name == "src")
this.load(value)
}
},
load: {
value: function(path) {
this.querySelector("img").src = path
}
}
})
document.registerElement("image-test", { prototype: proto })
function add() {
var el = document.createElement("image-test")
el.setAttribute("src", "https://placehold.it/100x50")
document.body.appendChild(el)
}
<image-test src="https://placehold.it/100x100">
</image-test>
<template>
<img title="portrait" />
</template>
<button onclick="add()">
add
</button>

Related

Injecting a custom element in HTML5 then retrieving its value using XPath comes back blank

I'm injecting an XML string generated from a web service, then trying to use XPath to query the attribute values using the following code.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>tox-js</title>
</head>
<body>
<script>
//
// -----------------------------------------------
// tox element
class Tox extends HTMLElement
{
constructor(url)
{
super();
fetch(url)
.then((response)=>
{
console.log("status: "+response.status);
return response.text();
})
.then((text)=>
{
console.log("text: "+text);
try
{
var dp = new DOMParser();
var xmlDOM = dp.parseFromString(text, "text/xml");
this.appendChild(xmlDOM.documentElement);
return true;
}
catch(err)
{
console.log("err: "+err.message);
return false;
}
})
.then((ok)=>
{
if (ok)
{
try
{
var xpe = new XPathEvaluator();
var txt = xpe.evaluate("//tox-js/example/#timestamp",document,null,XPathResult.STRING_TYPE,null);
console.log("//tox-js/example/#timestamp: "+txt.stringValue);
txt = xpe.evaluate("//tox-js/example/#feedback",document,null,XPathResult.STRING_TYPE,null);
console.log("//tox-js/example/#feedback: "+txt.stringValue);
}
catch(err)
{
console.log("err: "+err.message);
}
}
else
console.log("not ok");
}
);
}
}
//
// -----------------------------------------------
// register our element with the DOM
customElements.define('tox-js',Tox);
//
// -----------------------------------------------
// create an instance and add it to the body
document.body.appendChild(new Tox('http://localhost:8080/tox/example.test.formatted?in_mask=YYYYMMDD'));
// -----------------------------------------------
//
</script>
</body>
</html>
The result has the custom element injected.
<html lang="en">
<head>...</head>
<body>
<script>...</script>
<tox-js>
<example timestamp="20180103142036" feedback="ok">20190103</example>
</tox-js>
</body>
<html>
The console log confirms the return status and XML, but the result of the XPath is blank.
[Log] status: 200 (toxElement3.html, line 20)
[Log] text: <example timestamp="20190103142036" feedback="ok">20190103</example> (toxElement3.html, line 25)
[Log] //tox-js/example/#timestamp: (toxElement3.html, line 47)
[Log] //tox-js/example/#feedback: (toxElement3.html, line 49)
Where have I gone wrong? This should not be a timing issue since I'm using .then to wait for the previous step.
Seems it is related to the namespaces.
The following XPath works for me:
//tox-js/*[local-name()='example']/#timestamp
Check this answer:
XPath Doesn't Work in Dynamic HTML-Document
Also you can use document.createElement() or insertAdjacentHTML() to create element from text as described here: Creating a new DOM element from an HTML string using built-in DOM methods or Prototype
In this case your XPath will work as expected.
<html lang="en">
<head></head>
<body>
<script>
window.addEventListener('load', () => {
var text = `<example timestamp="20180103142036" feedback="ok">20190103</example>`;
var el = document.getElementsByTagName('tox-js')[0];
el.insertAdjacentHTML('afterbegin', text);
var xpe = new XPathEvaluator();
var txt = xpe.evaluate("//tox-js/example/#timestamp",document,null,XPathResult.STRING_TYPE,null);
console.log(`//tox-js/example/#timestamp: ${txt.stringValue}`);
});
</script>
<tox-js>
</tox-js>
</body>
<html>
P.S. I can't explain why the problem happens when using DOMParser. Maybe there are different namespaces for document and DOMParser. So if somebody has more details, feel free to extend the answer.
From the provided example...
var dp = new DOMParser();
var xmlDOM = dp.parseFromString(text, "text/xml");
this.appendChild(xmlDOM.documentElement);
...becomes...
this.insertAdjacentHTML('afterbegin', text);

Using one iron-ajax element for multiple requests

Theoretically, it should be possible to use one iron-ajax element for multiple requests by setting the auto attribute and then repeatedly setting the url property on the element. iron-ajax has a property called activeRequests, which is a read-only array, so it seems like it has supports for queueing up multiple requests simultaneously. However in practice it does not appear to work.
For example, in the JS Bin below, I retrieve a list of book IDs for books that contain the word polymer, and then use a for loop to repeatedly set the value of url.
<!doctype html>
<html>
<head>
<meta name="description" content="single iron-ajax for multiple requests">
<meta charset="utf-8">
<base href="https://polygit.org/components/">
<script href="webcomponentsjs/webcomponents-lite.min.js"></script>
<link href="polymer/polymer.html" rel="import">
<link href="iron-ajax/iron-ajax.html" rel="import">
</head>
<body>
<dom-module id="my-el">
<template>
<iron-ajax id="ajax"
url="https://www.googleapis.com/books/v1/volumes?q=polymer"
handle-as="json"
on-response="onResponse"
last-response="{{response}}" auto></iron-ajax>
</template>
<script>
Polymer({
is: 'my-el',
properties: {
response: {
type: Object,
notify: true
}
},
onResponse: function(e) {
var ajax = this.$.ajax;
var originalUrl = 'https://www.googleapis.com/books/v1/volumes?q=polymer';
var url = ajax.lastRequest.xhr.responseURL;
if (url.includes(originalUrl)) {
console.log('this is the first request');
for (var i = 0; i < ajax.lastResponse.items.length; i++) {
ajax.url = this.url(ajax.lastResponse.items[i].id);
}
} else {
console.log(ajax.lastResponse.selfLink);
}
},
url: function(id) {
return "https://www.googleapis.com/books/v1/volumes/" + id;
}
});
</script>
</dom-module>
<my-el></my-el>
</body>
</html>
It's indeed possible to use iron-ajax for multiple requests but not with auto enabled, or else you'll hit iron-ajax's debouncer. From the Polymer docs for iron-ajax:
With auto set to true, the element performs a request whenever its url, params or body properties are changed. Automatically generated requests will be debounced in the case that multiple attributes are changed sequentially.
In your question's code:
// template
<iron-ajax auto ...>
// script
onResponse: function(e) {
...
for (var i = 0; i < ajax.lastResponse.items.length; i++) {
ajax.url = this.url(ajax.lastResponse.items[i].id);
}
}
...you're presumably expecting iron-ajax to generate a new request with each URL, but the debouncer collapses the requests into one (taking only the last invocation).
Also worth noting: The response handler's event detail (i.e., e.detail) is the corresponding iron-request, which contains the AJAX response (i.e., e.detail.response). Using the event detail is preferrable because it avoids a race condition in simultaneous requests from iron-ajax, where this.$.ajax.lastResponse or this.$.ajax.lastRequest are overwritten asynchronously.
onResponse: function(e) {
var request = e.detail;
var response = request.response;
}
To reuse iron-ajax with a new URL, disable auto (which disables the debouncer) and manually call generateRequest() after updating the URL. This would allow multiple simultaneous async requests (and activeRequests would populate with more than one request).
// template
<iron-ajax ...> <!-- no 'auto' -->
// script
onResponse: function(e) {
var request = e.detail;
var response = request.response;
...
for (var i = 0; i < response.items.length; i++) {
ajax.url = this.url(response.items[i].id);
ajax.generateRequest();
}
},
ready: function() {
this.$.ajax.generateRequest(); // first request
}
Here's a modified version of your code:
<head>
<base href="https://polygit.org/polymer+1.5.0/components/">
<script src="webcomponentsjs/webcomponents-lite.min.js"></script>
<link rel="import" href="polymer/polymer.html">
<link rel="import" href="iron-ajax/iron-ajax.html">
</head>
<body>
<x-foo></x-foo>
<dom-module id="x-foo">
<template>
<!-- We're reusing this iron-ajax to fetch more data
based on the first response, and we don't want
iron-ajax's debouncer to limit our requests,
so disable 'auto' (i.e., remove the attribute
from <iron-ajax>). We'll call generateRequest()
manually instead.
-->
<iron-ajax id="ajax"
url="https://www.googleapis.com/books/v1/volumes?q=polymer"
handle-as="json"
on-response="onResponse"
on-error="onError">
</iron-ajax>
</template>
<script>
HTMLImports.whenReady(function() {
Polymer({
is: 'x-foo',
onError: function(e) {
console.warn('iron-ajax error:', e.detail.error.message, 'url:', e.detail.request.url);
},
onResponse: function(e) {
var ajax = this.$.ajax;
var originalUrl = 'https://www.googleapis.com/books/v1/volumes?q=polymer';
var url = e.detail.url;
if (url.includes(originalUrl)) {
var books = e.detail.response.items || [];
console.log('this is the first request');
for (var i = 0; i < books.length && i < 3; i++) {
ajax.url = this.url(books[i].id);
console.log('fetching:', ajax.url);
ajax.generateRequest();
}
} else {
var book = e.detail.response;
console.log('received:', e.detail.url, '"' + book.volumeInfo.title + '"');
}
},
url: function(id) {
return "https://www.googleapis.com/books/v1/volumes/" + id;
},
ready: function() {
// generate first request
this.$.ajax.generateRequest();
}
});
});
</script>
</dom-module>
</body>
https://jsbin.com/qaleda/edit?html,console
I don't know what's up with the activeRequests property, but I was able to get it to work by re-structuring my code a little. Basically, just implement a queue, and pop off an item from the queue and set url once the last request has finished.
<!doctype html>
<html>
<head>
<meta name="description" content="http://stackoverflow.com/questions/37817472">
<meta charset="utf-8">
<base href="https://polygit.org/components/">
<script href="webcomponentsjs/webcomponents-lite.min.js"></script>
<link href="polymer/polymer.html" rel="import">
<link href="iron-ajax/iron-ajax.html" rel="import">
</head>
<body>
<dom-module id="my-el">
<template>
<iron-ajax id="ajax"
url="https://www.googleapis.com/books/v1/volumes?q=polymer"
handle-as="json"
on-response="onResponse"
last-response="{{response}}" auto></iron-ajax>
</template>
<script>
Polymer({
is: 'my-el',
properties: {
response: {
type: Object,
notify: true
},
queue: {
type: Array,
notify: true,
value: function() { return []; }
}
},
onResponse: function(e) {
var ajax = this.$.ajax;
var originalUrl = 'https://www.googleapis.com/books/v1/volumes?q=polymer';
var url = ajax.lastRequest.xhr.responseURL;
if (url.includes(originalUrl)) {
console.log('this is the first request');
for (var i = 0; i < ajax.lastResponse.items.length; i++) {
this.push('queue', ajax.lastResponse.items[i].id);
}
ajax.url = this.url(this.pop('queue'));
} else {
console.log(ajax.lastResponse.selfLink);
ajax.url = this.url(this.pop('queue'));
}
},
url: function(id) {
return "https://www.googleapis.com/books/v1/volumes/" + id;
}
});
</script>
</dom-module>
<my-el></my-el>
</body>
</html>
https://jsbin.com/beyawe/edit?html,console
You can try out iron-multiple-ajax-behavior.
Let me know if this can be useful to you.

React JS this.setState does nothing when updating content via a nested component

I am working on a simple react example that will alter the content of a header on the screen based on what a user enters in a text field. Here's the react code:
var GreeterForm = React.createClass({
onFormSubmit: function(e){
e.preventDefault();
var name = this.refs.name;
this.refs.name.value = '';
this.props.onNewName(name);
},
render: function(){
return(
<form onSubmit={this.onFormSubmit}>
<input type="text" ref="name"/>
<button>Set Name</button>
</form>
);
}
});
var Greeter = React.createClass({
//returns an object of the default properties to be used
//these are used if no properties are passed in
getDefaultProps: function(){
return {
name: 'React!',
message: "this is from a component!"
};
},
//maintains a state for the component. Maintains the state as an object
//this is a default method for react and we override it.
getInitialState: function(){
return {
name: this.props.name
};
},
handleNewName: function(name){
this.setState({
name: name
});
},
//renders the greeter react component
render: function() {
var name = this.state.name;
var message = this.props.message;
return (
<div>
<h1>Hello {name}!</h1>
<p>{message}</p>
<GreeterForm onNewName={this.handleNewName}/>
</div>
);
}
});
var firstName = "DefaultName";
var mess = "This is a message from react."
//note that name and message are passed in as properties
ReactDOM.render(<Greeter name={firstName} message={mess}/>, document.getElementById('app'));
You can see that GreeterForm is nested within Greeter and is supposed to alter the content of the h1 tag when the user submits.
However, the content of the h1 tag is not changing. I sprinkled alert(name.value) along the code to ensure the correct name from the input field was being passed around, and that all checked out.
What could it be? Is something wrong with my setState function?
Here's the HTML if needed:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.js"></script>
</head>
<body>
<div id="app">
</div>
<script type="text/babel" src="app.jsx"></script>
</body>
</html>
You are not sending value of input. you are sending whole html element
in onFormSubmit function change this line
var name = this.refs.name;
to
var name = this.refs.name.value;

How to observe property changes with dom-bind

How to observe property changes if I use dom-bind? The change in property is updated inside curly brackets, so I assume that there is some event related to change, but I don't know it's name.
Example:
<!doctype html>
<html lang="">
<head >
<link rel="import" href="bower_components/polymer/polymer.html">
<script src="bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<style>
</style>
</head>
<body>
<template is="dom-bind" id="app2">
<p>test: <span>{{test}}</span></p>
<input type="button" value="Change" on-click="chTest">
</template>
<script>
(function (document) {
'use strict';
var app = document.querySelector('#app2');
app.test = "original value";
app.addEventListener('test-changed', function () {
console.log('change listener fired');
});
app.addEventListener('dom-change', function () {
console.log("dom-change");
});
app.chTest = function () {
console.log('click');
app.test = "new value";
// notifyPath-setter will fire "test-changed"-event
// app.notifyPath("test", "notify value");
};
})(document);
</script>
</body>
</html>
EDIT:
There is a need to clarify my question: I want to invoke some function, when app.test changes.
You have the answer commented out. Comment the line app.test and uncomment the app.notifyPath
// app.test = "new value";
// notifyPath-setter will fire "test-changed"-event
app.notifyPath("test", "notify value");
Working jsbin below:
http://jsbin.com/vaqosi/edit?html,console,output
Edit: One workaround is to two way bind it into a child element and listen for its changes. I have updated the jsbin.

Web Component: How to listen to Shadow DOM load event?

I want to execute a JavaScript code on load of the Shadow DOM in my custom element.
I tried the following code but it did not work
x-component.html:
<template id="myTemplate">
<div>I am custom element</div>
</template>
<script>
var doc = this.document._currentScript.ownerDocument;
var XComponent = document.registerElement('x-component', {
prototype: Object.create(HTMLElement.prototype, {
createdCallback: {
value: function() {
var root = this.createShadowRoot();
var template = doc.querySelector('#myTemplate');
var clone = document.importNode(template.content, true);
clone.addEventListener('load', function(e) {
alert('Shadow DOM loaded!');
});
root.appendChild(clone);
}
}
})
});
</script>
Then I use it in another html as follows -
index.html:
<!doctype html>
<html >
<head>
<script src="bower_components/webcomponentsjs/webcomponents.min.js"></script>
<link rel="import" href="x-component.html">
</head>
<body>
<x-component></x-component>
</body>
</html>
The doc variable is used as I am using Polymer webcomponents.js polyfill and the polyfill needs it.
What is the right syntax to listen to load event of Shadow DOM?
AFAIK, the only way to achieve this is to use MutationObserver:
attachedCallback: {
value: function() {
var root = this.createShadowRoot();
var template = document.querySelector('#myTemplate');
var clone = document.importNode(template.content, true);
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if(mutation.addedNodes) { // this is definitely a subject to change
alert('Shadow is loaded');
};
});
})
observer.observe(root, { childList: true });
root.appendChild(clone);
}
}
I would be glad to know if there is more elegant way, but for now I use this one.
Live preview: http://plnkr.co/edit/YBh5i2iCOwqpgsUU6En8?p=preview