How do you use a recursive function within a directive in angularJS? - angularjs

I'd like to create a ToDo app in AngularJS that uses nested ToDos. As part of this, I'd like the ability to indent or outdent a ToDo with a click (ultimately, it will react to a 'tab' or 'shift-tab' keypress). When indenting, the function should also indent any child ToDos. This is where I'm having trouble. I'm not sure how to implement a recursive function in a directive. If anyone has seen this done or could offer any help, i would appreciate it. I've created a working JSFiddle that indents a single ToDo but doesn't indent it's children. JS Fiddle is here https://jsfiddle.net/t1ba50k6/16/
Also, please offer any best practices on coding convention or to clean up the directive.
Thanks!
My HTML:
<ul id=Todos ng-app="app" ng-controller="todos">
<div ng-repeat='todo in model' ng-init="todoIndex = $index">
<todo-item content="todo"></todo-item>
</div>
My javascript:
var app = angular.module('app', []);
app.controller("todos", function ($scope) {
$scope.model = [{
"title": "Task item one",
"indentLevel": 1
}, {
"title": "Task item two",
"indentLevel": 2
}, {
"title": "Task item three",
"indentLevel": 2
}];
});
app.directive('todoItem', function ($compile) {
var getTemplate = function (content) {
var startStr = '';
var endStr = '';
if (content.indentLevel == 1) {
startStr = '<li class="todolist"><table><tr><td><div class="checkboxpadding"><img width="16" height="16" class="checkbox"></div></td><td><div ng-click="indent()">';
endStr = '</div></td></tr></table></li>';
return startStr + content.title + endStr;
} else if (content.indentLevel == 2) {
startStr = '<li class="todolist indent_2""><table><tr><td><div class="checkboxpadding"><img width="16" height="16" class="checkbox"></div></td><td><div ng-click="indent()">';
endStr = '</div></td></tr></table></li>';
return startStr + content.title + endStr;
}
};
var linker = function (scope, element, attrs) {
element.html(getTemplate(scope.content)).show();
$compile(element.contents())(scope);
scope.indent = function () {
var childElem = angular.element(element.children()[0]);
if (scope.content.indentLevel < 3) {
childElem.removeClass("indent_" + scope.content.indentLevel);
childElem.addClass("indent_" + (scope.content.indentLevel + 1));
scope.content.indentLevel++;
// update Database
// indent child nodes?
// for (var i=0; i < nodes; i++) { }
}
};
};
return {
restrict: "E",
link: linker,
scope: {
content: '='
}
};
});

Mapping your data to a nested structure would greatly simplify this.
Always use the same property names for the child arrays:
$scope.model = [{
"title": "Task item one",
"indentLevel": 1,
"children": [
{
"title": "Task item two",
"indentLevel": 2,
"children":[]
}, {
"title": "Task item three",
"indentLevel": 2,
"children":[]
}
]
}];

One solution is using $broadcast(name, args) and $on(name, listener) of scope.
when cliked fire an event:
scope.$broadcast('indent');
in child directives,
scope.$on('indent', scope.indent);

Related

Angular : load lots of data via directive causing browser issue

