Array deep watch in angular view (html) - angularjs

This is my view (html):
<my-directive collection="currentData"></my-directive>
and this is the data structure:
$scope.currentData = [...
{
"name": "lala Page",
"icon": "icon-docs",
"sub_data": [
{
"name": "1-article",
"href": "/1-article",
"icon": "fa fa-pencil"
},
{
"name": "2-article",
"href": "/2-article",
"icon": "fa fa-pencil"
},
{
"name": "3-article",
"href": "/3-article",
"icon": "fa fa-pencil"
}
...
]...
}]
Inside my-directive there are bind-once elements (on sub_data).
If the all array change - the view is changed,
but when I change the sub_data, the view don't updated.
Any idea, how to make the collection be affected by a changes in sub_data?
(I do want to use as less watchers as possible)
Edit
Adding the my-directive code:
angular.module('my_module',[]).directive('myDirective', function(){
return {
scope:{
collection: '='
},
replace: true,
template: ['<li my-directive-item ng-repeat="item in collection" raw-model="{{::item}}">',
'</li>'].join('')
};
});
I don't have a watch on collection, only the code above. I meant angular doesn't update the collection binding unless I change the array itself, and I want it to update the view when i change a sub property of an item in the collection.

Ok, I solved it. I'm not proud of the solution but it works:
Inside the controller:
$scope.updateArray = function() {
...
// Do stuff with tempData
...
// Trick for updating the view - because of the bind-once sub items
$scope.currentData = [];
$timeout(function(){
$scope.currentData = tempData;
}, 0);
};

Related

Using ng-if and ng-repeat correctly

I have a rudimentary understanding of AngularJS and have a couple (possibly stupid) questions about ng-if and ng-repeat.
If I have a ng-repeat that looks like this:
For all containers without titles, would the ng-repeat produce those containers and not show them? Or do they simply don't exist? The reason I ask is I've tried to replace ng-if with ng-show, but it does not produce the same outcome of "hiding" containers that do not have titles.
Is there a way to code a ng-if to say if the next container has no title, then do something. I tried something like ng-if="item.title=='' && $index+1", without any luck.
Any suggestions?
My data looks like this:
"_sections": [{
"_bootstrap_cells": 6,
"_count": 2,
"visible": true,
"columns": [{
"fields": [{
"name": "type_of_account",
"type": "field"
}, {
"name": "routing_transit_number",
"type": "field"
}]
}, {
"fields": [{
"name": "type_of_payment",
"type": "field"
}, {
"name": "check_digit",
"type": "field"
}]
}],
"caption": "Direct Deposit",
"id": "b456b9d2137ac340177c36328144b0ef"
}, {
"_bootstrap_cells": 12,
"_count": 1,
"visible": true,
"columns": [{
"fields": [{
"name": "account_number",
"type": "field"
}, {
"name": "account_title",
"type": "field"
}, {
"name": "financial_institution_name",
"type": "field"
}]
}],
"caption": "",
"id": ""
}
}]
The first section has two columns and a bootstrap cell value of 6 for each column, the second section only has one column and a bootstrap cell of 12. Since the second doesn't have a caption, I want it to be appended to the first section, but keep both sections' bootstrap formatting.
If containers is an array then it does not have a title property. You need to check the title on each item.
Also note that ng-if will not hide the content, but remove it or not insert it into the DOM. The counterpart of ng-show is ng-hide.
angular.module('appModule', [])
.controller('MyController', [function() {
this.containers = [{
aaa: 1,
title: 'One'
}, {
aaa: 2,
title: ''
}, {
aaa: 3,
title: 'Three'
}]
}]);
angular.bootstrap(window.document, ['appModule'], {
strictDi: true
});
<div ng-controller="MyController as myCtrl">
<div ng-repeat="item in myCtrl.containers track by $index">
<div ng-if="item.title!=''">{{$index}}. {{item.title}}</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.7/angular.min.js"></script>

Angular JS $scope $resource & Directive - Directive Loading faster than scope thus cant see data

