Encapsulation of web components and event binding to shadow DOM elements - html

I started to learn web components in details before to jump in using Polymer.
I was working on a simple example to create a spin button using two <button> and one <input type="text"> elements.
The template is:
<template id="tplSpinButton">
<style type="text/css">
.spin-button > * {
display: inline;
text-align: center;
margin: 0;
float: left;
}
</style>
<div class="spin-button">
<content select=".up-spin-button"></content>
<content select=".display-spin-button"></content>
<content select=".down-spin-button"></content>
</div>
</template>
And the host element is as:
<article>
<spin-button>
<button class="up-spin-button">+</button>
<input class="display-spin-button" type="text" value="0" size="2"/>
<button class="down-spin-button">-</button>
</spin-button>
</article>
And The JS code
var template = document.querySelector('#tplSpinButton');
var host = document.querySelector('article spin-button');
var articleShadowRoot = host.createShadowRoot();
articleShadowRoot.appendChild(document.importNode(template.content,true));
var counterBox = document.querySelector('.display-spin-button');
var upHandler = document.querySelector('.up-spin-button');
var downHandler = document.querySelector('.down-spin-button');
upHandler.addEventListener('click', function(e){
var count = parseInt(counterBox.value);
counterBox.value = count + 1;
}, false);
downHandler.addEventListener('click', function(e){
var count = parseInt(counterBox.value);
counterBox.value = count - 1;
}, false);
document.registerElement('spin-button', {
prototype: Object.create(HTMLElement.prototype)
});
During experiments I came to know that JS/ code does not work as <style> do in shadow DOM being part of <template>
In the above example I am adding insertion points (<content>) and then attaching event listener to distributed elements.
Is there any way to encapsulate the event listener implementation?
Is there any way to move the controls s and elements to shadow dom and then attaching event listener by any way?

Is there any way to encapsulate the event listener implementation?
You could make it part of your element's prototype and construct it inside of a lifecycle callback.
Is there any way to move the controls s and elements to shadow dom and then attaching event listener by any way?
Use getDistributedNodes to select the elements being projected into your content tags.
Here's a jsbin which illustrates both concepts. Hope that helps!

Related

How do I extract DOM elements from an AngularJS service into their own template?