I have an angular directive loading a svg map (using amchart) where I add thousands of svg circles. In the end everything works but my browser seems in pain and I would need to (1) optimize my loading and (2) display a loading symbol that could last till the map can actually display for real.
Today I use this kind of directive attribute to know when my directive is loaded :
directive('initialisation',['$rootScope',function($rootScope) {
return {
restrict: 'A',
link: function($scope) {
var to;
var listener = $scope.$watch(function() {
clearTimeout(to);
to = setTimeout(function () {
listener();
$rootScope.$broadcast('initialised');
}, 50);
});
}
};
Well this is not good to me as my loading symbol (angular-material) freezes, and then disappear to leaves an empty browser for a few seconds, before the map can render. For information I use ng-hide on the loading div and ng-show on the map div, and this is the way I apply it :
$scope.$on('initialised', function() {
$scope.$apply(function(){
$scope.mapLoaded = true;
});
})
Do you know a way to solve my (1) and (2) issue ? Or should I look for another js library to do this?
Thank you
PS : here is my map directive (images is an array with 20k entry at the moment) :
directive('amChartsLanguage', function() {
return {
restrict: 'E',
replace:true,
template: '<div id="mapLanguage" style="height: 1000px; margin: 0 auto"> </div>',
link: function(scope, element, attrs) {
var chart = false;
var initChart = function() {
if (chart) chart.destroy();
var images = [];
var legendData = [];
for(var i=0 ; i < scope.languageZeppelin.length ; i ++ ) {
images.push( {
"type": "circle",
"width": 7,
"height": 7,
"color": scope.languageZeppelin[i].color,
"longitude": scope.languageZeppelin[i].lon,
"latitude": scope.languageZeppelin[i].lat
} );
}
var legend = new AmCharts.AmLegend();
legend.width="10%";
legend.height="300";
legend.equalWidths = false;
legend.backgroundAlpha = 0.5;
legend.backgroundColor = "#FFFFFF";
legend.borderColor = "#ffffff";
legend.borderAlpha = 1;
legend.verticalGap = 10;
legend.top = 150;
legend.left = 70;
legend.position = "left";
legend.maxColumns = 1;
legend.data = scope.legend;
// build map
chart = AmCharts.makeChart( "mapLanguage", {
"type": "map",
"areasSettings": {
"unlistedAreasColor": "#15A892",
"autoZoom": true,
"selectedColor": "#FFCC00",
"color": "#909090"
},
"dataProvider": {
"map": "worldLow",
"getAreasFromMap": true,
"images": images,
"zoomLevel": 1,
"zoomLongitude": 6,
"zoomLatitude": 11
},
"export": {
"enabled": false
}
} );
chart.addLegend(legend);
chart.validateNow(legend);
};
initChart();
}
}
})
we have angular with a lot more data on the page. with poor design og architechture. loading ofcoarse too long but after that performance is great. we use d3

Load more button in AngularJS and lightGallery

I am using lightGallery for a website and I wanted to add a "load more" button to the gallery page, mostly for faster loading on mobile phones. I found various methods and tried them. Most of them don't work and/or doesn't suit my need for loading the elements on request.
One of them was with AngularJS:
var DemoApp = angular.module("DemoApp", []);
DemoApp.controller("DemoController",
function DemoController($scope) {
$scope.quantity = 0;
$scope.temp = [];
$scope.loadMore = function () {
for (i = $scope.quantity; i <= $scope.quantity + 1; i++) {
$scope.temp.push($scope.images[i]);
}
$scope.quantity = i;
}
$scope.images = [{
"src": "https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/13244856_238092939903469_3778800896503555327_n.jpg?oh=e539748b060ba0cb43852314e2fdef0b&oe=57F01511"
}, {
"src": "https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/13263891_238096316569798_4073904852035872297_n.jpg?oh=91a76b3515ac628706b912fdd3e9a346&oe=585C3DD1"
}, {
"src": "https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/13260007_238096336569796_6324932140560503740_n.jpg?oh=1795ba25c4604dced3cdcc91b9729cc7&oe=5820EE5A"
}, {
"src": "https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/12871473_238096353236461_8115646425269026926_n.jpg?oh=b8958326d24a1a649e6a40adf29b062b&oe=582BFD38"
}, {
"src": "https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/13256520_238096376569792_9057528716929719317_n.jpg?oh=a6bc66f75992c88260ae35bd4dbc9ff1&oe=5856F934"
}, {
"src": "https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/13254297_238096389903124_1429633590050411734_n.jpg?oh=5e8c94a0b6a77dea110704a5727e0ee5&oe=5819B551"
}, {
"src": "https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/13267713_238096416569788_8740461856631991826_n.jpg?oh=739e3268996e498f65867b314265250b&oe=585E4C93"
}];
$scope.loadMore();
});
And this is my HTML mark up:
<div class="col-xs-12">
<div ng-app="DemoApp" ng-controller="DemoController">
<div id="fotoalbum" class="col-thumb-wrap">
<div class="col-thumb" ng-repeat="image in temp" data-src="{{image.src}}">
<a href="{{image.src}}">
<i class="thumb" style="background-image: url({{image.src}});"></i>
</a>
</div>
</div>
<button class="btn btn-default" ng-click="loadMore()">Ik wil meer</button>
</div>
</div>
The "load more" button it self worked, however it broke the lightGallery itself.
Example: http://cytex.nl/projects/jk-installaties/album2.php
Then I found the solution for making lightGallery work with AngularJS in this StackOverflow question
I tried to combine the two but it still doesn't work. Now lightGallery gets fired up OK, but the "load more" button doesn't do anything!
Example: http://cytex.nl/projects/jk-installaties/album1.php
var DemoApp = angular.module('DemoApp', []);
DemoApp.controller('DemoController',
function DemoController($scope, $sce) {
$scope.total = 0;
$scope.temp = [];
$scope.loadMore = function () {
for (x = $scope.total; x <= $scope.total + 1; x++) {
$scope.temp.push($scope.photos[x]);
}
$scope.total = x;
}
$scope.photos = [{
fullres: 'https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/13244856_238092939903469_3778800896503555327_n.jpg?oh=e539748b060ba0cb43852314e2fdef0b&oe=57F01511'
}, {
fullres: 'https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/13263891_238096316569798_4073904852035872297_n.jpg?oh=91a76b3515ac628706b912fdd3e9a346&oe=585C3DD1'
}, {
fullres: 'https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/13260007_238096336569796_6324932140560503740_n.jpg?oh=1795ba25c4604dced3cdcc91b9729cc7&oe=5820EE5A'
}, {
fullres: 'https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/12871473_238096353236461_8115646425269026926_n.jpg?oh=b8958326d24a1a649e6a40adf29b062b&oe=582BFD38'
}, {
fullres: 'https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/13256520_238096376569792_9057528716929719317_n.jpg?oh=a6bc66f75992c88260ae35bd4dbc9ff1&oe=5856F934'
}, {
fullres: 'https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/13254297_238096389903124_1429633590050411734_n.jpg?oh=5e8c94a0b6a77dea110704a5727e0ee5&oe=5819B551'
},{
fullres: 'https://scontent-ams3-1.xx.fbcdn.net/v/t1.0-9/13267713_238096416569788_8740461856631991826_n.jpg?oh=739e3268996e498f65867b314265250b&oe=585E4C93'
}];
$scope.loadMore();
for (var i = 0; i < $scope.photos.length; i++) {
$scope.photos[i].fullres = $sce.trustAsResourceUrl($scope.photos[i].fullres);
}
})
.directive('lightgallery', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
if (scope.$last) {
element.parent().lightGallery({
showThumbByDefault: false
});
}
}
};
});
I am very new to AngularJS,so I'm sorry if this is a really stupid question. Could you guys help to identify the problem and maybe pointers on how to tackle this?
Append this fragment of your code to the last row of loadMore function:
for (var i = 0; i < $scope.photos.length; i++) {
$scope.photos[i].fullres = $sce.trustAsResourceUrl($scope.photos[i].fullres);
}
This part should be run each time you add an item to the array, It means this should be run each time the function loadMore triggers.
Hope this will be helpfull. Regards.
For others who ran into this issue:
The answer of Ramin Esfahani is of course correct. But you also have to destroy the lightGallery data ever time the "Load More" button is clicked. This is in done in the directive part. Of course change the "#fotoalbum" to your own ID or class. This is the code:
.directive('lightgallery', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
if (scope.$last) {
element.parent().lightGallery({
showThumbByDefault: false
});
$('#LoadMore').on('click', function(){
$('#fotoalbum').data('lightGallery').destroy('true');
$('#fotoalbum').lightGallery({
showThumbByDefault: false
});
});
}
}
};
})

