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

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

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);

Is there a way to add a behavior(s) dynamically?

I want to add a behavior after a component/behavior already loaded or a certain function that will add a behevaior to its components.
Something like this:
<script>
// samplebehavior.html file
// this is the behavior file
samplebehavior = {
testAlert: function(){
alert('test');
}
};
</script>
// my-component.html
<script>
Polymer({
is: "my-component",
test: function() {
url = "samplebehavior.html";
var importHTML = new Promise(function(resolve, reject) {
Polymer.Base.importHref(url, function(e) {
resolve(e.target);
}, reject);
});
importHTML.then(function(element) {
// add a behavior here
// I know this script does not work
this.push('behaviors', samplebehavior);
});
}
});
</script>
So that I can access the testAlert() function.
How to I add a behavior dynamically?
To the best of knowledge it's not possible.
Behaviors are mixed in with the element definition when the prototype is built.
What you could do is generate the behaviors array dynamically
var behavior = {
properties: {
smth: {
value: 'initial value'
}
}
}
Polymer({
is: 'my-elem',
behaviors: getBehaviors()
});
function getBehaviors() {
return [ behavior ];
}
Remember though that getBehaviors will only ever be called once. After that you won't be able to change behaviors of your elements.
It's ugly but you can copy all members of your behavior in your object
<script>
// samplebehavior.html file
// this is the behavior file
samplebehavior = {
testAlert: function(){
alert('test');
}
};
</script>
// my-component.html
<script>
Polymer({
is: "my-component",
test: function() {
url = "samplebehavior.html";
var importHTML = new Promise(function(resolve, reject) {
Polymer.Base.importHref(url, function(e) {
resolve(e.target);
}, reject);
});
importHTML.then(function(element) {
for (let member in samplebehavior) {
this[member] = samplebehavior[member];
}
});
}
});
</script>
Or maybe you can call the internal method _prepBehavior() https://github.com/Polymer/polymer/blob/ff6e884ef4f309d41491333860a8bc9c2f178696/src/micro/behaviors.html#L111
But i don't know if that can do side effects
<script>
// samplebehavior.html file
// this is the behavior file
samplebehavior = {
testAlert: function(){
alert('test');
}
};
</script>
// my-component.html
<script>
Polymer({
is: "my-component",
test: function() {
url = "samplebehavior.html";
var importHTML = new Promise(function(resolve, reject) {
Polymer.Base.importHref(url, function(e) {
resolve(e.target);
}, reject);
});
importHTML.then(function(element) {
this.push('behaviors', samplebehavior);
this._prepBehaviors();
});
}
});
</script>
With introduction to new value for lazyRegister setting this has become partially possible.
By partially i mean you can only do this if you can edit the code of the element in which you want to add the behavior.
Three changes you'll require to add behavior dynamically are
Set lazyRegister setting to max
window.Polymer = {
lazyRegister:"max"
};
Just like #tomasz suggested, in the element you want to add behavior dynamically add behaviors using a function. This is so, because we won't be able to access the element from where we'll try to add new behavior dynamically(atleast i was not able to).
Polymer({
.
.
behaviors: getBehavior(),
.
.
});
function getBehavior(){
var myArr = [myBehavior];
document.addEventListener('add-behavior',function(e){
debugger;
myArr.push(e.detail);
});
return myArr;
}
From the element where you want to dynamically add the behavior use beforeRegister callback to dispatch an event adding the new behavior object
beforeRegister: function(){
var event = new CustomEvent('add-behavior',{
detail:{
func: function(){console.log(this.myProp,"!")}
}
});
document.dispatchEvent(event);
}
Here's a plunkr for working example.

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.

How to pass attributes to help implement a custom element?

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>

Angular - append html through different frames with one module per frame

My code have the main windonw and one iframe and each one with your module. A button in main window fires click event that should append html into iframe, the new html when appended into that should apply interceptors and directives properly, but it doesn't work!
Angular javascript:
angular.module('module1',[]).controller('Controller1', function ($scope) {
$scope.get = function(){
$http.jsonp("some_url_here").success(function(html){
$scope.content = html;
});
}
}).directive('click', function($compile) {
return {
link: function link(scope, element, attrs) {
element.bind('click',function(){
var unbind = scope.$watch(scope.content, function() {
var div=document.getElementById("frame").contentWindow.angular.element("divId");
div.append($compile(scope.content)(div.scope()));
unbind();
});
});
}
}
});
angular.module('module2',[]).directive('a', function() {
return {
restrict:'E',
link: function(scope, element, attrs) {
console.log('ping!');
console.log(attrs.href);
}
};
});
Html code:
<html ng-app="modile1">
<div ng-controller="Controller1">
<button type="button", ng-click="get('any_value')", click:""/> Load frame
</div>
<iframe id="frame" src="/please/ignore/this">
<!-- considere the html as appended from iframe-src and contains ng-app="module2" -->
<html ng-app="module2">
<div id="divId">
<!-- code should be inject here -->
</div>
</html>
</iframe>
</html>
Please, considere that angularjs, jquery if applicable, modules-declaration as well as headers are loaded properly.
I'd like to load the html content from main-frame/window into iframe and run interceptors and directives properly. Is it possible? If yes, how can I do it?
Thanks for advancing!
I've tried this code and it seems work fine! I found it here: http://www.snip2code.com/Snippet/50430/Angular-Bootstrap
var $rootElement = angular.element(document.getElementById("frame").contentWindow.document);
var modules = [
'ng',
'module2',
function($provide) {
$provide.value('$rootElement', $rootElement)
}
];
var $injector = angular.injector(modules);
var $compile = $injector.get('$compile');
$rootElement.find("div#divId").append(scope.content);
var compositeLinkFn = $compile($rootElement);
var $rootScope = $injector.get('$rootScope');
compositeLinkFn($rootScope);
$rootScope.$apply();