I am using an API to load (Data) to my $scope resource, and I took an example from a directive online to create a treeview. Recursive Tree View Example
However I am changing a few things to load data from an API. Please note the commented data... when I uncomment my data everything works great, however when I use $scope.treeFamily = TreeView.query() I think there is a delay between the directive executing and me getting no data. Any insight will be helpful. Thank you!
var module = angular.module("module", ["ngResource", "ngRoute"]);
module.factory('TreeView', function ($resource) {
return $resource('/api/TreeView/:Id', {}, {
//show: { method: 'GET', isArray: true }, //<--- need to do query instead of show....
query: { method: 'GET', isArray: false},
update: { method: 'PUT', params: { id: '#id' } },
delete: { method: 'DELETE', params: { id: '#id' } }
})
});
module.controller('TreeCtrl', function ($scope, TreeView) {
$scope.treeFamily = TreeView.query();
//$scope.treeFamily = {
// name: "Parent",
// children: [{
// name: "Child1",
// children: [{
// name: "Grandchild1",
// children: []
// }, {
// name: "Grandchild2",
// children: []
// }, {
// name: "Grandchild3",
// children: []
// }]
// }, {
// name: "Child2",
// children: []
// }]
//};
});
module.factory('RecursionHelper', ['$compile', function ($compile) {
var RecursionHelper = {
compile: function (element) {
var contents = element.contents().remove();
var compiledContents;
return function (scope, element) {
if (!compiledContents) {
compiledContents = $compile(contents);
}
compiledContents(scope, function (clone) {
element.append(clone);
});
};
}
};
return RecursionHelper;
}]);
module.directive("tree", function (RecursionHelper) {
return {
restrict: "E",
scope: { family: '=' },
template:
'<p>{{ family.name }}</p>' +
'<ul>' +
'<li ng-repeat="child in family.children">' +
'<tree family="child"></tree>' +
'</li>' +
'</ul>',
compile: function (element) {
return RecursionHelper.compile(element);
}
};
});
The Result from what i get there using the following HTML.
<!DOCTYPE html>
<html lang="en" ng-app="module">
<head>
<title></title>
</head>
<body>
<div class="container">
<div ng-controller="TreeCtrl">
<table>
<tr>
<th>Name</th>
</tr>
<tr ng-repeat="result in treeFamily">
<td> From Table: {{result.name}}</td>
</tr>
</table>
<tree family="treeFamily"></tree>
</div>
<div ng-view=""></div>
</div>
Result :
Name
From Table: Parent
HOWEVER, this is from the the ng-repeat within my table, so i know the API is sending DATA and it is readable.
{
ID: "1",
type: "Folder",
name: "Parent",
children: []
}
The problem is that it seems that the directive is not loading this data.... If however uncomment the built in data I have for that scope it works fine...
I have a feeling that my directive is loading faster than my API call so I get no data. Am i doing something wrong?
Any help will be appreciated!
Additional Research...
$scope.treeFamily = { "ID": "1", "type": "Folder", "name": "Harvest", "children": null };
$scope.treeFamily = [{ "ID": "1", "type": "Folder", "name": "Harvest", "children": null }];
This is the difference.....
If i try to do ng-repeat on
$scope.treeFamily = { "ID": "1", "type": "Folder", "name": "Harvest", "children": null };
It will not work because it is expecting an object [...]
$scope.treeFamily = [{ "ID": "1", "type": "Folder", "name": "Harvest", "children": null }];
Thus the above will work.
However, when using the recursive tree, it seems as though it does not EXPECT to see an object other than children... thus
$scope.treeFamily = [{ "ID": "1", "type": "Folder", "name": "Harvest", "children": null }];
will fail......
HOWEVER, I changed my API to return like this:
{ "ID": "1", "type": "Folder", "name": "Harvest", "children": null }
It still wont work!!!!!
This is probably an Angular version issue. Automatic promise unwrapping was removed in version 1.2. Change the code to:
var treeFamily = TreeView.query(function(){
$scope.treeFamily = treeFamily;
});
or use the more explicit promise syntax:
TreeView.query().$promise.then(function(treeFamily){
$scope.treeFamily = treeFamily;
});
I don't think the order matters. Since in the documentation of $resource it says:
It is important to realize that invoking a $resource object method immediately returns an empty reference (object or array depending on isArray). Once the data is returned from the server the existing reference is populated with the actual data. This is a useful trick since usually the resource is assigned to a model which is then rendered by the view. Having an empty object results in no rendering, once the data arrives from the server then the object is populated with the data and the view automatically re-renders itself showing the new data. This means that in most cases one never has to write a callback function for the action methods.
Are you sure data is returned from the server?

