I am writing my first AngularJS app and I'm trying to get a directive to update its view when an array it received from the service changed.
My directive looks like this:
angular.module('Aristotle').directive('ariNotificationCenter', function (Notifications) {
return {
replace: true,
restrict: 'E',
templateUrl: 'partials/ariNotificationCenter.html',
controller: function ($scope) {
$scope.notifications = Notifications.getNotifications();
$scope.countUnread = function () {
return Notifications.countUnread();
};
}
};
});
The partial is quite simply:
<p>Unread count: {{countUnread()}}</p>
While my Notifications service looks like this:
function Notification (text, link) {
this.text = text;
this.link = link;
this.read = false;
}
var Notifications = {
_notifications: [],
getNotifications: function () {
return this._notifications;
},
countUnread: function () {
var unreadCount = 0;
$.each(this._notifications, function (i, notification) {
!notification.read && ++unreadCount;
});
return unreadCount;
},
addNotification: function (notification) {
this._notifications.push(notification);
}
};
// Simulate notifications being periodically added
setInterval(function () {
Notifications.addNotification(new Notification(
'Something happened!',
'/#/somewhere',
Math.random() > 0.5
));
}, 2000);
angular.module('Aristotle').factory('Notifications', function () {
return Notifications;
});
The getNotifications function returns a reference to the array, which gets changed by the setInterval setup when addNotification is called. However, the only way to get the view to update is to run $scope.$apply(), which stinks because that removes all the automagical aspect of Angular.
What am I doing wrong?
Thanks.
I believe the only problem with you code is that you are using setInterval to update the model data, instead of Angular built-in service $interval. Replace the call to setInterval with
$interval(function () {
Notifications.addNotification(new Notification(
'Something happened!',
'/#/somewhere',
Math.random() > 0.5
));
}, 2000);
And it should work without you calling $scope.$apply. Also remember to inject the $interval service in your factory implementation Notifications.
angular.module('Aristotle').factory('Notifications', function ($interval) {
$interval internally calls $scope.$apply.
I'm not an expert at Angular yet, but it looks like your problem may be in the partial.
<p>Unread count: {{countUnread()}}</p>
I don't think you can bind to a function's results. If this works, I believe it will only calculate the value once, and then it's finished, which appears to be the issue you are writing about.
Instead, I believe you should make a variable by the same name:
$scope.countUnread = 0;
And then update the value in the controller with the function.
Then, in your partial, remove the parentheses.
<p>Unread count: {{countUnread}}</p>
As long as $scope.countUnread is indeed updated in the controller, the changes should be reflected in the partial.
And as a side note, if you take this approach, I'd recommend renaming either the variable or the function, as that may cause issues, or confusion at the very least.
Related
How can i get this function:
$rootScope.$watch(function () {
return $mdMedia('md');
}, function watchCallback(newValue, oldValue) {
$scope.detectScreen = $mdMedia('max-width: 1280px');
})
which at the moment sits in my controller into a service/factory, so i can use it in multiple controllers.
.factory('MyFactory', [
'$rootScope',
'$mdMedia',
function ($rootScope, $mdMedia) {
return {
detectScreen: function (param) {
***///get the function to work from here///***
}
}
}
])
Put the function in the factory without a watch:
app.factory('MyFactory', ['$mdMedia',
function ($mdMedia) {
return {
detectScreen: function () {
return $mdMedia('max-width: 1280px');
};
}
}
]);
In the controllers, use the $doCheck Life-Cycle Hook to keep the scope variable up to date:
app.controller("myController", function(myFactory, $scope) {
this.$doCheck = function () {
$scope.detectScreen = myFactory.detectScreen();
};
});
I solved the issue my self. To whom it may concerns, here is the answer.
put this in your controller:
$scope.$watch(function () {
return CmsFactory.detectScreen();
}, function watchCallback(small) {
$scope.detectScreen = small;
})
And this in your factory:
return {
detectScreen: function () {
return $mdMedia('md');
}
}
Alternatively, you could move the $watch to the app.run() and emit an event when it changes that can be caught by your controllers.
I ran across this answer when I was doing some research. My reputation isn't high enough to post a comment, but to answer jhon dano's question to ookadoo, I think he means this:
if you inject $mdMedia into your controller, you can use a watch to update a scope variable when the screen size changes.
The watch would look like this:
$scope.ScreenIsXs = false;
$scope.$watch(function () {
return $mdMedia('xs');
}, function watchCallback(response) {
//console.log(response); //test code
$scope.ScreenIsXs = response;
});
Then you could use the $scope variable to drive an ng-if in your html:
<div ng-if="ScreenIsXs">I only show when your window is very small! </div>
This would be useful for simple applications that wouldn't required a factory/service. That said, your question directly stated that you needed a service/factory, so I'm not sure how useful the response would have been prior to you finding the solution.
Anyway, I hope this clarifies ookadoo's response for anyone else who stumbles across these answers in the future.
I am trying to pass data from directive to controller via service, my service looks like this:
angular
.module('App')
.factory('WizardDataService', WizardDataService);
WizardDataService.$inject = [];
function WizardDataService() {
var wizardFormData = {};
var setWizardData = function (newFormData) {
console.log("wizardFormData: " + JSON.stringify(wizardFormData));
wizardFormData = newFormData;
};
var getWizardData = function () {
return wizardFormData;
};
var resetWizardData = function () {
//To be called when the data stored needs to be discarded
wizardFormData = {};
};
return {
setWizardData: setWizardData,
getWizardData: getWizardData,
resetWizardData: resetWizardData
};
}
But when I try to get data from controller it is not resolved (I think it waits digest loop to finish), So I have to use $timeout function in my controller to wait until it is finished, like this:
$timeout(function(){
//any code in here will automatically have an apply run afterwards
vm.getStoredData = WizardDataService.getWizardData();
$scope.$watchCollection(function () {
console.log("getStoredData callback: " + JSON.stringify(vm.getStoredData));
return vm.getStoredData;
}, function () {
});
}, 300);
Despite of the fact that it works, what I am interested in is, if there is a better way to do this, also if this is bug free and the main question, why we use 300 delay and not 100 (for example) for $timeout and if it always will work (maybe for someone it took more time than 300 to get data from the service).
You can return a promise from your service get method. Then in your controller, you can provide a success method to assign the results. Your service would look like this:
function getWizardData() {
var deferred = $q.defer();
$http.get("/myserver/getWizardData")
.then(function (results) {
deferred.resolve(results.data);
}),
function () {
deferred.reject();
}
return deferred.promise;
}
And in your ng-controller you call your service:
wizardService.getWizardData()
.then(function (results) {
$scope.myData = results;
},
function () { });
No timeouts necessary. If your server is RESTFULL, then use $resource and bind directly.
Use angular.copy to replace the data without changing the object reference.
function WizardDataService() {
var wizardFormData = {};
var setWizardData = function (newFormData) {
console.log("wizardFormData: " + JSON.stringify(wizardFormData));
angular.copy(newFormData, wizardFormData);
};
From the Docs:
angular.copy
Creates a deep copy of source, which should be an object or an array.
If a destination is provided, all of its elements (for arrays) or properties (for objects) are deleted and then all elements/properties from the source are copied to it.
Usage
angular.copy(source, [destination]);
-- AngularJS angular.copy API Reference
This way the object reference remains the same and any clients that have that reference will get updated. There is no need to fetch a new object reference on every update.
I need a Factory object that can be applied with a $scope.property to get a result. I also need to know if either the modifier in the factory instance, or the $scope.property changes to update the result. How I am seeing this pattern might be wrong of course.
app.factory('MyFactory',
function () {
var MyFactory = function (name, modifier) {
this.name = name;
this.result = 0;
this.modifier = modifier;
}
//I might want to call this when modifier or MyProperty changes
MyFactory.prototype.modifyingMethod = function () {
this.result = this.modifier * //externalProperty;
}
MyFactory.prototype.watcher = function () {
//no idea what I will do here or if I need this at all
// I need to recalculate the result like with $watch
this.modifyingMethod();
}
return MyFactory;
}
)
app.controller('MyCtrl'
function($scope, MyFactory) {
$scope.MyProperty = 42;
$scope.MyFactoryOptions = [
new MyFactory('Option1', 1),
new MyFactory('Option2', 2),
new MyFactory('Option3', 3),
new MyFactory('Option4', 4),
new MyFactory('Option5', 5)
];
}
So I have the problem that I need to $watch MyProperty and the modifier (it can be changed bu users) so I can change the result. If the Property is a value type passing it into the Factory constructor will not work. Perhaps I could pass a function in the returns MyProperty.
Can I set up internal $watch on the factory. If I would do this outside of the factory, in the controller, I would need to do it for each instance. Should I perhaps set up some sort of register method on my factory object?
Are there any suggestions, patterns or something I might want to use?
Basically you could understand your Factory as an interface to a collection of objects (either an array or associative array respectively pure javascript object).
I find your approach with Objects very appealing and I am sure you can achieve similar things. Still I put together a fiddle, that shows how I would solve the problem:
In a MVC pattern your Factory would be the model, the controller should be as simple as possible and your HTML with directives represents the view.
You controller watches for changes from the user ($scope.MyProperty with $watch). While the model is self aware of any depending external property changes. Note that the changes of the ExternalObject service/factory will only be recognizable, if those aren't primitive values. That is why in the ExternalObject factory I return the whole object.
In a perfect world you wouldn't need to listen for changes with an interval, but will receive a javascript event. If this is possible, do it! Note that object updates out of Angular's scope will need you to do a $scope.$apply(), if you want to see the changes in the view. $intervaland $timeout both call a $scope.apply(), so using them is best practice.
Actually there still has to be a lot of cleanup to be done in this code, but it might give you a basic idea how to use an alternative structure:
var app = angular.module('yourApp', []);
window.yourGlobalObject = {};
setInterval(function () {
yourGlobalObject.externalProperty = Math.floor(Math.random() * 5000);
}, 1000);
app.factory('ExternalObject', ['$window', function ($window) {
return $window.yourGlobalObject;
}]);
app.factory('MyFactory', ['$interval', 'ExternalObject', function ($interval, ExternalObject) {
var options = [{
name: 'Option1',
modifier: 1
}, {
name: 'Option2',
modifier: 2
}, {
name: 'Option3',
modifier: 3
}],
cachedExternalProperty = 0,
cachedMyProperty = 0;
MyFactory = {
getOptions: function () {
return options;
},
addOption: function (name, modifier) {
options.push({
name: name,
modifier: modifier
});
},
setMyProperty: function (value) {
cachedMyProperty = value;
},
setResults: function (myProperty) {
angular.forEach(options, function (option, key) {
option.result = option.modifier * ExternalObject.externalProperty * myProperty;
});
console.log(options);
}
};
// let the service check for updates in the external property, if changed
$interval(function () {
if (cachedExternalProperty !== ExternalObject.externalProperty) {
cachedExternalProperty = ExternalObject.externalProperty;
MyFactory.setResults(cachedMyProperty);
}
}, 1000);
return MyFactory;
}]);
app.controller('MyCtrl', ['$scope', 'MyFactory', function ($scope, MyFactory) {
$scope.MyProperty = 42;
$scope.MyFactoryOptions = MyFactory.getOptions();
$scope.setResults = function () {
MyFactory.setResults($scope.MyProperty);
};
$scope.$watch('MyProperty', function (value) {
MyFactory.setMyProperty(value)
MyFactory.setResults(value);
});
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body>
<section ng-app="yourApp" ng-controller="MyCtrl">
<button ng-click="setResults(MyProperty)">Update Results</button>
<div ng-repeat="factory in MyFactoryOptions">{{factory.name}} {{factory.result}}</div>
<input type="number" ng-model="MyProperty">
</section>
</body>
I have developed a web application using Angular.js (It's my first). The application features a collection of interactive graphics (seat maps); so I created a module to handle the Raphael stuff, including a directive, like so:
angular.module('raphael', [])
.factory('fillData', function() {
return function(paper, data) {
var canvas = $(paper.canvas);
// Do fill the data and more ...
canvas.on('click', '[id]', function(e) {
this.classList.toggle('selected');
});
};
})
.directive('raphael', ['fillData',
function(fillData) {
return {
scope: {
raphael : '&',
seatData: '&'
},
link: function(scope, element, attrs) {
var paper = null;
var updateSeatData = function() {
if(scope.seatData()) fillData(paper, scope.seatData());
};
scope.$watch(scope.raphael, function() {
element.empty();
paper = new Raphael(element[0], '100%', '100%');
paper.add(scope.raphael());
updateSeatData();
});
scope.$watch(scope.seatData, function() {
updateSeatData();
});
}
};
}
]);
Everything works fine, until it get to the point where we need to interact with the vector in another level. Let's say, getting a count of selected seats, or deselecting all (triggered by some random element in the document).
I don't seem to be able to find a reasonable way of implementing it.
What do you suggest?
Is there any other approach to using a second library inside angular?
From what I understand you want to have directive which have certain internal state but you would like to access it's state from outside (other directive, service, etc.).
If so, then it seems that you could use service as state holder. In such case your directive will not hold state but it will be accessing it.
What do you mean by a reasonable way of implementing it? It looks good, although I would prefer to bind to the attribute seatData instead of passing function like
scope: {
seatData: '='
}
And then watch it
scope.$watch('seatData', function() {
fillData(paper, scope.seatData);
});
Is this you issue or I haven't understood it?
OK, here is the solution I came up with; I accessed the parent scope and put essential methods there.
Adding this line to the fillData factory:
return {
deselectAll: function() { ... }
};
And changed updateSeatData method to:
var updateSeatData = function() {
if(scope.seatData) {
var result = fillData(paper, scope.seatData[scope.level]);
angular.extend(scope.$parent, result);
}
};
p.s. Still open to hearing moreā¦
While attempting to answer a question regarding sharing data between two separate controllers I ran into a question .
I usually use services for for this task and began to create a jsfiddle, but I could not get it to work.
After a bit of debugging if I created the properties dynamically in setActivePersonWorks(person) the dirty checking worked and the second controller showed the correct value.
If I assigned the value in setActivePersonDoesNotWork() it did not.
If I used $timeout() I was able to verify that DataService.badPerson did indeed contain the correct data.
Am I doing something wrong? I guess if you do something with $apply() it will work correctly, but why does creating the values dynamically cause things to just work?
Working Example:
var myTest = angular.module("MyTest", []);
myTest.factory("DataService", function () {
var People = {
goodPerson: {},
badPerson: {},
setActivePersonWorks: function (person) {
People.goodPerson.name = person.name;
People.goodPerson.id = person.id;
},
setActivePersonDoesNotWork: function (person) {
People.badPerson = person;
}
};
return People;
});
function ViewController($scope, DataService, $timeout) {
$timeout(function () {
DataService.setActivePersonWorks({
id: 1,
name: "Good Mark"
});
DataService.setActivePersonDoesNotWork({
id: 2,
name: "Bad Mark"
});
}, 1000);
}
function DetailController($scope, DataService, $timeout) {
$scope.goodPerson = DataService.goodPerson;
$scope.badPerson = DataService.badPerson;
$timeout(function(){
$scope.message = "DataService has the value: " + DataService.badPerson.name + " but $scope.badPerson is " + $scope.badPerson.name;
}, 2000);
}
The <html/>
<div ng-app="MyTest">
<div ng-controller="ViewController"></div>
<div ng-controller="DetailController">
<h1>Works: {{goodPerson.name}}</h1>
<h1>Does Not Work: {{badPerson.name}}</h1>
{{message}}
</div>
</div>
On jsfiddle
When Angular sees
<h1>Does Not Work: {{badPerson.name}}</h1>
it sets up a $watch on object badPerson. Looking at your controller, $scope.badPerson is a reference to object DataService.badPerson. All is fine so far... the problem happens here:
setActivePersonDoesNotWork: function (person) {
People.badPerson = person;
}
When this function executes, badPerson is assigned a new/different object reference, but the controller is still $watching the old/original object reference.
The fix is to use angular.copy() to update the existing badPerson object, rather than assigning a new reference:
setActivePersonDoesNotWork: function (person) {
angular.copy(person, People.badPerson);
}
This also explains why setActivePersonWorks() works -- it does not assign a new object reference.