In my AngularJS app, I have a service (or more technically, a provider) which contains a bunch of logic to insert DOM elements on the <body>.
It is quite cumbersome and hard to follow; for example, there are a lot of .append calls of one element into another and into another.
I would like to extract all the DOM code from the service into its own template file. The problem is, while I can find examples for how to do this with directives, I cannot figure out how this would work with a service.
NOTE that I need this to remain a service (as opposed to a directive) because I need it to overlay the entire screen and be callable from a variety of different controllers.
I've recreated my situation in a Plunkr and below, though the service's DOM logic is obviously simplified and shrunk
var app = angular.module('App', []);
app.provider('MyProvider', [function(){
this.$get = ['$window', function($window){
return function(){
var bodyElement = angular.element($window.document.body);
var myNewElement = angular.element('<div class="my-new-element">');
var someText = angular.element('<p>Here is some text<p>');
var xButton = angular.element('<button>X</button>');
xButton.on('click', function(){
myNewElement.remove();
});
myNewElement.append(someText);
myNewElement.append(xButton);
bodyElement.append(myNewElement);
}
}];
}]);
app.controller('MainController', ['$scope', 'MyProvider', function($scope, MyProvider){
$scope.amount1 = 1234.56;
$scope.symbol2 = 'USD$';
$scope.amount2 = '1234.56';
$scope.activateService = function(){
MyProvider();
}
}]);
<!doctype html>
<html ng-app="App">
<head>
<script type="text/javascript" src="https://code.angularjs.org/1.5.8/angular.js"></script>
<style>
.my-new-element {
width: 100%;
height: 50%;
background-color: green;
position: fixed;
top: 0px;
left: 0px;
}
button {
padding: 10px;
margin: 15px 0;
background-color: #bbb;
}
</style>
</head>
<body ng-app="">
<div ng-controller="MainController">
<span>Amount1: {{amount1 | currency:"USD$"}}</span><br/>
<span>Symbol2: <input ng-model="symbol2"/></span><br/>
<span>Amount2: <input ng-model="amount2"/></span><br/>
<button ng-click="activateService()">Activate The Service</button>
</div>
</body>
</html>
You could probably this into both a Directive and a Service. Say something like:
app.service('MyOverlayService', [function(...){}]);
app.directive('myOverlay', ['MyOverlayService', function(os){...}];
Where your service contained things like:
visible [boolean]
list of objects to display (menu items perhaps?)
etc
and your directive listened to those things:
Toggle display/visible based on {{os.visible}}
Listed all the data on in for example if this is a menu, you'd ng-repeat items in os.menus
You could then put your overlay directive on your highest template, and in each of your controllers inject MyOverlayService and do a:
os.setVisible(true); // or os.toggle()

show/hide on multiple div without defining div element id

first am sorry for bad English / grammar
am creating something where you show and hide.
but my problem is that when I click show/hide it only brings input box 1 on both buttons. and I want it to show/hide each box.
my problem is that. I don't want to use the id to define show/hide Element
because if I have more than 10 div with input boxes I have to define them all by getElementById I don't want that.
I want when I click on the show/hide it brings input box without getElementById
so that even if I have more then 10 input box to show I only click and show/hide without defining its id
function myFunction(event) {
var x = document.getElementById("mydv");
if (x.style.display === "none") {
x.style.display = "block";
} else {
x.style.display = "none";
}
x.parentNode.insertBefore(x, event.target.nextSibling);
}
document.addEventListener('click', function(event){
if(event.target.className.includes("dv1")){
myFunction(event);
}
});
<!DOCTYPE html>
<html>
<head>
<title> SHOW / Hide </title>
</head>
<body>
<ul>
<li>
<div id="mydv" style="display:none;">
<p>input box 1
<input type="text" name="textfield">
</p>
</div>
<button class="dv1">SHOW/HIDE</button>
</li>
<li><div id="mydv" style="display:none;">
<p>input box 2
<input type="text" name="textfield">
</p>
</div>
<button class="dv1">SHOW/HIDE</button></li>
</ul>
</body>
</html>
If you want to specify an element on a page, that can be similar in every way to other elements except perhaps text content or something else, realistically you need an id, as this is how JavaScript defines a unique element.
But what you can do, is change your HTML button, to contain a rel, which is an attribute, and then get that attribute and use that to specify which element id you're looking for.
You can then call a function and simply pass "this" as an argument.
HTML :
<button onclick="hideShow(this)" rel="mydv">Show/Hide</button>
JavaScript :
<script>
function hideShow(elem){
var ele = document.getElementById(elem.getAttribute("rel"));
if(ele.style.display == "none"){
ele.style.display = "block";
}
else{
ele.style.display = "none";
}
}
</script>
If you are absolutely abhorrent to using ID's, you can use child nodes and specify which child by number, but this means if ever you change anything, you will break your code, which is foolish. I recommend using unique ID's and simply changing your code in the above ways.
Short and lazy answer to your problems - if you are going to keep your current hierarchy, you can simply find DIV tag inside your LI parentNode (since its the only DIV tag).
Basically it goes like this - button press -> change focus from button to parentNode LI -> finds DIV.
in short - in function myFunction(event) change
var x = document.getElementById("mydv");
to
var x = event.target.parentNode.getElementsByTagName("DIV")[0];
Working example:
https://jsfiddle.net/w2a9zg46/1/
The problem is that getElementById refers to the first element with that id. It simply ignores everything else. Using the same id for more than one element is a bad practice. An id should be a unique reference to that element, use class instead.

Access dom-repeat element's CSS class on tap

My dom repeat displays a list of icons which I can bookmark or unbookmark ,which generating dom-repeat I call a function to find if this icon is bookmarked or not,that will return CSS class
.book-marked {
color: red;
}
.not-book-marked {
color: green;
}
<template is="dom-repeat" items="{{membersList}}">
<iron-icon icon="bookmark" class$="[[_computeBookMark(item.userId)]]" on-tap="_toogleBookMark"></iron-icon>
</template>
Once I get all my list of icon now if user click that icon I need to toogle css class.so I wrote on-tap function
_toogleBookMark:function(e) {
var userData = e.model.item; //gets entire data object of that element
var index = e.model.index; //gets index of that element
},
I can't use ID since its dom-repeat ,Is there any other ways so that I can change CSS of that dom-repeat element in _toogleBookMark() function on clicking? or is it possible to change CSS with index??or using "e" reference in _toogleBookMark(e) function !!
Not sure if I understood correctly - you want to access the element you've tapped?
Just use the event.target property then. It will return the element on which the event happened, in this case, the icon you have tapped.
_toogleBookMark = function(e) {
e.target.classList.toggle("not-book-marked");
}
Check this example.
Mind you:
1) When using Shady DOM, assuming our element is a custom element, target can be a component from the element's template, not the element itself. To prevent that, use Polymer.dom(e).localTarget (read more here).
2) When using a custom element with light DOM children, the above may not be enough, your (local)target will be a light DOM child, not the element you wished for. In that case, use Element.closest(selector) to (optionally) go up the DOM to the element you want. Read more about the method here.
As you just want to swap your class on tap, do it like this:
Add an own attribute, like data-id="1" and the id attribute, but be sure they have the same value:
<template is="dom-repeat" items="{{membersList}}">
<iron-icon icon="bookmark" class$="[[_computeBookMark(item.userId)]]" on-tap="_toogleBookMark" data-id="{{item.userId}}" id="{{item.userId}}"></iron-icon>
</template>
Now, inside your _toggleBookMark function, you can access the current tapped element and swap CSS classes by:
_toogleBookMark:function(e) {
// this gives you your userId from the data-id attribute
var userId = e.path[0].dataId;
// we can access the element now with
var element = this.$$('#' + e.path[0].dataId);
if (element.classList.contains('book-marked')) {
element.classList.remove('book-marked');
element.classList.add('not-book-marked');
} else {
element.classList.add('book-marked');
element.classList.remove('not-book-marked');
}
},

