How to know when iron-list is finished creating html - javascript

Background
I am creating a a polymer iron-list and populating the list by setting the items property directly, like so: document.getElementById('itemsList').items = data;
When user changes category I change value of items in the above manner (retrieved with ajax). This works perfectly, but I now need to change the options in a select depending on my list category. I was able to do this using templating but it's cumbersome and may not even work later when the options need to be dynamic.
I would like to simply hide certain options with JavaScript but the select I want to manipulate is not present immediately after doing .items = data. I need a callback or some other way to detect when iron-list is done inserting HTML.
Research
I looked through the documentation and couldn't find any references to callbacks or events other than iron-resize, and that doesn't look helpful.
I could potentially figure it out by listening DOMNodeInserted events but that's probably worse than the solution I've already got.
A setTimeout would also work, but is also a bad solution.
From miyamoto: I could check _itemsRendered on iron-list which gets set to true, but I would probably need to do setInterval to check, also bad.
Question
Is there a callback of any sort to let me know when iron-list is finished creating HTML? Failing that maybe an event or something else I could use to know when it's done?
Update:
Answer seemd to be dom-change event, but now it seems it doesn't always fire. See Polymer iron-list does not always fire dom-change event

Something like this then. The computed bindings computeItems and computeOptions update their value as data or listCategory changes for the former, or item or listCategory for the latter changes. This allows polymer to manage all the data bindings for us: you just have to provide some function to compute it.
NB: That computing functions are not called until all dependent properties are define, i.e. not undefined.
<dom-module is="some-element">
<iron-list items="{{computeItems(data, listCategory)}}">
<template>
<select>
<template is="dom-repeat" items="{{computeOptions(item, listCategory)}}" as="option">
<option value="{{option.value}}"></option>
</template>
</select>
</template>
</iron-list>
</dom-module>
<script>
Polymer({
is: "some-element",
properties: {
data: Array,
listCategory: String
},
computeOptions: function(item, listCategory){
return item.options.filter(e=>e.category === listCategory)
},
computeItems: function(data, listCategory){
return data.filter(e=>e.category === listCategory)
}
})
</script>

We are currently trying to go a different route, but if anyone else needs to accomplish this it looks like the dom-change event should tell you.
Update:
This does not work in all cases, see Polymer iron-list does not always fire dom-change event

Related

Why does knockout template binding stop working after manually reordering - and reverting - dom items?