Multiple instances of custom directive, confusion with ngModel

I've created a custom directive to render a slider for a question (essentially wrapping jquery ui slider). The directive takes an ngModel and updates it when the user uses the slider, and there's a $watch attached to the parent model (the ngModel passed to the directive is only a part of a parent model). The directive has multiple instances on a page.
I've encountered an issue with the watch, as it seems as the watch always occurs on the last question on the page. So for example a page with 10 question, using the slider on question 1 - triggers the watch on the last question (question 10). I believe the issue has something to do with directives/isolated scope and/or the watch function, but I'm unable to solve it.
this.app.directive('questionslider', () => {
var onChangeEvent = (event, ui) => {
updateModel(ui.value);
};
var onSlideEvent = (event, ui) => {
updateUi(event, ui);
};
var updateUi = (event, ui) => {
$(ui.handle).find(".ff-handle-glyph > div").css("top", (ui.value) * - 10);
}
var updateModel = (newValue) => {
// find value in values list...
angular.forEach(isolatedScope.model.PossibleValues, function(value) {
if (parseInt(value.Name) === newValue) {
isolatedScope.$apply(function() {
isolatedScope.model.Value = value;
});
}
});
};
var isolatedScope: any;
return {
restrict: 'AE',
replace: true,
template: '<div></div>',
scope: {
model: '=ngModel',
},
link: function(scope, element, attrs, ctrl) {
isolatedScope = scope;
scope.$watch(ngModelCtrl, function() {
// use provided defaultValue if model is empty
var value = isolatedScope.model.Value === null ? isolatedScope.model.DefaultValue : isolatedScope.model.Value;
element.slider({
min: 0,
max: isolatedScope.model.PossibleValues.length,
value: value.Name,
change: onChangeEvent,
slide: onSlideEvent
});
}
}
};
};
Code to add watch in controller
this.$scope.questions.forEach(function(question) {
this.$scope.$watch(
function() { return question; },
function(newVal, oldVal) { this.updateQuestion(newVal, oldVal) },
true
);
});
UpdateQuestion function (right now just outputting current question)
function updateQuestion(newVal, oldVal) {
// prevent event on initial load
if (newVal === oldVal) {
return;
}
console.log(newVal);
}
The ng-repeat markup instantiating questionsliders
<div data-ng-repeat="question in Questions">
<h4>{{question.QuestionText}}</h4>
<p>{{question.RangeMinText}}</p>
<questionslider ng-model="question"></questionslider>
<p>{{question.RangeMaxText}}</p>
</div>
Question JSON would look like this
{
"DefaultValue": {
"Id": "5",
"Name": "5"
},
"Id": "1",
"IsAnswered": false,
"PossibleValues": [
{
"Id": "1",
"Name": "1"
},
{
"Id": "2",
"Name": "2"
},
{
"Id": "3",
"Name": "3"
},
{
"Id": "4",
"Name": "4"
},
{
"Id": "5",
"Name": "5"
},
{
"Id": "6",
"Name": "6"
},
{
"Id": "7",
"Name": "7"
},
{
"Id": "8",
"Name": "8"
},
{
"Id": "9",
"Name": "9"
},
{
"Id": "10",
"Name": "10"
}
],
"QuestionText": "hows it haning?",
"RangeMaxText": "not good",
"RangeMinText": "Very good",
"Type": 0,
"Value": null
}
],
"Title": "Question title",
"Type": 0
}
So issue is, no matter which question I update with the slider directive, it's always the last on page passed into updateQuestion.
UPDATE
I tried using $watchCollection, but nothing seems to fire the event.
this.$scope.$watchCollection(
'questions',
function (newVal, oldVal) {
// prevent event on initial load
if (newVal === oldVal) {
return;
}
for (var i = 0; i < newVal.length; i++) {
if (newVal[i] != oldVal[i]) {
this.$log.info("update : " + newVal.Id);
}
}
}
);
I also tried with
function() { return questions; }
as first expressions. Still no luck.
Maybe using individual controllers for each question is my only option, but it seems a bit of a workaround.
UPDATE
So i tried using individual controllers for each question, adding a watch per question in the controller, and the strange thing is that even this is reproducing same scenario. It's still the last question on the page passed into the watch function.
markup
<div data-ng-repeat="question in Questions" data-ng-controller="QuestionInstanceController">
<h4>{{question.QuestionText}}</h4>
<p>{{question.RangeMinText}}</p>
<questionslider ng-model="question"></questionslider>
<p>{{question.RangeMaxText}}</p>
</div>
Controller code
app.controller('QuestionInstanceController', function($scope) {
console.log($scope.question); // outputs correct question here!
$scope.$watch(
function() { return $scope.question; },
function(newValue, oldValue) {
console.log(newValue); // always outputs last question on page
},
true);
}
}
It must have something to do with my custom directive, am i somehow overwriting previous instances when having multiple instances on the page?
So i managed to find the solution myself. The issue was that i had a "var isolatedScope;" in the directive which was assigned on every run of the link(). I thought vars declared in the directive were isolated on each instance but infact they are overwritten on every implementation of the directive.
The solution therefore was to move the implementation of onChange directly to the init of the slider component, and thus avoiding the need of access to the scope variable later
this.app.directive('questionslider', () => {
var onChangeEvent = (event, ui) => {
};
var onSlideEvent = (event, ui) => {
updateUi(ui.value, ui.handle);
};
var updateUi = (value, element) => {
}
var onStartSlide = (event, ui) => {
}
var onEndSlide = (event, ui) => {
}
return {
restrict: 'AE',
require: 'ngModel',
replace: true,
templateUrl: 'templates/questionSlider.html',
scope: {
model: '=ngModel'
},
link: (scope: any, element, attrs, ngModelCtrl: ng.INgModelController) => {
scope.$watch(ngModelCtrl, () => {
// use provided defaultValue if model is empty
var value = scope.model.Value === null ? scope.model.DefaultValue : scope.model.Value;
var hasNoValue = scope.model.Value === null;
if (hasNoValue) {
element.find('.ui-slider-handle').addClass('dg-notset');
}
element.slider({
min: parseInt(scope.model.PossibleValues[0].Name),
max: scope.model.PossibleValues.length,
value: parseInt(value.Name),
slide: onSlideEvent,
start: onStartSlide,
stop: onEndSlide,
animate: true,
change: (event, ui) => {
// find value in values list...
angular.forEach(scope.model.PossibleValues, (val) => {
if (parseInt(val.Name) === ui.value) {
scope.$apply(() => {
scope.model.Value = val;
});
}
});
onChangeEvent(event, ui);
}
});
});
}
};
});
I expect that your $scope.$watch logic is throwing you off. The simplest way might be to put a watch on the entire array of questions using $watchCollection:
$scope.$watchCollection(questions, function(newValue, oldValue) {
for(index=0; index<newValue.length; index++) {
if (newValue[index] !== oldValue[index]) {
console.log("Question " + index + " updated to: " + newValue[index]);
}
}
});
Otherwise, you could probably create a separate controller for each item in your ng-repeat loop and have a watch there that deals with the change. I don't love this solution, either, as it's kinda long-winded. First, a new controller for dealing with the questions:
app.controller('QuestionCtrl', function($scope) {
$scope.$watch('question', function(newValue, oldValue) {
if (newVal === oldVal) {
return;
}
console.log(newVal);
}
});
With your view slightly modified to include a reference to the new QuestionCtrl:
<div data-ng-repeat="question in Questions" ng-controller='QuestionCtrl'>
<h4>{{question.QuestionText}}</h4>
<p>{{question.RangeMinText}}</p>
<questionslider ng-model="question"></questionslider>
<p>{{question.RangeMaxText}}</p>
</div>
This article gives more information about using controllers with ng-repeat.
I hope one of these helps you out.

Angular record the order of checkboxes as clicked

I am trying to add the functionality in my angular application to record the order of the checkboxes as they are clicked and hence show them in same order as they were clicked.
Here is the snippet:
var app = angular.module('checkbox', []);
app.controller('homeCtrl', function ($scope) {
$scope.array = [1, 5];
$scope.array_ = angular.copy($scope.array);
$scope.list = [{
"id": 1,
"value": "apple",
}, {
"id": 3,
"value": "orange",
}, {
"id": 5,
"value": "pear"
}];
$scope.update = function () {
if ($scope.array.toString() !== $scope.array_.toString()) {
return "Changed";
} else {
return "Not Changed";
}
};
})
});
In this snippet array should show the numbers in the order they were clicked.
Not sure if I understood exactly what you need but please check this Plunker
The idea is to use a function like this to record the click order:
$scope.onClick = function(box) {
var index = $scope.model.clickOrder.indexOf(box);
if (index !== -1) {
$scope.model.clickOrder.splice(index, 1);
} else {
$scope.model.clickOrder.push(box);
}
updateOrderedList();
};
And a function like this to update the checkboxes order:
var updateOrderedList = function() {
$scope.model.orderedList = []
.concat($scope.model.clickOrder)
.concat($scope.model.list.filter(function(item) {
return $scope.model.clickOrder.indexOf(item) === -1;
}));
};