Use Reveal.js with Polymer

I have a project with Polymer + Reveal.js
I have a view with polymer that gets all the Slides/Sections.
<template is="dom-repeat" items="[[my.slides]]" as="slide">
<section>
<h1>slide.title</h1>
<h2>slide.content</h2>
</section>
</template>
When I try to start Reveal.js, I have the issue related to:
(index):21136 Uncaught TypeError: Cannot read property
'querySelectorAll' of undefined
I think is because Reveal.js cannot select a Webcomponent generated by Polymer, because Reveal.js needs to have all slides content wrote on the html file by separate.
Then my question is: How to use Polymer Webcomponents with Reveal,js?
Alan: Yes, you are right.
Now I am creating DOM elements directly with JS avoiding Polymer shadowDOM elements.
Then I created a function called createSlides where - based in a JSON response - I appending slides (sections) within slides div.
Fist I create a Polymer template similar to:
<template>
<div class="reveal">
<div id="slides" class="slides">
<section>
This section will be removed
</section>
</div>
</div>
</template>
Next I removed the unused slide and appended some slides. Finally start the Reveal presentation
ready()
{
this.removeInitialSlide();
this.addSomeSlides();
this.startRevealPresentation();
}
clearInitialSlides()
{
var slidesComp = this.$.slides;
while (slidesComp.hasChildNodes()) {
slidesComp.removeChild(slidesComp.lastChild);
}
}
addSomeSlides()
{
var slide1 = document.createElement("section");
var image = document.createElement("img");
image.src = "some/path/to/image.jpg";
slide1.appendChild(image);
this.$.slides.appendChild(slide1);
var slide2 = document.createElement("section");
slide2.innerHTML = "<h1>Test content</h1>"
this.$.slides.appendChild(slide2);
}
Working fine for now.
I think you most likely can't use reveal.js in a web component created with Polymer right now and here's why.
If you look at reveal.js's code it looks for dom elements with the reveal and slides classes on the main document like this:
dom.wrapper = document.querySelector( '.reveal' );
dom.slides = document.querySelector( '.reveal .slides' );
The problem with that is that Polymer elements have their own local dom which is a different dom tree which can't be accessed using methods like document.querySelector which means reveal.js can't access to them.

How to remove a shadow root from an HTML element adorned with a Shadow DOM from a template?

