I'm trying to understand what events that originate in light DOM look like when received in shadow DOM via a <content> element. I'm reading the Shadow DOM W3C Draft, and I don't entirely understand it but it sounds like events are to be "retargeted" from the point of view of the EventListener attachment.
In the cases where event path is across multiple node trees, the
event's information about the target of the event is adjusted in order
to maintain encapsulation. Event retargeting is a process of computing
relative targets for each ancestor of the node at which the event is
dispatched. A relative target is a node that most accurately
represents the target of a dispatched event at a given ancestor while
maintaining the encapsulation.
and
At the time of event dispatch:
The Event target and currentTarget attributes must return the relative
target for the node on which event listeners are invoked
So here's a simple Polymer custom element that just puts its children into a container, and adds a click EventListener to the container (in the shadow DOM). In this case the child is a button.
<!DOCTYPE html>
<html>
<head>
<script src="bower_components/platform/platform.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">
</head>
<body unresolved>
<polymer-element name="foo-bar">
<template>
<div id="internal-container" style="background-color:red; width:100%;">
<content></content>
</div>
</template>
<script>
Polymer("foo-bar", {
clickHandler: function(event) {
console.log(event);
var element = event.target;
while (element) {
console.log(element.tagName, element.id);
element = element.parentElement;
}
},
ready: function() {
this.shadowRoot.querySelector('#internal-container').addEventListener('click', this.clickHandler);
}
});
</script>
</polymer-element>
<foo-bar id="custom-element">
<button>Click me</button>
</foo-bar>
</body>
</html>
When I run this on Chrome 38.0.2075.0 canary, when I click on the button I get:
MouseEvent {dataTransfer: null, toElement: button, fromElement: null, y: 19, x: 53…}altKey: falsebubbles: truebutton: 0cancelBubble: falsecancelable: truecharCode: 0clientX: 53clientY: 19clipboardData: undefinedctrlKey: falsecurrentTarget: nulldataTransfer: nulldefaultPrevented: falsedetail: 1eventPhase: 0fromElement: nullkeyCode: 0layerX: 53layerY: 19metaKey: falsemovementX: 0movementY: 0offsetX: 45offsetY: 10pageX: 53pageY: 19path: NodeList[0]relatedTarget: nullreturnValue: truescreenX: 472screenY: 113shiftKey: falsesrcElement: buttontarget: buttontimeStamp: 1404078533176toElement: buttontype: "click"view: WindowwebkitMovementX: 0webkitMovementY: 0which: 1x: 53y: 19__proto__: MouseEvent test.html:17
BUTTON test.html:20
FOO-BAR custom-element test.html:20
BODY test.html:20
HTML test.html:20
and when I click on the container I get:
MouseEvent {dataTransfer: null, toElement: div#internal-container, fromElement: null, y: 15, x: 82…} test.html:17
DIV internal-container test.html:20
So I get an event target in either the light or shadow DOM, depending on which DOM the source element was in. I was expecting to get a target from the shadow DOM in both cases because that's where the EventListener is attached. My questions are:
Is this the way it is supposed to work, and
If so, is there an alternative way to get events that bubble up from the light DOM retargeted to the shadow DOM?
In case someone wants to ask, "What are you trying to do?", I'm not trying to do anything specifically other than understand the behavior.
Events with shadow dom are tricky. I try to capture a braindump below.
Is this the way it is supposed to work
Yep. If you're testing in Chrome, you get native shadow dom.
I wrote a section on event retargeting in the HTML5Rocks - Shadow DOM 301 article. Basically, retargeting means that events that originate in the shadow dom look like they come from the element itself.
In your example, you're logging the event internal to the shadow dom, so it's still seen there. If you also add a 'click' listener outside of the element, the target will look as if it came from the element:
<script>
var el = document.querySelector('#custom-element');
el.addEventListener('click', function(e) {
console.log(e.target.tagName); // logs FOO-Bar
});
</script>
http://jsbin.com/womususe/1/edit
The 'click' event bubbles. This is why you see BUTTON in your top example. Why do you see it at all? You see it because the button is not part of your element's shadow dom. It's in the light dom and the target of the element. It's important to remember that light DOM nodes are still logically in the main document. They're not moved into the shadow dom, merely rendered at <content> insertion points.
BTW, there are a couple of Polymerized fixes to your examples:
this.shadowRoot.querySelector('#internalcontainer') -> this.$.internalcontainer. this.$.ID is Polymer's "automatic node finding" feature.
You don't need to use addEventListener() at all. Instead , use <div id="internalcontainer" on-click="{{clickHandler}}">. This is a declarative event handler.
Related
I used the below html code for checking how the event bubbling works. I added the event handler for child element using $(document).delegate and for parent and super element I used HTML event attributes for handling click events. Generally when clicking on child element the event should gets bubbled from child to parent wise. Since I am using delegate for adding event in child element. When I click on child element the child alert gets triggered last after the parent and super gets triggered.
Could any body please help how to make the child to triggered first using $documet.delegate
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script type="text/javascript">
$(document).delegate("#child", 'click', clickHandler);
function Superclick(event){
alert ("super");
}
function Parentclick(event){
alert ("parent");
}
function clickHandler(e){
alert("child");
}
</script>
</head>
<body>
<div style="width:500px;height:500px;background-color:red" id="super" onclick="Superclick(event)">
<div style="width:300px;height:300px;background-color:green" id="parent" onclick="Parentclick(event)">
<div style="width:250px; height:250px; background-color:yellow" id="child">
</div>
</div>
</div>
</body>
</html>
There's nothing you can do to stop the child event being invoked last in this case as that is specifically how event delegation works. It relies on the event bubbling through the DOM from the event target to the parent where the delegated handler is bound.
Also note that delegate() is deprecated. You should use on() instead, however it won't make a difference in this example.
There may be workarounds you can use to retrieve the necessary parent elements from the child using DOM traversal in the single delegated event handler, however how that would be done and any benefits it has would depend on your specific use case.
Sorry if this comes out a bit garbled, I'm not sure how to ask this question.
What I am trying to do is keep the DOM synced with a localStorage value, and am updating the localStorage value with an interact.js mouse event.
Currently, I am able to properly set the localStorage value, but am having problems updating the DOM.
My current build is within the Polymer framework, so I am having trouble selecting shadow DOM content.
The DOM tree looks like
PARENT-ELEMENT
# SHADOW ROOT
EL
EL
DIV
CUSTOM ELEMENT
EL
EL
Here are some ways I have failed to solve the problem. The Custom Element is in pure JS, since I am not sure how to properly wrap interact.js function in Polymer:
I tried directly accessing the PARENT-ELEMENT's shadow DOM from the Custom Element in pure JS.
var shadowDOMNode = document.querySelector('PARENT-ELEMENT');
var dom_object_1 = shadowDOMNode.querySelector('#dom_object_1');
dom_object_1.innerHTML = localStorage.dom_object_1;
I tried selecting a helper updateDOM() function from the PARENT Polymer element and running it from the Custom Element's setter directly.
if (event.dy > 0) {
this.$$('PARENT-ELEMENT').updateDOM();
}
Maybe I am taking the wrong approach entirely, but I haven't been able to find analogues for interact.js in using native Polymer functions.
I hope this question was clear enough...
If we ignore the interact.js part of the problem and focus on Polymer, you could probably solve this without coupling the two.
To bind to a localStorage value with Polymer, use the <iron-localstorage> element. In the following example, the localStorage value named flavor_1_amount is loaded and stored into a property named _flavor1Amount. If the value doesn't exist in localStorage or is empty, the <iron-localstorage> element fires an event (iron-localstorage-load-empty), which allows you to bind to a callback (e.g., to initialize it).
<iron-localstorage name="flavor_1_amount"
value="{{_flavor1Amount}}"
use-raw
on-iron-localstorage-load-empty="_initFlavor1Amount">
</iron-localstorage>
In the same element, you could provide an input for the user to update the localStorage value.
<paper-input label="Flavor Amount (mL)" value="{{_flavor1Amount}}"></paper-input>
And you can use <iron-localstorage>.reload() to keep your data binding in sync, assuming it could be changed externally.
See this codepen for a full demo. Check your localStorage from Chrome DevTools:
Generally speaking you should use this.set() or any of the array mutation methods if it's an array in order for the ShadowDOM to be notified properly.
Since you want to perform this update from outside the element itself, imperatively, I'd suggest this:
Expose a couple of methods from your element that you can use to add/remove/change property values from outside your element.
These methods would internally use the proper channels to make the changes.
An example (you can call addItem() to add items from outside your element):
<base href="https://polygit.org/components/">
<script src="webcomponentsjs/webcomponents-lite.min.js"></script>
<link href="polymer/polymer.html" rel="import">
<dom-module id="x-example">
<template>
<template is="dom-repeat" items="[[data]]">
<div>{{item.name}}</div>
</template>
</template>
<script>
HTMLImports.whenReady(function() {
"use strict";
Polymer({
is: "x-example",
properties: {
data: {
type: Array,
value: [
{name: "One"},
{name: "Two"},
{name: "Three"}
]
}
},
// Exposed publicly, grab the element and use this method
// to add your item
addItem: function(item) {
this.push("data", item);
}
});
});
</script>
</dom-module>
<x-example id="x-example-elem"></x-example>
<script>
setTimeout(function() {
// simply 'grab' the element and use the
// `addItem()` method you exposed publicly
// to add items to it.
document.querySelector("#x-example-elem").addItem({name: "Four"});
}, 2500);
</script>
Important: That being said, this is not the "Polymeric" way of doing stuff as this programming-style is imperative, in constrast with Polymer's style which is more declarative. The most Polymeric solution is to wrap your interact.js functionality in an element itself and use data-binding between your 2 elements to perform the changes.
Hi I wanted to disabled the context menu on my site in general so I put oncontextmenu= return false in the body, but I want it enabled for one specific image on the site. Is there any way to re enable the context menu for this specific element?
nYour mistake is to add it to the body tag, because this effectively ensures that an "oncontextmenu" event is added to every element that is a child of <body> - this is because of event "bubbling" in the DOM.
Here is an example that I think achieves what you're looking for:
<html>
<body>
<h1 oncontextmenu="test();">Right click me to do custom stuff!</h1>
<h1>Right click me and the usual should happen</h1>
</body>
<script type="text/javascript">
function test() {
window.alert("Custom stuff happens!");
}
</script>
</html>
While it is possible to prevent propagation of events (see jQuery's https://api.jquery.com/event.stoppropagation/ function as a good way to do this), in your case the above should do the trick without needing a lot of extra code to decide whether the element clicked was supposed to respond to the event or not.
Element needs some time for template-repeat to render all content, so paper-spinner is used to notify the user to wait.
How can I know that template-repeat has finished so I can turn off the spinner?
And related question: how can inner element "item-details" be selected? Again, template-repeat has to be finished first.
Here's the code I am using:
<polymer-element name="item-list">
<template>
<paper-spinner active></paper-spinner>
<template id="repeat_items" repeat="{{ item in car.items }}">
<item-details id="item_details" item="{{item}}"></item-details>
</template>....
This is some simulation of the problem: plnkr.co
Edit
links from research:
spinner example
why does onmutation disconnect after first mutation?
polymer-how-to-watch-for-change-in-content-properties
There are component lifecycle hooks.
You are probably looking for domReady.
Called when the element’s initial set of children are guaranteed to exist. This is an appropriate time to poke at the element’s parent or light DOM children. Another use is when you have sibling custom elements (e.g. they’re .innerHTML‘d together, at the same time). Before element A can use B’s API/properties, element B needs to be upgraded. The domReady callback ensures both elements exist.
Polymer('tag-name', {
domReady: function() {
// hide the spinner
// select the first item details element
}
});
As for selecting elements, you can traverse the component's shadow dom like so:
this.shadowRoot.querySelector(selector);
EDIT...
The domReady hook is great if you have all of your data up-front. If you get data asynchronously, then you can use a change watcher.
Here's is a fork of your plunkr that successfully selects the child components after the data changes. Notice the setTimeout(f, 1) that defers selection until after the DOM updates.
carsChanged: function(){
var _this = this;
setTimeout(function(){
console.log(_this.shadowRoot.querySelectorAll('item-details'))
},1)
}
I suggest something like this - http://jsbin.com/bifene/4/edit
Leverages Polymer's onMutation function to watch for changes to a DOM node. Note that it only gets called once so you'll need to re-register it every time you load new items & restart the spinner.
I have a custom Polymer element that is meant to represent a complex input type. It contains an actual input tag inside its Shadow DOM, like this:
<polymer-element name="my-input">
<template>
<input type="text" on-blur="{{onBlur}}" on-focus="{{onFocus}}"/>
</template>
<script>
Polymer('my-input', {
onBlur: ...,
onFocus: ...
});
</script>
</polymer-element>
Currently, focus and blur events are leaking to outside listeners when the user clicks between the internal input and other areas within the custom element. If you open the Dev Tools Console in this CodePen, you'll see internal and external focus and blur events happening even when clicking between the input and the surrounding green area (which is all inside the custom element).
Is there no way to capture the focus and blur events inside my custom element so I can only fire them when actually focusing and blurring the entire custom element?
Turns out I needed a tabindex on my polymer-element tag:
<polymer-element name="my-input" tabindex="0">
Here's a working CodePen where the external focus and blur events only register when the entire element is focused and blurred.