Ember ArrayController binding to view - arrays

I've snipped out the init function which sets up the initials array.
This is an array of arays indexed as "A", "B", "C" etc.
Each of these contains station object that begin with that letter.
I have buttons that fire off setByInitial which copy the relevant initial array into content.
this.content.setObjects(this.initials[initial])
works fine and my view updates, but is horribly slow (150ms +) station objects are pertty big and there are over 3500 of them...
this.set("content",Ember.copy(this.initials[initial],true))
Is much fatser (around 3ms) updated the content aray (as can be seen with some logging to console), but does not cause the view to update.
this.set("content",this.initials[initial])
is even faster, but also does not update the view.
I've tried using arrayContentDidChange() etc. but can't get that to work either.
How do I inform the view that this dfata has changed if I use the faster method? Or is there another wau to do this?
App.StationListController = Ember.ObjectController.extend({
content : [],
initials : [],
setByInitial : function(initial)
{
// this.content.setObjects(this.initials[initial])
this.set("content",Ember.copy(this.initials[initial],true))
}
});
<script type="text/x-handlebars" id="stationList">
<ul>
{{#each content}}
<li>{{#linkTo "station" u}}{{n}}{{/linkTo}}</li>
{{/each}}
</ul>
</script>
Thanks to #mike-grassotti example I can see that what I was doing ought to work, but it still doesn't! As is often the case, what I have posted here is a simplification. My real app is not so straight forward...
My index template contain several views. Each view has it's own data and controller. So it seems it's something in that complexity which is breaking it. So, I've started with Mike's example and added just a little - in order to move towards what I really want - and promptly broken it!
I now have:
var App
= Ember.Application.create({})
App.Router.map(function() {
this.resource('index', {path: '/'});
this.resource('station', {path: '/:code/:name'});
this.resource('toc', {path: '/toc/:code/:name'});
});
App.Station = Ember.Object.extend({});
App.IndexController = Ember.ObjectController.extend({
needs : ["StationList"],
listStationsByInitial: function(initial)
{
this.get("controllers.StationList").listByInitial(initial)
}
});
App.StationListView = Em.View.extend({
stationsBinding : 'App.StationListController',
init : function()
{
console.log("view created",this.stations)
}
});
App.StationListController = Ember.ArrayController.extend({
content : [],
initials : [[{u:1,n:'Abe'},{u:2,n:'Ace'}],[{u:3, n:'Barb'},{u:4,n:'Bob'}],[{u:5,n:'Card'},{u:6,n:'Crud'}]],
init : function()
{
this.set("content",this.initials[0])
},
listByInitial : function(initial)
{
this.set("content",this.initials[initial])
console.log('StationListController',this.content);
}
});
and
t type="text/x-handlebars" data-template-name="application">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="index">
<button {{action listStationsByInitial '0'}}>A</button>
<button {{action listStationsByInitial '1'}}>B</button>
{{#view App.StationListView controllerBinding="App.StationListController"}}
<ul>
{{#each stations}}
<li>{{#linkTo "station" u}}{{n}}{{/linkTo}}</li>
{{else}}
<li>No matching stations</li>
{{/each}}
</ul>
<button {{action listByInitial '2'}}>C</button>
{{/view}}}
</script>
Firstly, I no longer see the list of station rendered. Neither initially, nor on the click of a button.
I expected {{#with content}} to get the data from App.StationListController.content, but that didn't work. So, I created App.StationListView with a binding stationsBinding to that controller. Still no joy...
What am I doing wrong here?
Secondly, my function listStationsByInitial is called when I click button A or B. So I'd expect listByInitial (in StationListController) to be called when I click button C (since it's inside of the view where I've said to use StationListController). But instead I get an error:
error: assertion failed: The action 'listByInitial' did not exist on App.StationListController
Why doesn't that work?
I'm doubly frustrated here because I have already build a pretty large and complex Ember app (http://rail.dev.hazardousfrog.com/train-tickets) using 1.0.pre version and am now trying to bring my konwledge up-to-date with the latest version and finding that almost nothing I learned applies any more!

How do I inform the view that this dfata has changed if I use the faster method?
You should not have to inform the view, this is taken care of via bindings. I can't see anything in your example that would prevent bindings from updating automatically, and made a simple jsFiddle to demonstrate. Given the following, the list of stations is modified when user hits one of the buttons and view updates automatically:
App = Ember.Application.create({});
App.Router.map( function() {
this.route('stationList', {path: '/'});
this.route('station', {path: '/station/:station_id'});
});
App.Station = Ember.Object.extend({});
App.StationListController = Ember.ObjectController.extend({
content : [],
initials : [
[{u:1,n:'Abe'}, {u:2,n:'Ace'}], [{u:1,n:'Barb'}, {u:2,n:'Bob'}]
],
setByInitial : function(initial)
{
console.log('setByInitial', initial);
this.set("content",this.initials[initial])
}
});
<script type="text/x-handlebars" id="stationList">
<h2>Stations:</h2>
<button {{action setByInitial '0'}}>A</button>
<button {{action setByInitial '1'}}>B</button>
<ul>
{{#each content}}
<li>{{#linkTo "station" u}}{{n}}{{/linkTo}}</li>
{{/each}}
</ul>
</script>
See: http://jsfiddle.net/mgrassotti/7f4w7/1/

Related

AngularJS ng-repeat update does not apply when object keys stay the same?

I'm trying to make a minimal but fancy AngularJS tutorial example, and I am running into an issue where after updating the entire tree for a model (inside the scope of an ng-change update), a template that is driven by a top-level ng-repeat is not re-rendered at all.
However, if I add the code $scope.data = {} at a strategic place, it starts working; but then the display flashes instead of being nice and smooth. And it's not a great example of how AngularJS automatic data binding works.
What am I missing; and what would be the right fix?
Exact code - select a country from the dropdown -
This jsFiddle does not work: http://jsfiddle.net/f9zxt36g/
This jsFiddle works but flickers: http://jsfiddle.net/y090my10/
var app = angular.module('factbook', []);
app.controller('loadfact', function($scope, $http) {
$scope.country = 'europe/uk';
$scope.safe = function safe(name) { // Makes a safe CSS class name
return name.replace(/[_\W]+/g, '_').toLowerCase();
};
$scope.trunc = function trunc(text) { // Truncates text to 500 chars
return (text.length < 500) ? text : text.substr(0, 500) + "...";
};
$scope.update = function() { // Handles country selection
// $scope.data = {}; // uncomment to force rednering; an angular bug?
$http.get('https://rawgit.com/opendatajson/factbook.json/master/' +
$scope.country + '.json').then(function(response) {
$scope.data = response.data;
});
};
$scope.countries = [
{id: 'europe/uk', name: 'UK'},
{id: 'africa/eg', name: 'Egypt'},
{id: 'east-n-southeast-asia/ch', name: 'China'}
];
$scope.update();
});
The template is driven by ng-repeat:
<div ng-app="factbook" ng-controller="loadfact">
<select ng-model="country" ng-change="update()"
ng-options="item.id as item.name for item in countries">
</select>
<div ng-repeat="(heading, section) in data"
ng-init="depth = 1"
ng-include="'recurse.template'"></div>
<!-- A template for nested sections with heading and body parts -->
<script type="text/ng-template" id="recurse.template">
<div ng-if="section.text"
class="level{{depth}} section fact ng-class:safe(heading);">
<div class="level{{depth}} heading factname">{{heading}}</div>
<div class="level{{depth}} body factvalue">{{trunc(section.text)}}</div>
</div>
<div ng-if="!section.text"
class="level{{depth}} section ng-class:safe(heading);">
<div class="level{{depth}} heading">{{heading}}</div>
<div ng-repeat="(heading, body) in section"
ng-init="depth = depth+1; section = body;"
ng-include="'recurse.template'"
class="level{{depth-1}} body"></div>
</div>
</script>
</div>
What am I missing?
You changed reference of section property by executing section = body; inside of ng-if directives $scope. What happened in details (https://docs.angularjs.org/api/ng/directive/ngIf):
ng-repeat on data created $scope for ng-repeat with properties heading and section;
Template from ng-include $compile'd with $scope from 1st step;
According to documentation ng-if created own $scope using inheritance and duplicated heading and section;
ng-repeat inside of template executed section = body and changed reference to which will point section property inside ngIf.$scope;
As section is inherited property, you directed are displaying section property from another $scope, different from initial $scope of parent of ngIf.
This is easily traced - just add:
...
<script type="text/ng-template" id="recurse.template">
{{section.Background.text}}
...
and you will notice that section.Background.text actually appoints to proper value and changed accordingly while section.text under ngIf.$scope is not changed ever.
Whatever you update $scope.data reference, ng-if does not cares as it's own section still referencing to previous object that was not cleared by garbage collector.
Reccomdendation:
Do not use recursion in templates. Serialize your response and create flat object that will be displayed without need of recursion. As your template desired to display static titles and dynamic texts. That's why you have lagging rendering - you did not used one-way-binding for such static things like section titles. Some performance tips.
P.S. Just do recursion not in template but at business logic place when you manage your data. ECMAScript is very sensitive to references and best practice is to keep templates simple - no assignments, no mutating, no business logic in templates. Also Angular goes wild with $watcher's when you updating every of your section so many times without end.
Thanks to Apperion and anoop for their analysis. I have narrowed down the problem, and the upshot is that there seems to be a buggy interaction between ng-repeat and ng-init which prevents updates from being applied when a repeated variable is copied in ng-init. Here is a minimized example that shows the problem without using any recursion or includes or shadowing. https://jsfiddle.net/7sqk02m6/
<div ng-app="app" ng-controller="c">
<select ng-model="choice" ng-change="update()">
<option value="">Choose X or Y</option>
<option value="X">X</option>
<option value="Y">Y</option>
</select>
<div ng-repeat="(key, val) in data" ng-init="copy = val">
<span>{{key}}:</span> <span>val is {{val}}</span> <span>copy is {{copy}}</span>
</div>
</div>
The controller code just switches the data between "X" and "Y" and empty versions:
var app = angular.module('app', []);
app.controller('c', function($scope) {
$scope.choice = '';
$scope.update = function() {
$scope.data = {
X: { first: 'X1', second: 'X2' },
Y: { first: 'Y1', second: 'Y2' },
"": {}
}[$scope.choice];
};
$scope.update();
});
Notice that {{copy}} and {{val}} should behave the same inside the loop, because copy is just a copy of val. They are just strings like 'X1'. And indeed, the first time you select 'X', it works great - the copies are made, they follow the looping variable and change values through the loop. The val and the copy are the same.
first: val is X1 copy is X1
second: val is X2 copy is X2
But when you update to the 'Y' version of the data, the {{val}} variables update to the Y version but the {{copy}} values do not update: they stay as X versions.
first: val is Y1 copy is X1
second: val is Y2 copy is X2
Similarly, if you clear everything and start with 'Y', then update to 'X', the copies get stuck as the Y versions.
The upshot is: ng-init seems to fail to set up watchers correctly somehow when looped variables are copied in this situation. I could not follow Angular internals well enough to understand where the bug is. But avoiding ng-init solves the problem. A version of the original example that works well with no flicker is here: http://jsfiddle.net/cjtuyw5q/
If you want to control what keys are being tracked by ng-repeat you can use a trackby statement: https://docs.angularjs.org/api/ng/directive/ngRepeat
<div ng-repeat="model in collection track by model.id">
{{model.name}}
</div>
modifying other properties won't fire the refresh, which can be very positive for performance, or painful if you do a search/filter across all the properties of an object.

Changing the template data not refreshing the elements

I have searched and tried suggestions mentioned in various posts but no luck so far.
Here is my issue.
I have created a custom element <month-view id="month-view-element"></month-view> in my mainpage.html. Inside mainpage.html when this page is initially loaded i created a empty json object for all the 30days of a month and print a placeholder type cards in UI. Using the code below.
var json = [];
for(var x = 0; x < total; x++) {
json.push({'hours': 0, 'day': x+1, 'year': year});
}
monthView.month = json; //Doing this line. Prints out the desired empty cards for me in the UI.
created a month-view.html something like below:
<dom-module id='month-view'>
<template>
<template is="dom-repeat" items= "{{month}}">
<paper-card class="day-paper-card" heading={{item.day}}>
<div class="card-content work">{{item.work}}</div>
<div class="card-actions containerDay layout horizontal">
<div style="display:inline-block" class="icon">
<paper-icon-button icon="icons:done" data-hours = "8" data-day$="{{item.day}}" data-month$={{item.month}} data-year$={{item.year}} on-click="updateWorkHours"></paper-icon-button>
<paper-tooltip>Full day</paper-tooltip>
</div>
</div>
</paper-card>
</template>
</template>
<script>
Polymer({
is: "month-view",
updateWorkHours: function (e, detail) {
console.log(e);
this.fire('updateWorkHour', {day: e.target.dataHost.dataset.day,
month: e.target.dataHost.dataset.month,
year: e.target.dataHost.dataset.year,
hours: e.target.dataHost.dataset.work
});
}
});
</script>
</dom-module>
There is another file script.js which contains the function document.addEventListener('updateWorkHour', function (e) { // doStuff });. I use this function to make a call to a google client API. I created a client request and then do request.execute(handleCallback);
Once this call is passed i landed in handleCallback function. In this function i do some processing of the response data and save parts of data into json variable available in the file already. And once all processing is done i did something like below.
monthView.month = json;
But this above line is not refreshing my UI with the latest data. Is there anything I am missing? Any suggestions or anything i am doing incorrectly.
You need to use 'set' or 'notifyPath' while changing Polymer Object or Arrays in javascript for the databinding/obserers to work. You can read more about it in https://www.polymer-project.org/1.0/docs/devguide/data-binding.html#path-binding
In your case try below code
monthView.set('month',json);
Updated suggestions:
Wrap your script on main page with. This is required for non-chrome browsers.
addEventListener('WebComponentsReady', function() {})
This could be scoping issue. Try executing 'document.querySelector('#month-view-element');' inside your callback addWorkHoursCallBack. Also, Use .notifyPath instead of .set.

Exception error when I try to iterate over an array

I have a collection, Ages, which only holds one key: ages.
In it I store an array of strings like so:
['51', '24', '21', '19', '15']
I struggled a bit with how to iterate over it in the template but this is what I found worked. First the HTML:
<template name="ageFilter">
{{#each age}}
<li>
{{this}}
</li>
{{/each}}
</template>
And the helper:
Template.ageFilter.helpers({
age: function() {
return Ages.findOne().ages
}
})
This is a rather ugly solution that rests on my having only one object in my database (since I use findOne() but it's not a big problem, and it works. The template iterates over the array and outputs it.
There's only one problem: the browser console throws an error!
Exception in template helper: TypeError: Cannot read property 'ages' of undefined
Why is this and how can I get rid of it?
Maybe your age helper is first called before subscription to your Ages publication occurs, therefore the first call(s) to Ages.findOne() end(s) up returning nothing. You could check if your Ages document has been fetched correctly:
Template.ageFilter.helpers({
age: function() {
var ageItem = Ages.findOne();
return (ageItem ? ageItem.ages : null);
}
});
But that's just hacking around the issue. Best approach would be to make sure your template does not get rendered before subscription is done. You could use iron-router and put your subscriptions in waitOn for that. For example:
Router.route('home', {
path: '/',
waitOn: function() {
return Meteor.subscribe('ages');
}
});
Also, overall you might want to change your design and just put one age value per document in your Ages collection, stored in a key like value or something. It seems more logical. This way you could do:
<template name="ageFilter">
{{#each ages}}
<li>
{{value}}
</li>
{{/each}}
</template>
And your helper:
Template.ageFilter.helpers({
ages: function() {
return Ages.find().fetch();
}
});

Observing nested properties in Ember controller

I'm in the middle of a small project involving Ember. It's my very first time at working with this framework and it has not been an easy learning so far :(
Right now I'm having troubles dealing with nested arrays. What I want to do is pretty standard (at least it seems that way): I have items, item categories and category types (just a way to organize them).
The idea is that there are checkboxes (categories) that allow me to filter the items that are shown in the webpage. On the other hand, there are checkboxes (types) that allow me to check multiple catgories at a time.
In order to implement this I've defined a route (in which I retrieve all the data from these models) and a controller. Originally, I only had items and categories. In this context, I observe the changes in the filters (categories) like this: categories.#each.isChecked and then show the item selection. Unfortunately, now that the hierarchy is types->categories, is not possible to observe changes in categories in the same manner according to the docs:
Note that #each only works one level deep. You cannot use nested forms like todos.#each.owner.name or todos.#each.owner.#each.name.
I google a little bit but didn't find too much about it, so I right now I was thinking in using a custom view for categories (one that extends the Ember.Checkbox) and send an event to the controller whenever a category is checked or unchecked. Is more of a "manual" work and I guess is far from Ember's way of dealing with this type of things.
Is there a standard way of doing this?
Thanks in advance for any help.
One way of solving this would be to observe the category types and filter categories, the same way that the categories are being observed.
This is an example,
http://emberjs.jsbin.com/naqebijebapa/1/edit
(one to many relationships have been assumed)
hbs
<script type="text/x-handlebars" data-template-name="index">
<ul>
{{#each catType in catTypes}}
<li>{{input type="checkbox" checked=catType.isSelected }}{{catType.id}} - {{catType.name}}</li>
{{/each}}
</ul>
<hr/>
<ul>
{{#each cat in filteredCats}}
<li>{{input type="checkbox" checked=cat.isSelected }}{{cat.id}} - {{cat.name}}</li>
{{/each}}
</ul>
<hr/>
<ul>
{{#each item in filteredItems}}
<li>{{item.id}} - {{item.name}}</li>
{{/each}}
</ul>
</script>
js
App = Ember.Application.create();
App.Router.map(function() {
// put your routes here
});
App.CategoryType = Em.Object.extend({
id:null,
name:null,
isSelected:true
});
App.Category = Em.Object.extend({
id:null,
name:null,
type:null,
isSelected:false
});
var catTypeData = [
App.CategoryType.create({id:1,name:"type1"}),
App.CategoryType.create({id:2,name:"type2"}),
App.CategoryType.create({id:3,name:"type3"}),
App.CategoryType.create({id:4,name:"type4"})
];
var catData = [
App.Category.create({id:1,name:"cat1",type:catTypeData[1]}),
App.Category.create({id:2,name:"cat2",type:catTypeData[2]}),
App.Category.create({id:3,name:"cat3",type:catTypeData[0]})
];
var itemsData = [
{id:1,name:"item1",cat:catData[0]},
{id:2,name:"item2",cat:catData[0]},
{id:3,name:"item3",cat:catData[1]}
];
App.IndexRoute = Ember.Route.extend({
model: function() {
return Em.RSVP.hash({catTypes:catTypeData,cats:catData,items:itemsData});
}
});
App.IndexController = Ember.ObjectController.extend({
filteredItems:[],
filterItemsBasedOnCategory:function(){
var selectedCats = this.get("cats").filterBy("isSelected");
if(!Em.isEmpty(selectedCats))
this.set("filteredItems",this.get("items").filter(function(item){
return selectedCats.contains(item.cat);}));
else
this.set("filteredItems",[]);
}.observes("cats.#each.isSelected"),
filterCatsBasedOnCategoryType:function(){
var selectedCatTypes = this.get("catTypes").filterBy("isSelected");
if(!Em.isEmpty(selectedCatTypes))
this.set("filteredCats",this.get("cats").filter(function(cat){
var itContainsIt = selectedCatTypes.contains(cat.type);
if(!itContainsIt){
cat.set("isSelected",false);
}
return itContainsIt;
}));
else
this.set("filteredCats",[]);
}.observes("catTypes.#each.isSelected")
});

How to get a single item from a GoInstant collection?

How do you get a single item from a GoInstant GoAngular collection? I am trying to create a typical show or edit screen for a single task, but I cannot get any of the task's data to appear.
Here is my AngularJS controller:
.controller('TaskCtrl', function($scope, $stateParams, $goKey) {
$scope.tasks = $goKey('tasks').$sync();
$scope.tasks.$on('ready', function() {
$scope.task = $scope.tasks.$key($stateParams.taskId);
//$scope.task = $scope.tasks.$key('id-146b1c09a84-000-0'); //I tried this too
});
});
And here is the corresponding AngularJS template:
<div class="card">
<ul class="table-view">
<li class="table-view-cell"><h4>{{ task.name }}</h4></li>
</ul>
</div>
Nothing is rendered with {{ task.name }} or by referencing any of the task's properties. Any help will be greatly appreciated.
You might handle these tasks: (a) retrieving a single item from a collection, and (b) responding to a users direction to change application state differently.
Keep in mind, that a GoAngular model (returned by $sync()) is an object, which in the case of a collection of todos might look something like this:
{
"id-146ce1c6c9e-000-0": { "description": "Destroy the Death Start" },
"id-146ce1c6c9e-000-0": { "description": "Defeat the Emperor" }
}
It will of course, have a number of methods too, those can be easily stripped using the $omit method.
If we wanted to retrieve a single item from a collection that had already been synced, we might do it like this (plunkr):
$scope.todos.$sync();
$scope.todos.$on('ready', function() {
var firstKey = (function (obj) {
for (var firstKey in obj) return firstKey;
})($scope.todos.$omit());
$scope.firstTodo = $scope.todos[firstKey].description;
});
In this example, we synchronize the collection, and once it's ready retrieve the key for the first item in the collection, and assign a reference to that item to $scope.firstTodo.
If we are responding to a users input, we'll need the ID to be passed from the view based on a user's interaction, back to the controller. First we'll update our view:
<li ng-repeat="(id, todo) in todos">
{{ todo.description }}
</li>
Now we know which todo the user want's us to modify, we describe that behavior in our controller:
$scope.todos.$sync();
$scope.whichTask = function(todoId) {
console.log('this one:', $scope.todos[todoId]);
// Remove for fun
$scope.todos.$key(todoId).$remove();
}
Here's a working example: plunkr. Hope this helps :)

Resources