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

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.

Related

How to know when iron-list is finished creating html

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

What is initialised first - controller or partial/html?

Hi please explain reason for following three scenarios as I am unable to know why is this happening -
1)
<div ng-controller="myctrl">
<p>something for DOM manipulation</p>
</div>
2)in route I write
('someroute',{
templateUrl : "mytemplate",
controller : "myctrl"
});
mytemplate:
<div>
<p>something for dom manipulation</p>
</div>
3)
<div ng-include="mytemplate" ng-controller="myctrl"></div>
with template being same as above
The controllers in all the above scenarios are same, and in all of them I am just trying to select p tag of DOM by writing angular.element('p'). But this seems inconsistent. It works very well in 2nd scenario, it never works in 3rd scenario and I am not sure about 1st sccenario. Can someone explain which method is best for dom selection/manipulation, as I have to add a class to this 'p' tag on hover.
I am not understanding which gets initialized first- controller or partial?
Manipulating DOM inside controllers is discouraged. Quote from Best Practice - Dom Manipulations:
Dom Manipulations should not exist in controllers, services or anywhere else but in directives.
If you only need to style elements on hover, using the p:hover CSS selector would be enough without touching the DOM. ng-class and ng-mouseover can help you if you really want the class.
For more complex scenarios, you may want to write your own directive. You can check the article above for a guide to do that.
Load order from the first case: HTML first. Directives like ngController are loaded after parsing the HTML. Therefore the HTML already exists when the controller is loaded.
Load order for the second case: I'm not sure about it. You may check documentation for ngRoute or uiRouter depending on the router you are using.
Execution order for the third case: Controller first. The directive ngController have higher priority than the ngInclude directive. Therefore, the controller is loaded first.
Quote from ngController documentation :
This directive executes at priority level 500.
Quote from ngInclude documentation :
This directive executes at priority level 400.

Angular custom directive - two way binding which always sets attribute to true or false

I'm creating a custom Angular directive for a slide in menu which needs to watch a couple of attributes and one of those attributes needs to be two way bound to the main controller scope (sometimes). However, sometimes the attribute will not be added by the developer so it needs to be added automatically and set to the default (false). So, the directive can be used like this.
<slide-menu position="right" is-open="menuIsOpen"></slide-menu>
or like this:
<slide-menu></slide-menu>
When used the first way the main controller will be able to open and close the menu by changing the value of the boolean $scope.menuIsOpen.
When used without supplying the is-open attribute it should default to false and is obviously used internally and by a child toggle directive.
An additional complication is that whether the attribute is supplied by the developer or not it should exist in the DOM. so in the second example above the directive would set itself to false by default and add the attribute is-open="false" to the DOM?
The reason for requiring is-open="false/true" in the DOM at all times is that the menu is actually operated using CSS tansitions which use the following selector:
slide-menu[is-active="true"]{
// Slide the menu in using transforms/transitions
}
There is a jsfiddle here which shows how far I have got.
http://jsfiddle.net/jonhobbs/gEPvE/
Obviously it doesn't work, but it shows how I have tried to set a default and how I have tried to use # and & on the isolated scope for a one time binding (the menu position) and a 2 way bound expression for the is-open variable.
I'm clearly a long way from achieving what I need but any advice would really be appreciated.
Have a look at this fiddle http://jsfiddle.net/gEPvE/38/
I took the one you started and updated it to act like you specified.
You can make a two way binding value optional by adding a ? on the scope definition.
Like this
{
scope: {
'isOpen':'=?'
}
}
Now the is-open attribute is optional.
Then you can set the default value in the directive controller, like you had started to do.
Next, in order to synchronize the DOM attribute with the scope value you can use $watch.
$scope.$watch('isOpen', function(val) {
$element.attr('is-open', val);
});
Finally, I changed the second 'slideMenuToggle' directive to wrap/transclude its element in order to add an ng-click handler. This is mainly to avoid any nastiness with calling $scope.$apply yourself.
Let me know if that works for you.
EDIT
Answering your question in the comment, you can pass a value directly without having it be bound to the scope, you just need to wrap the value in quotes.
For example
<div ng-controller='ctrl'>
<hello world='imOnScope'></hello>
</div>
Assuming 'hello' is a directive with a scope of 'world': '=?' then angular will assign a reference to the parent scope's 'imOnScope' object to the directive's $scope.world member, allowing a two way binding scenario.
To just provide a value directly you may do something like this
<div ng-controller="ctrl">
<hello world="'directValue'"></hello>
</div>
In this scenario angular will just assign 'directValue' to the directive's $scope.world member.
You need to add ngTouch to your module.
var app = angular.module('app', ['ngTouch']);
And add this script:
http://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-touch.js
The reason for requiring is-open="false/true" in the DOM at all times
is that the menu is actually operated using CSS tansitions which use
the following selector
Forcing directive attributes to be appropriate for css selectors is terrible idea. As you correctly stated, they are for developers. So add a class to the element dynamically.
It seems that you're misusing &, it would be ok to set up a callback, but since you don't do this, in its current state you can end up with one-way # with confidence.
I guess it can be something like this (just added ngTouch and ng-controller for parent scope).
You could replace
$scope.watch('isOpen', function () {
$element.toggleClass('opened', $scope.isOpen);
});
with
$scope.watch('isOpen', function () {
$attrs.isOpen = !!$scope.isOpen;
});
and get the behaviour you're asking for, easy as that. Ok, it is boolean now, and it reflects the scope, and you can use [is-open=true] selector. But guess what will happen with your binding? Broken. Fortunately, you can do
$scope.watch('isOpen', function () {
$element.attr('is-open', !!$scope.isOpen);
});
instead. Voila, we tricked Angular because it doesn't look after jqlite. But what will will happen with the binding when the directive will be re-compiled for any reason? Again, isOpen's binding is non-existing 'true' or 'false' scope variable. Broken.

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.

Ember.js / Handlebars: views rendered using {{view}} helper not binding attributes

I have a mysterious issue with Ember/Handlebars views. I am attempting to implement very simple data binding in a Handlebars view. This works correctly when render my view by doing:
Ember.Views.NavView.create().append()
But the bound attribute isn't shown when I attempt to render another instance of the same view using the {{view}} helper, like so:
<script type="text/x-handlebars">
{{view App.Views.NavView}}
</script>
In the first case the attributes (hardcoded on the View for this test-case) are shown correctly. In the second case I get "metamorph-0-start" and "metamorph-0-end" tags, but the value itself is not rendered.
I have set up a JSFiddle (http://jsfiddle.net/XUyht/2/) that illustrates the problem clearly.
You'll see that I have rendered the view twice: the first using the {{view}} helper and the second using append() - but the attribute "working" only shows in the latter case.
What's going on here?
I don't know why you need this kind of implementation of the template, but anyway, since the 1.0-pre, the default context of a view is either its controller, either its parent view. So in your case, if you replace tmp.foo with view.tmp.foo, this is working.
see http://jsfiddle.net/Sly7/amLfk/

Categories

Resources