What are the possible reasons for document.getElementById, $("#id") or any other DOM method / jQuery selector not finding the elements?
Example problems include:
jQuery silently failing to bind an event handler
jQuery "getter" methods (.val(), .html(), .text()) returning undefined
A standard DOM method returning null resulting in any of several errors:
Uncaught TypeError: Cannot set property '...' of null
Uncaught TypeError: Cannot set properties of null (setting '...')
Uncaught TypeError: Cannot read property '...' of null
Uncaught TypeError: Cannot read properties of null (reading '...')
The most common forms are:
Uncaught TypeError: Cannot set property 'onclick' of null
Uncaught TypeError: Cannot read property 'addEventListener' of null
Uncaught TypeError: Cannot read property 'style' of null
The element you were trying to find wasn’t in the DOM when your script ran.
The position of your DOM-reliant script can have a profound effect on its behavior. Browsers parse HTML documents from top to bottom. Elements are added to the DOM and scripts are (generally) executed as they're encountered. This means that order matters. Typically, scripts can't find elements that appear later in the markup because those elements have yet to be added to the DOM.
Consider the following markup; script #1 fails to find the <div> while script #2 succeeds:
<script>
console.log("script #1:", document.getElementById("test")); // null
</script>
<div id="test">test div</div>
<script>
console.log("script #2:", document.getElementById("test")); // <div id="test" ...
</script>
So, what should you do? You've got a few options:
Option 1: Move your script
Given what we've seen in the example above, an intuitive solution might be to simply move your script down the markup, past the elements you'd like to access. In fact, for a long time, placing scripts at the bottom of the page was considered a best practice for a variety of reasons. Organized in this fashion, the rest of the document would be parsed before executing your script:
<body>
<button id="test">click me</button>
<script>
document.getElementById("test").addEventListener("click", function() {
console.log("clicked:", this);
});
</script>
</body><!-- closing body tag -->
While this makes sense and is a solid option for legacy browsers, it's limited and there are more flexible, modern approaches available.
Option 2: The defer attribute
While we did say that scripts are, "(generally) executed as they're encountered," modern browsers allow you to specify a different behavior. If you're linking an external script, you can make use of the defer attribute.
[defer, a Boolean attribute,] is set to indicate to a browser that the script is meant to be executed after the document has been parsed, but before firing DOMContentLoaded.
This means that you can place a script tagged with defer anywhere, even the <head>, and it should have access to the fully realized DOM.
<script src="https://gh-canon.github.io/misc-demos/log-test-click.js" defer></script>
<button id="test">click me</button>
Just keep in mind...
defer can only be used for external scripts, i.e.: those having a src attribute.
be aware of browser support, i.e.: buggy implementation in IE < 10
Option 3: Modules
Depending upon your requirements, you may be able to utilize JavaScript modules. Among other important distinctions from standard scripts (noted here), modules are deferred automatically and are not limited to external sources.
Set your script's type to module, e.g.:
<script type="module">
document.getElementById("test").addEventListener("click", function(e) {
console.log("clicked: ", this);
});
</script>
<button id="test">click me</button>
Option 4: Defer with event handling
Add a listener to an event that fires after your document has been parsed.
DOMContentLoaded event
DOMContentLoaded fires after the DOM has been completely constructed from the initial parse, without waiting for things like stylesheets or images to load.
<script>
document.addEventListener("DOMContentLoaded", function(e){
document.getElementById("test").addEventListener("click", function(e) {
console.log("clicked:", this);
});
});
</script>
<button id="test">click me</button>
Window: load event
The load event fires after DOMContentLoaded and additional resources like stylesheets and images have been loaded. For that reason, it fires later than desired for our purposes. Still, if you're considering older browsers like IE8, the support is nearly universal. Granted, you may want a polyfill for addEventListener().
<script>
window.addEventListener("load", function(e){
document.getElementById("test").addEventListener("click", function(e) {
console.log("clicked:", this);
});
});
</script>
<button id="test">click me</button>
jQuery's ready()
DOMContentLoaded and window:load each have their caveats. jQuery's ready() delivers a hybrid solution, using DOMContentLoaded when possible, failing over to window:load when necessary, and firing its callback immediately if the DOM is already complete.
You can pass your ready handler directly to jQuery as $(handler), e.g.:
<script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
<script>
$(function() {
$("#test").click(function() {
console.log("clicked:", this);
});
});
</script>
<button id="test">click me</button>
Option 5: Event Delegation
Delegate the event handling to an ancestor of the target element.
When an element raises an event (provided that it's a bubbling event and nothing stops its propagation), each parent in that element's ancestry, all the way up to window, receives the event as well. That allows us to attach a handler to an existing element and sample events as they bubble up from its descendants... even from descendants added after the handler was attached. All we have to do is check the event to see whether it was raised by the desired element and, if so, run our code.
Typically, this pattern is reserved for elements that don't exist at load time or to avoid attaching a large number of duplicate handlers. For efficiency, select the nearest reliable ancestor of the target element rather than attaching it to the document.
Native JavaScript
<div id="ancestor"><!-- nearest ancestor available to our script -->
<script>
document.getElementById("ancestor").addEventListener("click", function(e) {
if (e.target.id === "descendant") {
console.log("clicked:", e.target);
}
});
</script>
<button id="descendant">click me</button>
</div>
jQuery's on()
jQuery makes this functionality available through on(). Given an event name, a selector for the desired descendant, and an event handler, it will resolve your delegated event handling and manage your this context:
<script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
<div id="ancestor"><!-- nearest ancestor available to our script -->
<script>
$("#ancestor").on("click", "#descendant", function(e) {
console.log("clicked:", this);
});
</script>
<button id="descendant">click me</button>
</div>
Short and simple: Because the elements you are looking for do not exist in the document (yet).
For the remainder of this answer I will use getElementById for examples, but the same applies to getElementsByTagName, querySelector, and any other DOM method that selects elements.
Possible Reasons
There are three reasons why an element might not exist:
An element with the passed ID really does not exist in the document. You should double check that the ID you pass to getElementById really matches an ID of an existing element in the (generated) HTML and that you have not misspelled the ID (IDs are case-sensitive!).
If you're using getElementById, be sure you're only giving the ID of the element (e.g., document.getElemntById("the-id")). If you're using a method that accepts a CSS selector (like querySelector), be sure you're including the # before the ID to indicate you're looking for an ID (e.g., document.querySelector("#the-id")). You must not use the # with getElementById, and must use it with querySelector and similar. Also note that if the ID has characters in it that aren't valid in CSS identifiers (such as a .; id attributes containing . characters are poor practice, but valid), you have to escape those when using querySelector (document.querySelector("#the\\.id"))) but not when using getElementById (document.getElementById("the.id")).
The element does not exist at the moment you call getElementById.
The element isn't in the document you're querying even though you can see it on the page, because it's in an iframe (which is its own document). Elements in iframes aren't searched when you search the document that contains them.
If the problem is reason 3 (it's in an iframe or similar), you need to look through the document in the iframe, not the parent document, perhaps by getting the iframe element and using its contentDocument property to access its document (same-origin only). The rest of this answer addresses the first two reasons.
The second reason — it's not there yet — is quite common. Browsers parse and process the HTML from top to bottom. That means that any call to a DOM element which occurs before that DOM element appears in the HTML, will fail.
Consider the following example:
<script>
var element = document.getElementById('my_element');
</script>
<div id="my_element"></div>
The div appears after the script. At the moment the script is executed, the element does not exist yet and getElementById will return null.
jQuery
The same applies to all selectors with jQuery. jQuery won't find elements if you misspelled your selector or you are trying to select them before they actually exist.
An added twist is when jQuery is not found because you have loaded the script without protocol and are running from file system:
<script src="//somecdn.somewhere.com/jquery.min.js"></script>
this syntax is used to allow the script to load via HTTPS on a page with protocol https:// and to load the HTTP version on a page with protocol http://
It has the unfortunate side effect of attempting and failing to load file://somecdn.somewhere.com...
Solutions
Before you make a call to getElementById (or any DOM method for that matter), make sure the elements you want to access exist, i.e. the DOM is loaded.
This can be ensured by simply putting your JavaScript after the corresponding DOM element
<div id="my_element"></div>
<script>
var element = document.getElementById('my_element');
</script>
in which case you can also put the code just before the closing body tag (</body>) (all DOM elements will be available at the time the script is executed).
Other solutions include listening to the load [MDN] or DOMContentLoaded [MDN] events. In these cases it does not matter where in the document you place the JavaScript code, you just have to remember to put all DOM processing code in the event handlers.
Example:
window.onload = function() {
// process DOM elements here
};
// or
// does not work IE 8 and below
document.addEventListener('DOMContentLoaded', function() {
// process DOM elements here
});
Please see the articles at quirksmode.org for more information regarding event handling and browser differences.
jQuery
First make sure that jQuery is loaded properly. Use the browser's developer tools to find out whether the jQuery file was found and correct the URL if it wasn't (e.g. add the http: or https: scheme at the beginning, adjust the path, etc.)
Listening to the load/DOMContentLoaded events is exactly what jQuery is doing with .ready() [docs]. All your jQuery code that affects DOM element should be inside that event handler.
In fact, the jQuery tutorial explicitly states:
As almost everything we do when using jQuery reads or manipulates the document object model (DOM), we need to make sure that we start adding events etc. as soon as the DOM is ready.
To do this, we register a ready event for the document.
$(document).ready(function() {
// do stuff when DOM is ready
});
Alternatively you can also use the shorthand syntax:
$(function() {
// do stuff when DOM is ready
});
Both are equivalent.
Reasons why id based selectors don't work
The element/DOM with id specified doesn't exist yet.
The element exists, but it is not registered in DOM [in case of HTML nodes appended dynamically from Ajax responses].
More than one element with the same id is present which is causing a conflict.
Solutions
Try to access the element after its declaration or alternatively use stuff like $(document).ready();
For elements coming from Ajax responses, use the .bind() method of jQuery. Older versions of jQuery had .live() for the same.
Use tools [for example, webdeveloper plugin for browsers] to find duplicate ids and remove them.
If the element you are trying to access is inside an iframe and you try to access it outside the context of the iframe this will also cause it to fail.
If you want to get an element in an iframe you can find out how here.
As #FelixKling pointed out, the most likely scenario is that the nodes you are looking for do not exist (yet).
However, modern development practices can often manipulate document elements outside of the document tree either with DocumentFragments or simply detaching/reattaching current elements directly. Such techniques may be used as part of JavaScript templating or to avoid excessive repaint/reflow operations while the elements in question are being heavily altered.
Similarly, the new "Shadow DOM" functionality being rolled out across modern browsers allows elements to be part of the document, but not query-able by document.getElementById and all of its sibling methods (querySelector, etc.). This is done to encapsulate functionality and specifically hide it.
Again, though, it is most likely that the element you are looking for simply is not (yet) in the document, and you should do as Felix suggests. However, you should also be aware that that is increasingly not the only reason that an element might be unfindable (either temporarily or permanently).
If script execution order is not the issue, another possible cause of the problem is that the element is not being selected properly:
getElementById requires the passed string to be the ID verbatim, and nothing else. If you prefix the passed string with a #, and the ID does not start with a #, nothing will be selected:
<div id="foo"></div>
// Error, selected element will be null:
document.getElementById('#foo')
// Fix:
document.getElementById('foo')
Similarly, for getElementsByClassName, don't prefix the passed string with a .:
<div class="bar"></div>
// Error, selected element will be undefined:
document.getElementsByClassName('.bar')[0]
// Fix:
document.getElementsByClassName('bar')[0]
With querySelector, querySelectorAll, and jQuery, to match an element with a particular class name, put a . directly before the class. Similarly, to match an element with a particular ID, put a # directly before the ID:
<div class="baz"></div>
// Error, selected element will be null:
document.querySelector('baz')
$('baz')
// Fix:
document.querySelector('.baz')
$('.baz')
The rules here are, in most cases, identical to those for CSS selectors, and can be seen in detail here.
To match an element which has two or more attributes (like two class names, or a class name and a data- attribute), put the selectors for each attribute next to each other in the selector string, without a space separating them (because a space indicates the descendant selector). For example, to select:
<div class="foo bar"></div>
use the query string .foo.bar. To select
<div class="foo" data-bar="someData"></div>
use the query string .foo[data-bar="someData"]. To select the <span> below:
<div class="parent">
<span data-username="bob"></span>
</div>
use div.parent > span[data-username="bob"].
Capitalization and spelling does matter for all of the above. If the capitalization is different, or the spelling is different, the element will not be selected:
<div class="result"></div>
// Error, selected element will be null:
document.querySelector('.results')
$('.Result')
// Fix:
document.querySelector('.result')
$('.result')
You also need to make sure the methods have the proper capitalization and spelling. Use one of:
$(selector)
document.querySelector
document.querySelectorAll
document.getElementsByClassName
document.getElementsByTagName
document.getElementById
Any other spelling or capitalization will not work. For example, document.getElementByClassName will throw an error.
Make sure you pass a string to these selector methods. If you pass something that isn't a string to querySelector, getElementById, etc, it almost certainly won't work.
If the HTML attributes on elements you want to select are surrounded by quotes, they must be plain straight quotes (either single or double); curly quotes like ‘ or ” will not work if you're trying to select by ID, class, or attribute.
I want to use a java-script method in a polymer Template. I am using Vaadin with Polymer Elements. In my Project I have a Vaadin-Grid of Objects that can be of different type. I want to render these types with different Templates.
This problem can be solved with a dom-if template, as described by ollitietavainen in this answer
This works perfectly, but there is a problem. When using more than two different Types of Objects in the Grid, one would need to use the same amount of booleans to set that up. Suppose we have a fictional shop that displays PC-Parts, and each type of PC-Part needs to be rendered with its own template, then we would need something like the fallowing. This is quite cumbersome.
private boolean isMemory(AbstractPcPart pcPart) {
return pcPart.getClass().equals(Memory.class);
}
private boolean isGraphicsCard(AbstractPcPart pcPart) {
return pcPart.getClass().equals(GraphicsCard.class);
}
private boolean isCPU(AbstractPcPart pcPart) {
return pcPart.getClass().equals(CPU.class);
}
// … is-checker for all other types of pcParts.
private void initColumn() {
addColumn(Objects.requireNonNull(CardFactory.getTemplate())
.withProperty("partCard", CardFactory::create)
.withProperty("isMemory", this::isMemory)
.withProperty("isGraphicsCard", this::isGraphicsCard)
.withProperty("isCPU", this::isCPU)
// add all other properties
);
}
The corresponding Templates would look something like this.
<template is='dom-if' if='[[item.isMemory]]'>"
<memory-card part-card='[[item.partCard]]'>
</memory-card>"
</template>
<template is='dom-if' if='[[item.isGraphicsCard]]'>"
<graphics-card part-card='[[item.partCard]]'>
</graphics-card-card>"
</template>
<template is='dom-if' if='[[item.isCPU]]'>"
<cpu-card part-card='[[item.partCard]]'>
</cpu-card>"
</template>
<!-- one additional template for every type of part -->
The question now is, if there is any other way, that would not be needing all these Properties.
Luckily there is, as Kuba Šimonovský explained in an answer to another question.
Using this method we could rewrite the code from above to something like the fallowing.
private String type(AbstractPcPart pcPart) {
return pcPart.getClass().getSimpleName();
}
private void initColumn() {
addColumn(Objects.requireNonNull(CardFactory.getTemplate())
.withProperty("partCard", CardFactory::create)
.withProperty("type", this::type));
}
This time we use a java-script method to conditionally select the corresponding template.
<template is='dom-if' if='[[_isEqualTo(item.type, "Memory")]]'>"
<memory-card part-card='[[item.partCard]]'>
</memory-card>"
</template>
<template is='dom-if' if='[[_isEqualTo(item.type, "GraphicsCard")]]'>"
<graphics-card part-card='[[item.partCard]]'>
</graphics-card-card>"
</template>
<template is='dom-if' if='[[_isEqualTo(item.type, "CPU")]]'>"
<cpu-card part-card='[[item.partCard]]'>
</cpu-card>"
</template>
<!-- one additional template for every type of part -->
The Polymer Template is a bit more complicated now, but on the java side, the code is much shorter, and possibly easier to maintain. There is probably still some overhead, as every template gets added to the dom. But in addition to that only the content from the templates that we want to see gets added to the dom.
I don’t think there is a better way to do this though.
So using this method, we need a java-script method called _isEqualTo. This method is not a standard method so we need to implement it ourselves. The implementation for this method is straightforward.
function _isEqualTo(one, other) {
return one == other;
}
But the answer from Kuba does not specify where to implement this method. I have tried to put the method in different places with no luck. The js console in my browser always complains that it can not find the method.
Digging a little bit deeper I found this Link. So maybe what i want to have is a global variable.
window._isEqualTo = function(one, other) {
return one == other;
}
But even with this change the same warning persists. What’s weird is that the function is visible in the interactive console in the developer tools. Setting a breakpoint in the java-script file that i have added the function; and calling the function in the console reveals that it is really the correct function that get’s called, leading me to beleave that the function gets initialized too late in the lifecycle of the application. Although I am not sure at all.
And because the function is not found, the grid in the view will be empty. It still shows the rows, but they don’t show content.
I really hope someone can help me out.
Here is a Git-Repository to reproduce my problem. The concerning views are the PartsDomIfView and the PartsDomIfElegantView.
Instead of using the deprecated TemplateRenderer, you could create a LitRenderer (v22+) and create a custom lit component that can be used there as your column's content. In there you could create complex logic based templates as a separate component, that can be better maintained.
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.
I'm starting to learn angularJS better, and I've noticed that AngularJS tries to make strong emphasis on separating the view from the controller and encapsulation. One example of this is people telling me DOM manipulation should go in directives. I kinda got the hang of it now, and how using link functions that inject the current element allow for great behavior functionality, but this doesn't explain a problem I always encounter.
Example:
I have a sidebar I want to open by clicking a button. There is no way to do this in button's directive link function without using a hard-coded javascript/jquery selector to grab the sidebar, something I've seen very frowned upon in angularJS (hard-coding dom selectors) since it breaks separation of concerns. I guess one way of getting around this is making each element I wish to manipulate an attribute directive and on it's link function, saving a reference it's element property into a dom-factory so that whenever a directive needs to access an element other than itself, it can call the dom-factory which returns the element, even if it knows nothing where it came from. But is this the "Angular way"?
I say this because in my current project I'm using hard-coded selectors which are already a pain to mantain because I'm constantly changing my css. There must be a better way to access multiple DOM elements. Any ideas?
There are a number of ways to approach this.
One approach, is to create a create a sidebar directive that responds to "well-defined" broadcasted messages to open/close the sidebar.
.directive("sidebar", function(){
return {
templateUrl: "sidebar.template.html",
link: function(scope, element){
scope.$root.$on("openSidebar", function(){
// whatever you do to actually show the sidebar DOM content
// e.x. element.show();
});
}
}
});
Then, a button could invoke a function in some controller to open a sidebar:
$scope.openSidebar = function(){
$scope.$root.$emit("openSidebar");
}
Another approach is to use a $sidebar service - this is somewhat similar to how $modal works in angularui-bootstrap, but could be more simplified.
Well, if you have a directive on a button and the element you need is outside the directive, you could pass the class of the element you need to toggle as an attribute
<button my-directive data-toggle-class="sidebar">open</button>
Then in your directive
App.directive('myDirective', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
angular.element('.' + attrs.toggleClass).toggleClass('active');
}
};
}
You won't always have the link element argument match up with what you need to manipulate unfortunately. There are many "angular ways" to solve this though.
You could even do something like:
<div ng-init="isOpen = false" class="sidebar" ng-class="{'active': isOpen}" ng-click="isOpen = !isOpen">
...
</div>
The best way for directive to communicate with each other is through events. It also keeps with the separation of concerns. Your button could $broadcast on the $rootScope so that all scopes hear it. You would emit and event such as sidebar.open. Then the sidebar directive would listen for that event and act upon it.
I have run into a problem where paper-dropdown element's on-core-select event is being fired by a core-selector belonging to a separate element in my polymer app. Here are excerpts from the polymer element that includes the paper-dropdown (along with the polymer event script):
<paper-dropdown id="widthUnits" class="unitSelection" selected="{{item.data.designWidth[1]}}" on-core-select="{{ conditionUnitSelectionChanged }}" valueattr="label">
<paper-item label="mm"></paper-item>
<paper-item label="cm"></paper-item>
<paper-item label="m"></paper-item>
</paper-dropdown>
conditionUnitSelectionChanged: function(e, detail, sender) {
// Ensure the selection has in fact changed
if (sender.selected != detail.item.label)
{
this.ajaxUpdateUnit(sender);
}
},
And here is the core-selector and related code that is part of an entirely different element within the application. FYI, SelectedItemKey is watched by polymer 'Changed' events in both of the elements involved...if that matters.
<core-selector id="itemSelector" target="{{$.itemList}}" multi="false" selected="{{selectedItemKey}}" selectedAttribute="active"></core-selector>
<div id="itemList" layout horizontal wrap>
<template repeat="{{item, i in items}}">
<div id="{{item.name}}">
<entriesHeading name="{{item.name}}" horizontal layout center>
<div flex>{{item.name}}</div>
<paper-menu-button icon="more-vert" halign="right">
<paper-item label="Edit" on-tap="{{ itemEdit }}"></paper-item>
<paper-item label="Copy" on-tap="{{ itemCopy }}"></paper-item>
<paper-item label="Delete" on-tap="{{ itemDelete }}"></paper-item>
</paper-menu-button>
</entriesHeading>
<entriesContainer vertical layout>
*** container contents ***
</entriesContainer>
</div>
</template>
</div>
Any suggestions on how I can avoid this unwanted interplay with core-select events? Perhaps a specific listener of some sort (limited to listening for paper-dropdown(s) core-select event)?
It's not possible for the paper-dropdown to receive an event from anywhere but inside it's own subtree. You have to present a jsbin or some kind of reproduction, otherwise I must suggest your diagnosis is incorrect.
You should try to figure out what is going on with the events, to make sure you have good understanding of the system.
Having said that, another way of approaching the problem is by being data-driven and not control-driven.
IOW, it's best to react to data-changes instead of events. It's hard to give really good suggestions because I can only see a tiny piece of your application, but here are some suggestions:
You have
<paper-dropdown id="widthUnits" class="unitSelection"
selected="{{item.data.designWidth[1]}}"
on-core-select="{{ conditionUnitSelectionChanged }}" valueattr="label">
It's a bit unfortunate that this important data is referenced as item.data.designWidth[1]. Generally one wants to factor the application data so that you aren't using deeply nested expressions like that. Just as an example, if you could build a UI like design-width-editor and bind it to <design-width-editor designWidth="{{item.data.designWidth[1]}}"> then you could put logic inside of design-width-editor that just deals with designWidth and doesn't need to know about item or data. This gives you a lot more flexibility with your data structures and makes it easier to think about.
In any case, given the construction you have, one thing you could do is observe the data directly:
observe: {
'item.data.designWidth[1]`: 'designWidth1Changed'
}
Now you can implement designWidth1Changed() to take the needed action. The key bit here is that you are no longer dependent on any particular UI for modifying the designWidth data. You can replace that UI at will; all that matters is that if the value changes, some action is taken.
Scott put me on the right track. After some refactoring as described in the previous comments, I used async to my advantage in order to avoid observers executing when I didn't want them to (such as when the elements model item object changed...and therefore all of its observed properties). Here is a sample of some of the script code from the host element mentioned above that was implemented to resolve the final issue:
ignoreChanges: null,
observe: {
'item.data.designWidth[0]': 'designWidthValueChanged',
'item.data.designWidth[1]': 'designWidthUnitChanged',
}
designWidthValueChanged: function(oldVal, newVal) {
if (!this.ignoreChanges) {
// send update of width value input via ajax
this.ajaxUpdateCondition("designWidth", newVal, this.item.data.designWidth[1]);
}
},
designWidthUnitChanged: function(oldVal, newVal) {
if (!this.ignoreChanges) {
// send update of width unit selection via ajax
this.ajaxUpdateCondition("designWidth", this.item.data.designWidth[0], newVal);
}
},
itemKeyChanged: function(oldVal, newVal) {
// itemKey is a published attribute that is 2 way bound to a parent element (where item selection occurs from a collection)
this.toggleIgnoreChanges(true); //set flag to ignore changes to observed values while item object switches
this.item = this.items[newVal]; //point to the correct/selected item in the collection (items is published attribute of this element)
this.async(this.toggleIgnoreChanges); //reset flag after observe functions have executed
},
toggleIgnoreChanges: function(flagstatus) {
this.ignoreChanges = flagstatus || !this.ignoreChanges;
}