$rootScope.$apply() ignores variable assignment - angularjs

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?

Related

AngularJS Testing with Mocha and Sinon - Mocking Firebase Dependencies

I am trying to skip going to Firebase for my data in my tests, and return some simple results instead. I do not want to test that the Firebase code works, but that my factories work in returning data.
I have the following factories:
// Get Firebase Reference
angular.module('MyApp').factory('FireBaseData', function(FIREBASE) {
var FireBaseReference = new Firebase(FIREBASE.URL); // FIREBASE.URL = xxx.firebaseio.com
return FireBaseReference;
})
// Get the Firebase Array of Team Records
angular.module('MyApp').factory('AllTeams', ["FireBaseData", "$firebaseArray",
function(FireBaseData, $firebaseArray) {
return $firebaseArray(FireBaseData.child('Teams'));
}
]);
I have created mocks that replace the individual functions, and my tests will use these.
'use strict';
var $MockFirebaseArray = function(ArrayWithData) {
return ArrayWithData;
};
var $MockFirebaseObject = function(ObjectWithData) {
return ObjectWithData;
};
var MockFirebaseData = function() {
return {
child: function(StringValue) {
return "";
}
};
};
Tests with the mocks:
'use strict';
describe('Firebase Mocks', function() {
var TestArray = [
{ 'aString': 'alpha', 'aNumber': 1, 'aBoolean': false },
{ 'aString': 'bravo', 'aNumber': 2, 'aBoolean': true },
{ 'aString': 'charlie', 'aNumber': 3, 'aBoolean': true },
{ 'aString': 'delta', 'aNumber': 4, 'aBoolean': true },
{ 'aString': 'echo', 'aNumber': 5 }
];
describe('MockFirebaseData', function() {
var TestFirebase = MockFirebaseData();
it('should return empty text ("") from FireBaseData', function() {
assert.equal('', TestFirebase.child('SomeNode'));
});
});
describe('$MockFirebaseArray', function() {
it('should have the data array passed', function() {
var TestData = $MockFirebaseArray(TestArray);
assert.equal(TestArray.length, TestData.length);
});
});
describe('$MockFirebaseObject', function() {
it('should have the data object passed', function() {
var TestData = $MockFirebaseObject(TestArray[0]);
assert.equal(TestArray[0].length, TestData.length);
assert.deepEqual(TestArray[0], TestData);
});
});
});
This shows that the Mocks are working to return data, which is what I want to stay away from actually accessing Firebase. Now, when I try to use my factory in a test, I am getting errors.
Test the Factory:
describe('Teams Module', function() {
beforeEach(module('MyApp')); // Load My Application
describe('AllTeams Array', function() {
// Create Test Data
var TeamData = [
{ "Key": 1, "Name":"Team 1", "Logo": "Team1.jpg" },
{ "Key": 3, "Name":"Team 3", "Logo": "Team3.jpg" },
{ "Key": 2, "Name":"Team 2", "Logo": "Team2.jpg" },
];
beforeEach(function () {
module(function($provide) {
var MockData = MockFirebaseData();
$provide.value('FireBaseData', MockData);
$provide.value('$firebaseArray', $MockFirebaseArray(TeamData));
});
});
it('can get an instance of AllTeams factory', inject(function(AllTeams) {
assert.isDefined(AllTeams);
}));
});
});
Error returned:
PhantomJS 1.9.8 (Windows 7 0.0.0)
Teams Module
AllTeams Array
can get an instance of AllTeams factory FAILED
TypeError: '[object Object],[object Object],[object Object]' is not a function (evaluating '$firebaseArray(FireBaseData.child('Teams'))')
at app/Team.js:9
Instead of:
$provide.value('$firebaseArray', $MockFirebaseArray(TeamData));
try this:
$provide.value('$firebaseArray', $MockFirebaseArray);
I believe this is what you were intending to do in the first place. When injected, your factory will then be able to call $firebaseArray as a function.

How do you use a recursive function within a directive in 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);

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;
}));
};

grabbing just required results from json --- restangular

I am trying to grab just few ids from json and not the complete payload of json. Is that possible? here is my code
Car.controller('IndexCtrl', function ($scope, CarRestangular) {
$scope.cars = CarRestangular.all('car').getList();
});
here is my json
[
{
"id": 1,
"name": "Mike",
},
{
"id": 2,
"name": "Floyd",
},
{
"id": 3,
"name": "Roy",
},
{
"id": 4,
"name": "Roy",
},
]
I want to get records where ids are 2 and 4. How could I do that. Also if possible, what would I need to do to get top two records in an other case that I would need.
Thanks
The newer versions of angular don't automatically unproxy promises in scope. Therefore you can't immediatelt do:
$scope.cars = CarRestangular.all('car').getList();
But you have to do:
CarRestangular.all('car').getList().then(function(result){
$scope.cars = result;
));
The filtering would be the best if you have lodash included.
https://gist.github.com/lc-nyovchev/7414166e4dbf2dfbc908
You define a service with a method that does the filtering for you:
.factory('SomeService',['$q', 'Restangular', function($q,Restangular){
return {
someMethod: function(){
var deferred = $q.defer();
Restangular.all('car').getList().then(function(results){
var filtered = _.filter(results, function(result){
return result.id === 2 || result.id === 4;
});
return deferred.promise;
}
};
}])
Then you use the service in your controller:
.controller('YourController', ['SomeService', function(SomeService){
SomeService.someMethod().then(function(filteredResult){
$scope.cars = filteredResult;
});
}]);
Alternatively you can just do:
CarRestangular.all('car').getList().then(function(results){
$scope.cars = _.filter(results, function(result){
return result.id === 2 || result.id === 4;
});
});
Edit: all those answers assume you want to do the filtering client side. IF your backend supports it, you can do something like:
CarRestangular.all('car').getList(id:[2,4]).then(function(result){
$scope.cars = result;
));
And if your backend is written in such a way that is supports filtering on the id attribute, it would return you the correct results and you wouldn't have to filter them.

Resources