$rootScope.$apply() ignores variable assignment

In my Service I have this code:
setInterval(function () {
$rootScope.$apply(function () {
cart.push({
"Id": 4,
"Name": "Some item",
"Price": 4
});
});
}, 3000);
Working example: http://jsfiddle.net/ko8edf32/7/
This works correctly: the cart change is pushed to the view.
However, when I assign a new value to cart, the change is NOT pushed to the view:
setInterval(function () {
$rootScope.$apply(function () {
var newcart = [{
"Id": 4,
"Name": "Some item " + Math.random(),
"Price": 4
}];
cart = newcart;
});
}, 3000);
example: http://jsfiddle.net/ko8edf32/8/
What is the reason of this?
What is the best/most elegant solution to solve this issue?
EDIT
This is the working solution I've built, based on the answers below:
jsfiddle.net/ko8edf32/11
I've changed your cart into more "angular" way with two-way databinding. I've added the second controller to show you how nice it works all together without getters/setters and generally with a bit of magic
http://plnkr.co/edit/U5pUlAPJTNd9V0baSjAu?p=preview
homeApp.factory("cartService", function($rootScope) {
var service = null
service = {
all: function() {
return service.cart;
},
add: function(item) {
service.total += item.Price
service.cart.push(item);
},
remove: function(item) {
service.total -= item.Price
service.cart.splice(service.cart.indexOf(item), 1);
},
cartUpdated: function(newValue) {
service.cart = newValue;
},
cart: [],
total: 0
}
return service;
});
You have to use objects on $scope.
$scope.model = { cart: [] };
Here is a working example: http://jsfiddle.net/56vkqygn/2/
Here is a explanation: What are the nuances of scope prototypal / prototypical inheritance in AngularJS?

Resources