I am using a knockout foreach (more specifically, template: { foreach: items }) binding to display a list of elements.
I then proceed to take the following actions:
Swap the first and second elements of the observable array. I see the changes reflected on screen, as expected.
Repeat the previous action to revert to the initial state. Again, this works as expected.
Now, swap the first and second DOM elements. I see the changes reflected on screen, as expected.
Repeat the previous action to revert to the initial state. Again, this works as expected.
Even though we have manually tampered with the DOM, we have reverted to exactly the initial state, without invoking knockout during the DOM tampering. This means the state is restored to the last time knockout was aware of it, so it should look to knockout as if nothing ever changed to begin with.
However, if I perform the first action again, that is, swap the first two elements in the array, the changes are not reflected on screen.
Here is a jsfiddle to illustrate the problem: https://jsfiddle.net/k7u5wep9/.
I know that manually tampering with the DOM managed by knockout is a bad idea and that it can lead to undefined behaviour. This is unfortunately unavoidable in my situation due to third party code. What stumps me is that, even after reverting the manual edits to the exact initial state, knockout still does not work as expected.
My question is: what causes this behaviour?
And then, how does one work around it?
Turns out there is nothing magical happening here. The mistake I made was to only consider elements instead of all nodes. The knockout template binding keeps a record of all nodes when reordering, not just elements.
Before manually editing the DOM, the child nodes of the template binding are:
NodeList(6) [text, div, text, text, div, text].
After manually swapping the first two elements using parent.insertBefore(parent.children[1], parent.children[0]), this turns into:
NodeList(6) [text, div, div, text, text, text].
Repeating the action yields:
NodeList(6) [text, div, div, text, text, text].
Although this is identical to the initial state when only referring to elements, it is quite different when referring to all nodes.
The solution now becomes clear. One way to perform a proper manual swap is to replace
parent.insertBefore(parent.children[1], parent.children[0]);
with
let nexts = [parent.children[0].nextSibling, parent.children[1].nextSibling];
parent.insertBefore(parent.children[1], nexts[0]);
parent.insertBefore(parent.children[0], nexts[1]);
as seen in https://jsfiddle.net/k7u5wep9/2/.
Obviously more care has to be taken when there are no text nodes before/after, but the idea remains the same.
By manipulating the DOM, you have broken the binding made.
Do not manipulate directly the DOM. Knockout will not detect the changes made.
If you put a with: items around your foreach, it at least keeps working but requires double click if dom order != array order .. might get you on track atleast, maybe you can re-order the ko-array inside the dom function to keep their 'orders' in sync?
let vm = {
items: ko.observableArray(['item1', 'item2']),
reorder_array() {
vm.items([vm.items()[1], vm.items()[0]]);
},
reorder_dom() {
let parent = document.querySelector('#items');
parent.insertBefore(parent.children[1], parent.children[0]);
vm.reorder_array();
}
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div data-bind="with: items">
<div id="items" data-bind="template: { foreach: $data }">
<div data-bind="text: $data"></div>
</div>
</div>
<button data-bind="click: reorder_array">Reorder array</button>
<button data-bind="click: reorder_dom">Reorder DOM</button>
<div>
Reorder the array twice, then reorder DOM twice. This should work as expected, and end up with the initial state. Then, try to reorder the array again. It should not work. Why?
</div>

View not updating even the html changed

I used Vuejs, and the following code is from a function under the methods property.
I try to manipulate the DOM inside a promise, where I retrieve info from the database and use that info as a selector:
.then(() =>{
for (var i in obj){
for (var j in obj[i]){
document.getElementById(i + j).style.display = 'none';
}
}
})
I tried both jquery and js, and log them out to see if they selected the right element, and they did print out the correct element that I want to manipulate, however, when I try to addClass() removeClass() or hide(), even though these actions are executed successfully(I verified it by logging out the changed element), they are not updated in the view, when I set the display property as "none", and I checked the HTML code, these elements are not in the HTML, but they are displayed normally in the view.
I wonder why this happened, and wanted to know if there's a proper way to update the view as well, thank you.
Vue's DOM objects are kept in a Shadow DOM in browsers that support it. This provides isolation from the rest of the DOM but will appear hidden in some cases. This is why your attempts to alter the DOM aren't having any effect.
The very point of Vue is to remove the need to directly alter the DOM. The first parts of the Vue Guide show just how easy it is to update the view simply by altering data.
You're missing style. document.getElementById(i + j).style.display = 'none';
As an aside, you can and should really do this through Vue using the data object. Vue is data oriented and its reactivity works using data, computed, etc. Directly manipulating the DOM as you are doing will bypass any reactivity.

Triggering change on input

I have written some code that changes an input quantity on a magento 1.9 ecommerce website.
jQuery("input.qty").val("10");
The problem is the javascript that triggers the total to update doesn't fire. I have found the code responsible and it looks like this:
(function() {
var qtyFields = $('super-product-list').select('input.qty');
qtyFields.each(function(el) {
el.observe("change", updateGroupedPrice);
});
Event.observe(window, "load", updateGroupedPrice);
function updateGroupedPrice() {
//do stuff
}
})();
I think this is using prototype.js but I tried to isolate it in a codepen but couldn't get it working.
I have tried to trigger the change event like so:
jQuery("input.qty").trigger("change")
But it does not work. I also ran through a load of other events but in the dev tools it shows the code listening on "change".
Does anyone know why I can't trigger the change?
Since the page is using Prototype.js, you ought to keep using that to trigger your change. If you introduce jQuery into this, you're a) loading another complete duplicate of what Prototype already does, and b) asking for a lot of trouble isolating the fact that $() is a method in both libraries.
Your jQuery is a little fishy to me, too. You're setting the value of one picker (I imagine) and yet you are addressing it with a classname, so potentially there is more than one select.qty in the page, and all of them will change to value 10, firing off (potentially) multiple callback functions.
The Prototype code you see here is setting up a "listener" for changes on what you would address in jQuery as$(#super-product-list input.qty) inputs.
jQuery always treats $() as returning an array of objects, and thus all of its methods act on the array, even if it only contains one member. Prototype has two different methods for accessing elements in the DOM: $('some_id'), which always returns one element (or none, if no match), and $$('some css selector'), which always returns an array (of zero or more matching elements). You would write (or use native) callback methods differently, depending on which accessor you used to gather the element(s).
If you want to change one of these inputs, you will need to isolate it before you set its value.
Let's say there are three select pickers with the classname qty in your #super-product-list element. You want to change the third one to 10:
$('super-product-list').select('input.qty').last().setValue('10');
Or, much smarter than this, you add an ID to the third one, and then your code is much shorter:
$('quantity_3').setValue('10');
In either case, this will send the "change" event from your select, and the updateGroupedPrice method will observe that and do whatever you have coded it to do.
You won't need to (and should not ever) trigger the change event -- that's a "native" event, and the browser owns it. jQuery's trigger() (which is fire() in Prototype, is used exclusively for "synthetic events", like you see in Bootstrap: show.bs.modal, hide.bs.modal, etc. You can spot these by the punctuation in their names; usually dots or colons to namespace the events and avoid collisions with other code.
Finally, if you really, really, really wanted to change every single #super-product-list select.qty element on the whole page to '10', you would do this in Prototype.js:
$$('#super-product-list select.qty').invoke('setValue', 10);

Polymer refresh the property's value which is calculated by a method

We are currently working on a project, in which we use a combination of AngularJS and Polymer.
We have some structure, but what's really important is this piece of code:
<template is="dom-bind" angupoly="{dataContainer:'dataContainer'}">
<menu-list for="places" data="{{dataContainer.getSomeData()}}">
</template>
We have a variable defined on $scope named dataContainer, which is being set in a controller. The problem is that this code is executed before the controller prepares that property, so it's undefined - it throws:
[dom-bind::_annotatedComputationEffect]: compute method dataContainer.getSomeData() not defined
And the data are never refreshed again and it does not work. On the contrary, with a property it works (it does not matter if it's first state is undefined), it is refreshed.
Because it's a really important point in our application, we wanna ask. How to reach required behaviour?
Thanks, have a nice day! :)
I am not familiar with polymer and if there are possibilties to delay the execution of the polymer code or if there a digest cycles one could work with like in AngularJS.
But I would guess you could avoid this kind of race condition with a simple ng-if= in conjunction with <ng-include> on AngularJS side - as it does not add the elements to the DOM, thus avoid any kind of interaction with polymer until it is included.
So e.g.:
<figure ng-if="dataContainer.getSomeData">
<ng-include src="'template.html'">
</figure>
And within template.html your (unmodified) code:
<template is="dom-bind" angupoly="{dataContainer:'dataContainer'}">
<menu-list for="places" data="{{dataContainer.getSomeData()}}">
</template>
I would be happy if this helps you - but as I said it is just a guess and I don't know much about polymer and how it interacts with the DOM.
The error that you are seeing is not caused by dataContainer being undefined, but the function that you call on it. Polymer does not support this type of function calls in data-binding. From the docs:
Data binding binds a property or sub-property of a custom element (the host element) to a property or attribute of an element in its local DOM (the child or target element).
You are calling a function on a property, which does not work.
If you want to call a function, you could to this with computed bindings.
<menu-list for="places" data="{{getData(dataContainer.*)}}">
However, this assumes that your menu-list is placed in some parent Polymer element and I'm not sure if this is the case here as you use a dom-bind. However, if you do have a parent element you could then add the computed function there.
Polymer({
is: 'parent-element',
properties: {dataContainer: ....},
getData: function() {
return dataContainer.getSomeData();
}
});
getData will be called anytime dataContainer or its sub-properties change.

Getting an Element in AngularJS

It seems that getting an element in AngularJS is a bad idea, i.e. doing something like:
$('.myElement')
in say, a controller is not an angular way of doing things.
Now my question is, how should I get something in angular?
Right now, what I'm doing (and is an accepted way of doing it) is by watching a variable, and my directive does something based on it.
scope.$watch('varToWatch', function (varToWatch) {
if(attrs.id == varToWatch)
{
//Run my Directive specific code
}
});
However, while this particular design works for most cases, watch is an expensive operation, and having lots of directives watching can really slow down your application.
TL:DR - What is an angular way of getting a directive based on a variable on the directive? (like the one above)?
If you want to get/set values you don't need to fetch the element using jQuery. Angular data binding is the way to do it.
directives is the way to go if you want to do animations or any kind of element attributes and DOM manipulation.
Your code is basically right; the directive should watch something in the $scope and perform it's logic when that thing changes. Yes, watch statements are expensive, and that is a problem once your number of watches start to approach ~2000.
Looking at your code though, I see one problem:
The variable $scope.varToWatch references an id in the template.
When this variable changes, you want something to happen to the element which has this id.
The problem here is in the first point: The controller should know nothing about the DOM, including the id of any element. You should find another way to handle this, for example:
<div my-directive="one"> ... </div>
<div my-directive="two"> ... </div>
<div my-directive="three"> ... </div>
...etc
And in your directive:
scope.$watch('varToWatch', function (varToWatch) {
if(attrs.myDirective == varToWatch)
{
// Run my Directive specific code
}
});
You are very vague as to what you're trying to achieve, but I'll try to answer in context of your last comment.
I have a lot of the same directives (therefore the code will run on all of them), but I need to get only one directive from the lot.
You talk a lot about getting the right element. The directive element is passed to the link function in the directive. If you are not using this element (or children of it) directly, but rather trying to search for the element you want somehow, you are most likely approaching the problem the wrong way.
There are several ways to solve this, I'm sure. If you're thinking about animations, there is already support for that in Angular, so please don't try reinvent the wheel yourself. For other logic, here are two suggestions:
Secondary directive
If the logic you want to apply to this directive is generic, i.e. it could be applied to other directives in your application, you could create a new directive which works together with directives. You can set prioritization in directive in order to control which directive is executed first.
<main-directive ... helper-directive="{{condition_for_applying_logic}}"></main-directive>
jsFiddle example
Expanding main directive
If the logic is tightly coupled to this directive, you can just create a new attribute, either dynamic or static, and bind to it in the directive. Instead of checking 'attrs.id == varToWatch', you check if $scope.apply-logic === 'true' and apply the logic then.
<main-directive ...></main-directive> <!-- Not applied here -->
<main-directive apply-logic="true" ...></main-directive> <!-- Applied here -->
<main-directive apply-logic="{{some.varOnScope}}"...></main-directive> <!-- Conditional -->
Please comment if something is unclear.

Categories

Resources