Why is my $watch showing an array that appears to be empty?

I'm working on creating an AngularJS directive in order to use D3 to render a visualization, but I'm running into problems when it comes to setting a $watch. The majority of my stuff looks extremely similar to the AngularJS tutorial. My resources are configured in a file called resources.json, which I'm sure is returning correctly.
Here's the relevant code of what I have so far:
app.js
var resourceApp = angular.module("resourceApp", [
'ngRoute',
'resourceControllers',
'resourceDirectives',
'resourceServices'
]);
/* ... routing config ... */
controllers.js
var resourceControllers = angular.module('resourceControllers', []);
resourceControllers.controller("OverviewCtrl", [ '$scope', 'Resource',
function($scope, Resource) {
$scope.resources = Resource.query();
}
]);
/* ... other controllers ... */
directives.js
var resourceDirectives = angular.module('resourceDirectives', []);
resourceDirectives.directive("resourceVisualization", function() {
return {
restrict: 'E',
scope: {
resources: '='
},
link: function(scope, el, attrs) {
// svg setup is here
scope.$watch("resources", function(nRes, oRes) {
if (nRes) {
// this logs an array of Resource objects
//(once expanded in Firebug)
console.log(nRes);
var cats = nRes.map(function(r) { return r.category; });
// this logs an empty array
console.log(cats);
}
});
}
};
});
overview.html
<ul>
<li ng-repeat="resource in resources">
{{resource.name}}
</li>
</ul>
<resource-visualization resources="resources"></resource-visualization>
resources.json (which is what services.js is configured to pull from)
[
{
"id": 1,
"category": "test",
"type": "sample",
"name": "Test1"
},
{
"id": 2,
"category": "test",
"type": "sample4",
"name": "Test2"
},
{
"id": 3,
"category": "fake",
"type": "sample1",
"name": "Test3"
},
{
"id": 4,
"category": "new",
"type": "sample2",
"name": "Test4"
}]
Now, I know that the REST call is working, because the <ul> is populated. However, in the directive, the logging statements are returning empty arrays. I'm aware of the the async-ness of $resource, but the object that is logged first in the $watch contains $resolved: true.
Am I missing something?
It's all fine. Your call to Resource.query() returns immediately an empty array. If the ajax call returns the real data, your array will be filled with the arrived data. So the first assignment to $scope.resources fires your $watch function with an empty array. You my solve your problem if you are using the $watchCollection function. See http://docs.angularjs.org/api/ng.$rootScope.Scope for further information.

multiselect treeview in backbone