I'm exploring imports, templates, shadow DOM and custom elements in Chrome Canary (33.0.1712.3). In a grid layout I have a particular content element (region of the display) that will display different web components or cloned light DOM fragments imported from files.
However, I'm unable to redisplay ordinary HTML DOM once a shadow DOM has been added because I don't know how to remove the shadow root. Once created, the shadow root remains and interferes with the rendering of ordinary DOM. (I've looked at various W3C specs such as intro to web components, shadow DOM, templates, Bidelman's articles on HTML5 Rocks, etc.) I've isolated the problem in a simple example below:
Click "show plain old div"; click "show shadowed template"; click "show plain old div". Inspect in devtools after each click. After the third click, there is no output below the buttons and in devtools I am seeing:
<div id="content">
#document-fragment
<div id="plaindiv">Plain old div</div>
</div>
What do I need to add to removeShadow() to remove the shadow root and fully reset the content element to its initial state?
removing_shadows.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<template id="shadowedTemplateComponent">
<style>
div { background: lightgray; }
#t { color: red; }
</style>
<div id="t">template</div>
<script>console.log("Activated the shadowed template component.");</script>
</template>
<template id="plainDiv">
<div id="plaindiv">Plain old div</div>
</template>
</head>
<body>
<div>
<input type="button" value="show plain old div" onclick="showPlainOldDiv()"/>
<input type="button" value="show shadowed template" onclick="showShadowTemplate()"/>
<div id="content"></div>
</div>
<script>
function removeChildren(elt) {
console.log('removing children: %s', elt);
while (elt.firstChild) {
elt.removeChild(elt.firstChild);
}
}
function removeShadow(elt) {
if (elt.shadowRoot) {
console.log('removing shadow: %s', elt);
removeChildren(elt.shadowRoot); // Leaves the shadow root property.
// elt.shadowRoot = null; doesn't work
// delete elt.shadowRoot; doesn't work
// What goes here to delete the shadow root (#document-fragment in devtools)?
}
}
function showPlainOldDiv() {
console.log('adding a plain old div');
var host = document.querySelector('#content');
removeChildren(host);
removeShadow(host);
var template = document.querySelector('#plainDiv');
host.appendChild(template.content.cloneNode(true));
}
function showShadowTemplate() {
console.log('adding shadowed template component');
var host = document.querySelector('#content');
removeChildren(host);
removeShadow(host);
var template = document.querySelector('#shadowedTemplateComponent');
var root = host.shadowRoot || host.webkitCreateShadowRoot();
root.appendChild(template.content.cloneNode(true));
}
</script>
</body>
</html>
The spec of Shadow DOM moved from v0 to v1.
One of the changes is that in v1 there is no way to create shadow root on itself and the host element may contain only one shadow root.
So it seems like the answer of replacing the shadow root with a new blank shadow root is not valid anymore.
Solution paths:
if the host element self (div in your example) has no special value beside holding that Shadow DOM, one can just replace the host element as a whole
if one still likes to preserve the host, clearing the Shadow DOM with something like e.shadowRoot.innerHTML = '' might be sufficient
You can't remove a shadow root once you add it. However, you can replace it with a newer one.
As mentioned here, http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom-301/, the newest shadow root "wins" and becomes the rendered root.
You can replace your shadow root with a new shadow root that only contains the <content> pseudo-element to insert everything from the light DOM back into the shadow DOM. At that point, as far as I know it will be functionally equivalent to having no shadow DOM at all.
rmcclellan is correct that you cannot truely "remove" a ShadowRoot v2. But, you can fake it.
The OuterHTML PARTIAL Solution
elementWithShadowDOMv2.outerHTML = elementWithShadowDOMv2.outerHTML;
HOWEVER, there is a major caveat: although there is no visual change, elementWithShadowDOMv2 still refers to the destroyed element with the ShadowDOMv2 as if elementWithShadowDOMv2.parentNode.removeChild( elementWithShadowDOMv2 ) were called. This also "removes" event listeners on the element. Observe the demo below.
var addShadowHere = document.getElementById("add-shadow-here");
addShadowHere.addEventListener("mouseenter", function() {
addShadowHere.style.border = '2em solid blue';
});
addShadowHere.addEventListener("mouseleave", function() {
addShadowHere.style.border = '';
});
var shadow = addShadowHere.attachShadow({mode:"open"});
var button = shadow.appendChild(document.createElement("button"));
button.textContent = "Click Here to Destroy The ShadowDOMv2";
button.addEventListener("click", function() {
addShadowHere.outerHTML = addShadowHere.outerHTML;
update();
});
update();
function update() {
// This just displays the current parent of the addShadowHere element
document.getElementById("parent-value").value = "" + (
addShadowHere.parentNode &&
addShadowHere.parentNode.cloneNode(false).outerHTML
);
}
<div id="add-shadow-here">Text Hidden By Shadow DOM</div>
addShadowHere.parentNode => <input readonly="" id="parent-value" />
Notice how the blue border stops working after you remove the ShadowDOM. That is because the event listeners are no longer registered on the new element: the event listeners remain registered on the old element that has now been removed from the DOM.
Thus, you must refresh any references to the element and reattach any event listeners. Here is an example of how you could reobtain a reference to the new element.
function removeShadowWithCaveat(elementWithShadow) {
if (!elementWithShadow.parentNode) return elementWithShadow.cloneNode(true);
var parent = elementWithShadow.parentNode;
var prior = elementWithShadow.previousSibling;
elementWithShadow.outerHTML = elementWithShadow.outerHTML;
return prior.nextSibling || parent.firstChild;
}
If you need access to the elements which are naturally hidden by the existing shadow root and which will become exposed after the expulsion of the shadow root, then here is an alternative method that will perfectly preserve these nodes.
function removeShadowWithCaveat(elementWithShadow) {
if (!elementWithShadow.parentNode) return elementWithShadow.cloneNode(true);
var ref = elementWithShadow.cloneNode(true);
while (elementWithShadow.lastChild) ref.appendChild( elementWithShadow.lastChild );
elementWithShadow.parentNode.replaceChild(elementWithShadow, elementWithShadow);
return ref;
}
Working Solution
var createShadowProp = (
"createShadowRoot" in Element.prototype ? "createShadowRoot" : "webkitCreateShadowRoot"
);
function removeChildren(elt) {
console.log('removing children: %s', elt);
while (elt.firstChild) {
elt.removeChild(elt.firstChild);
}
}
function removeShadowWithCaveat(elementWithShadow) {
if (!elementWithShadow.parentNode) return elementWithShadow.cloneNode(true);
var ref = elementWithShadow.cloneNode(true);
while (elementWithShadow.lastChild) ref.appendChild( elementWithShadow.lastChild );
elementWithShadow.parentNode.replaceChild(elementWithShadow, elementWithShadow);
return ref;
}
function showPlainOldDiv() {
console.log('adding a plain old div');
var host = document.querySelector('#content');
removeChildren(host);
// Remove the shadow
host = removeShadowWithCaveat(host);
var template = document.querySelector('#plainDiv');
host.appendChild(template.content.cloneNode(true));
}
function showShadowTemplate() {
console.log('adding shadowed template component');
var host = document.querySelector('#content');
removeChildren(host);
// Remove the shadow
host = removeShadowWithCaveat(host);
var template = document.querySelector('#shadowedTemplateComponent');
var root = host.shadowRoot || host[createShadowProp]({
"open": true
});
root.appendChild(template.content.cloneNode(true));
}
<div>
<input type="button" value="show plain old div" onclick="showPlainOldDiv()"/>
<input type="button" value="show shadowed template" onclick="showShadowTemplate()"/>
<div id="content"></div>
</div>
<template id="shadowedTemplateComponent" style="display:none">
<style>
div { background: lightgray; }
#t { color: red; }
</style>
<div id="t">template</div>
<script>console.log("Activated the shadowed template component.");</script>
</template>
<template id="plainDiv" style="display:none">
<div id="plaindiv">Plain old div</div>
</template>
Also note the misuse of vendor prefixes (a problem that far too many developers have issues with). You are correct that, at the time that this question was asked, there was only the prefixed version of createShadowRoot (which was webkitCreateShadowRoot). Nevertheless, you must ALWAYS check to see if the unprefixed createShadowRoot version is available in case if browsers standardize the API in the future (which is now the case). It might be nice to have your code working today, but it's awesome to have your code working several years from now.
In Chrome:
Press F12, DevTool will open
Click gear icon in DevTool
Uncheck "show user agent shadow DOM" checkbox
Enjoy !