I'm new to backbone and trying to establish some good paradigms.
Right now, I'm working on a search heavy site. There are dozens of attributes to search on, many are min max type, but 6 or so are multi select. Prior to backbone, I was using something called listtree to make a collapsible listtree for the multiselect options. I'm still going to use those css classes, but now I'm trying to use backbone with models and views. TBH, this seems like more work than just using straight jquery, so maybe I'm missing something.
My question is, how should I structure the models and the views for several multiselect widgets in a treeview?
Here is the code I have so far:
<script type='text/template' id='listtree_bs'>
<div class="listtree">
<ul>
<% _.each(context, function(element, index){ %>
<li>
<span>
<input class="checkbox-listview-master" type="checkbox" value="<%= element.value %>"><%= element.name %><i class="glyphicon glyphicon-chevron-up"></i>
</span>
<ul style="display: none;">
<% _.each(element.items, function(childelement, index){ %>
<li>
<span>
<input class="checkbox-listview-master" type="checkbox" value="<%= childelement.value %>"><%= childelement.name %><i class="glyphicon glyphicon-chevron-up"></i>
</span>
</li>
<% }); %>
</ul>
</li>
<% }); %>
</ul>
</div>
</script>
var ListTreeModel = Backbone.Model.extend({
urlRoot: "/search/multiselect/",
idAttribute:'value',
});
var ListTreeModels = Backbone.Collection.extend({
model: ListTreeModel,
url: "/search/multiselect/",
parse: function (response) {
return response.data;
}
});
var listtreemodels = new ListTreeModels();
listtreemodels.fetch()
var ListTreeView = Backbone.View.extend({
events: {
"treechecked": "treechecked"
},
treechecked: function( e ){
console.log('triggered');
});
var listtreeview = new ListTreeView({el: $('#listtree_bs')});
The response.data from above looks kind of like this (I can easily change the backend though to facilitate the front end)
{
"data": [
{
"other": 0,
"values": [
{
"value": 1,
"key": "type (35513)"
}
],
"value": "type_of_code",
"key": "C Type",
"missing": 275793
},
{
"other": 25273,
"values": [
{
"value": 41,
"key": "United States of America (187293)"
}
],
"value": "primary_country_id",
"key": "Primary Country",
"missing": 3475
},
{
"other": 10958,
"values": [
{
"value": 623,
"key": "company 623 (12602)"
}
],
"value": "controller_id",
"key": "Search by Controller",
"missing": 248288
},
{
"other": 1294,
"values": [
{
"value": 6,
"key": "animal type (247267)"
},
{
"value": 7,
"key": "animal type y (23315)"
}
],
"value": "animal_id",
"key": "Animals",
"missing": 0
},
{
"other": 0,
"values": [
{
"value": 5,
"key": "Inactive (63693)"
},
{
"value": 1,
"key": "Active (825)"
}
],
"value": "current_status_code_table_id",
"key": "Current Status",
"missing": 109101
},
{
"other": 0,
"values": [
{
"value": 0,
"key": "stuff (275058)"
},
{
"value": 1,
"key": "more stuff (39860)"
},
{
"value": 2,
"key": "even more stuff (668)"
}
],
"value": "stuff_indicator",
"key": "Stuff Indicator",
"missing": 0
}
]
}
so right now, models are populated at the data level, but should they be populated at the nested level and manage this with some kind of one to many relationship?
What these multiselects do is fill out a search form that will get sent back to the server when the user hits search. Can I bind the above views to the model even if they are nested?
I'm trying backbone as an experiment here, but is this what it was really designed for? The search results are complicated and are sliced down in dozens of views. I was hoping to use backbone to keep the dom light and nimble. Right now it's getting bogged down in a lot of event call backs and just a lot of html.
Setting up a hierarchy of models is not that difficult : you just have to build your submodels when you parse the data. One way to do it is
var ListTreeModel = Backbone.Model.extend({
urlRoot: "/search/multiselect/",
idAttribute:'value',
constructor: function(data, opts) {
// force the parsing of the data
opts = _.extend({}, opts, {parse: true});
// setup the children collection
this.values = new ListTreeModels();
// call the parent constructor
Backbone.Model.call(this, data, opts);
},
parse: function(data) {
// populate the children
if (_.isArray(data.values))
this.values.set(data.values);
// remove the children from th emodel attributes
return _.omit(data, 'values');
}
});
var ListTreeModels = Backbone.Collection.extend({
model: ListTreeModel,
url: "/search/multiselect/",
parse: function (response) {
return response.data;
}
});
A demo showing the result http://jsfiddle.net/nikoshr/pZU5J/
Once your model structure is up and running, you can render your views (generate the associated HTML) and defined events. Here is a sample way to do it:
var ListTreeView = Backbone.View.extend({
'tagName': 'ul',
render: function () {
var $el = this.$el;
//create a view for each model
this.collection.each(function(model) {
var view = new ListItemView({
model: model
});
$el.append(view.render().el);
});
return this;
}
});
var ListItemView = Backbone.View.extend({
'tagName': 'li',
events: {
'click ': function(e) {
e.stopPropagation(); // avoid triggering an event on the parent level
console.log(this.model.get('value'));
}
},
render: function () {
//render the node
var template = _.template($('#listitem').html());
this.$el.html(template(this.model.toJSON()));
//and add a view for the sub collection
var subview = new ListTreeView({
collection: this.model.values
});
this.$el.append(subview.render().el);
return this;
}
});
with the listitem template defined as
<script type='text/template' id='listitem'>
<span>
<input class="checkbox-listview" type="checkbox" value="<%= value %>"> <%= key %>
</span>
</script>
And a demo http://jsfiddle.net/nikoshr/pZU5J/3/
Hierarchical views can be tricky to render, you probably will have to investigate further on the matter.

Apply jQuery code after directive template is available on DOM

I have the following Directive:
appDirective.directive('sidebar', function () {
var directiveDefinitionObject = {
//return {
restrict: 'E',
replace: true,
template: '<li ng-repeat="m in menu" ng-class="{\'dropdown\':m.submenu, \'open\':m.open}">' +
'<a href="{{m.url}}" ng-class="{\'dropdown-toggle\':m.submenu}" ng-attr-data-toggle="{{m.type}}">' +
'<i class="{{m.image}}"></i>' +
' {{m.name}}' +
'<b class="caret" ng-if="m.submenu"></b>' +
'</a>' +
'<ul ng-if="m.submenu" class="dropdown-menu">' +
'<li ng-repeat="s in m.submenu"><a ng-class="{\'active\':s.active}" href="{{s.url}}"><i class="{{s.image}}"></i> {{s.name}}</a></li>' +
'</ul>' +
'</li>',
link: function (scope, elem, attrs) {
scope.menu = [
{
"name": "Dashboard",
"url": "/manage/dashboard/index",
"image": "fa fa-dashboard",
"type": "dropdown",
"open": dashboardOpen,
"submenu": [
{
"name": "Overview",
"active": dashboard_overview_active,
"url": "/manage/dashboard/index",
"image": "fa fa-circle-o"
},
{
"name": "System Performance",
"active": dashboard_overview_nodes,
"url": "/manage/dashboard/nodes",
"image": "fa fa-tasks"
},
{
"name": "Query Performance",
"url": "/manage/dashboard/index",
"image": "fa fa-bolt"
}
]
},
{
"name": "Data Integration",
"url": "/manage/dataintegration/index",
"image": "fa fa-table"
},
{
"name": "User Management",
"url": "/manage/users/index",
"type": "dropdown",
"image": "fa fa-users",
"open": usersOpen,
"submenu": [
{
"name": "Users",
"active": users_users,
"url": "/manage/users/index",
"image": "fa fa-user"
},
{
"name": "Roles & Permissions",
"active": users_roles,
"url": "/manage/users/roles",
"image": "fa fa-lock"
},
{
"name": "Organizations",
"active": users_orgs,
"url": "/manage/users/organizations",
"image": "fa fa-globe"
}
]
},
{
"name": "Logs and Alerts",
"url": "/manage/logger/index",
"image": "fa fa-bars"
}
];
scope.$watch('menu', function(val) {
console.log(val);
$('.dropdown').on('show.bs.dropdown', function (e) {
$(this).find('.dropdown-menu').first().stop(true, true).slideDown();
});
$('.dropdown').on('hide.bs.dropdown', function (e) {
$(this).find('.dropdown-menu').first().stop(true, true).slideUp();
});
})
}
}
return directiveDefinitionObject;
});
I've added some jQuery code to the $watchfunction that I want to run after the directive has actually drawn the template. However the $watch fires before the elements are added to the DOM.
This is not the way we should work with angular. When working with angular, we should attach event handler though directives like ng-click and ng-change.
In your case, when the model changes, there is no guarantee that the view already finishes rendering. The view also listens to changes to your model as your code does by using $watch. When the model changes, it will notify the view and your code separately. Usually, there is a trick by using $timeout to schedule the work for the next cycle. But I think we could take advantage of jQuery event delegation to deal with dynamic elements.
Try setting replace: false and use event delegation. Don't need $watch
elem.on('show.bs.dropdown','.dropdown', function (e) {
$(this).find('.dropdown-menu').first().stop(true, true).slideDown();
});
elem.on('hide.bs.dropdown','.dropdown', function (e) {
$(this).find('.dropdown-menu').first().stop(true, true).slideUp();
});
Using $watch in your case has another downside that multiple event handlers may be added to the same element because every time the model changes, this code will be run again.

Resources