Injecting a $scope.property by ref into a factory instance - javascript

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>

Related

Call method from within itself?

I have this snippet:
myApp.factory('productsStore', function ($http, $q, Product) {
var products = "";
products = productsStore.get();
return {
get: function () {
return Product.query({});
}
};
});
How can I call the get() method, from within the same 'factory'? products = productsStore.get() does not work obviously.
You can use the Revealing Module Pattern:
myApp.factory('productsStore', function ($http, $q, Product) {
var products = "";
var get = function () {
return Product.query({});
};
products = get();
return {
get: get
};
});
Reasons I enjoy this pattern:
No cluttering with this., MyObject., etc. prefixing.
You see clearly in the bottom return what is publicly exposed.
A great article on the subject: Mastering the Module Pattern
You could assign the object you return to a variable, then call the get function defined in your variable, then return the variable.
Either that, or just duplicate your query code.

How to bind properly to a service property in AngularJS?

I have a factory where I have a couple of predefined partners (it could be anything else, I thought it's an example that's easy to understand). On run time, we select the current partner (based on some logic I omitted here).
angular.module('myApp').factory('PartnersService', function ($location, $log) {
var partners = {
firstPartner: {
name: 'Default Partner',
id: 1 // just an extra property as example
},
secondPartner: {
name: 'Other Partner',
id: 2
}
};
// set default value
var partner = partners.firstPartner;
var initPartner = function () {
// based on some logic (omitted), select partner
partner = partners.secondPartner;
$log.log("initPartner should have changed partner to " + partner.name);
};
return {
initPartner: initPartner,
partners: partners,
partner: partner,
};
});
Then, I would like to access the partner as PartnersService.partner and see as it changes, e.g. from a controller:
angular.module('myApp').controller('myController',
function ($scope, $log, PartnersService) {
// PartnersService.partner is the default partner (firstPartner)
PartnersService.initPartner();
// After initPartner, PartnersService.partner is still
// the default, but I expected it to change
});
I found some workarounds (in my opinion... are they workarounds?), but it feels unnatural for me, so I'd like to ask if there's a better way.
See my full, working example on JS Bin. I apologize if you find the example a bit lengthy, but I wanted to make sure Stack Overflow users understand my concerns and can point out if something is wrong with the way I think.
Workaround 1 (getter?):
angular.module('myApp').controller('myController', function ($scope, $log, PartnersService) {
PartnersService.initPartner();
var partner = PartnersService.getPartner();
$log.log('I could use a getter: ' + partner.name);
});
angular.module('myApp').factory('PartnersService', function ($location, $log) {
var getPartner = function () {
return partner;
};
return {
getPartner: getPartner,
// ...
};
});
Workaround 2 (nest in an object literal):
angular.module('myApp').controller('myController', function ($scope, $log, PartnersService) {
PartnersService.initPartner();
$log.log('or nest the partner in an extra object literal: '
+ PartnersService.extraObjectLiteral.partner.name);
});
angular.module('myApp').factory('PartnersService', function ($location, $log) {
var partners = { /*...*/ }
var extraObjectLiteral = {
partner: partners.firstPartner
};
var initPartner = function () {
// based on some logic (omitted), select partner
extraObjectLiteral.partner = partners.secondPartner;
};
return {
extraObjectLiteral: extraObjectLiteral,
//...
};
});
Change to:
this.partner = partners.secondPartner;
in your initPartner method. That will solve it.
What you're really doing when you do
var partners = { ... }
var partner = partners.firstPartner;
is, you're creating local objects in the class, but they are not exposed members of the class. And with the
return {
initPartner: initPartner,
partners: partners,
partner: partner
};
you create members of the class, and copy the values of the local variables to the class' members. In your initPartner method, you change the local object, but the class' object remain unchanged.
I would go with the approach of using getters and setters as this follows the Revealing Module Pattern which is really preferred for maintaining cleaner, more readable and understandable code.
Factory code is as follows.
(function () {
angular.module('myApp').
factory('PartnersService', PartnersService);
function PartnersService($location, $log) {
var partners = {
firstPartner: {
name: 'Default Partner',
id: 1 // just an extra property as example
},
secondPartner: {
name: 'Other Partner',
id: 2
}
};
var partner;
//Set default value
setDefaultPartner();
var service = {
getPartner: getPartner,
setPartner: setPartner
};
return service;
/*** Function Declarations ***/
/* Accessible Functions */
function getPartner() {
return partner;
}
function setPartner() {
var selectedPartner = initPartner();
return selectedPartner;
}
/* Private Functions */
function initPartner() {
/*
* Some (omitted) logic to select partner
*/
partner = partners.secondPartner;
$log.log("initPartner should have changed partner to " + partner.name);
return partner;
}
function setDefaultPartner() {
partner = partners.firstPartner;
}
}
})();
Notice that the only public/accessible members left are the getPartner and setPartner (which calls initPartner) functions.
The controller would be the following.
(function () {
angular.module('myApp').
controller('myController', myController);
function myController($scope, $log, PartnersService) {
$scope.partner = PartnersService.getPartner();
$log.log("should be default partner: " + $scope.partner.name);
$scope.partner = PartnersService.setPartner();
$log.log("After setPartner which calls initPartner, the current partner is now the " + $scope.partner.name);
}
})();
Modifying your JS Bin, the following is the resulting console log.
"should be default partner: Default Partner"
"initPartner should have changed partner to Other Partner"
"After setPartner which calls initPartner, the current partner is now the Other Partner"
I don't think you are missing anything obvious. Both of your workarounds seem ok to me, except that in the latter case I would probably use the following structure:
var partners = { ... };
var initPartner = function () {
retObj.partner = partners.secondPartner;
};
var retObj = {
initPartner: initPartner,
partners: partners,
partner: partners.firstPartner
};
return retObj;

Changing controller $scope vars dynamically

So. I have simple controller and service:
angular
.module('field', [])
.controller('FieldController', function(FieldService) {
var vm = this;
vm.name = FieldService.getName();
})
.service('FieldService', function() {
var name = 'John'
this.getName = function() {
return name;
};
this.setName = function(newName) {
name = newName;
};
})
;
Then i have some $interval in anotherService, that getting data every 1 second and calling FieldService.setName:
.service('UpdateService', function($http, FieldService) {
$interval(function() {
$http.get('/somUrl')
.then(function(response) {
FieldService.setName(response.name);
});
});
})
But it won't change my HTML.
If i switch from primitive to object in returning value getName, then it's working.
Is there another approach? I personally think, that this structure i created is bad, but can't understand how it should be done.
JavaScript is always pass-by-value, but when your variable is an object, the 'value' is actually a reference to the object. So in your case, you are getting a reference to the object, not the value. So when the object changes, that change isn't propagated like a primitive would be.
Your code seems a bit incorrect, too. You are setting the value of response.name to FieldService.setName, which is actually a function.
If you want to use the getter/setter approach you have listed, then you could use events to let the controller know that name has changed.
.service('UpdateService', function($http, FieldService, $rootScope) {
$interval(function() {
$http.get('/somUrl')
.then(function(response) {
FieldService.setName(response.name);
$rootScope.$broadcast('nameChanged', {
name : response.name
});
});
});
})
.controller('FieldController', function(FieldService, $scope) {
var vm = this;
vm.name = FieldService.getName();
$scope.$on('nameChanged', function (evt, params) {
vm.name = params.name;
});
})
Another way to accomplish this is to use a $scope.$watch on the service variable in the controller:
.controller('FieldController', function($scope, FieldService) {
$scope.name = FieldService.getName();
$scope.$watch(function () {
return FieldService.getName();
}, function (newVal, oldVal) {
if (newVal !== oldVal) {
$scope.name = newVal;
}
});
})
I would move my $interval function inside a controller and then just update a $scope attribute every second. Then Angular will take care of the rendering.. Or you must also use an $interval function in your controller which gets the service content (ie FieldService.getName) every second.
I would use it this way:
angular
.module('field', [])
.controller('FieldController', function($scope, FieldService) {
$scope.name = function(){
FieldService.getName();
};
})
.service('FieldService', function() {
var name = 'John'
this.getName = function() {
return name;
};
this.setName = function(newName) {
name = newName;
};
});
Use name() in your html to see the update value.And your other service:
.service('UpdateService', function($http, FieldService) {
$interval(function() {
$http.get('/somUrl')
.then(function(response) {
FieldService.setName(response.name);
});
}, 1000);
})
There are numerous ways in which you can achieve this. No way is the best way. Depends on person to person.
Hope this helps.
There are several ways to solve that problem.
1) Move the $interval to controller.Then you will have a variable, which holds that data and you can bind it in view
2) You can use AngularJs Events.$rootScope will help you to send signal and catch it wherever you want.
If you want more info about this solutions, you can see it here:
http://www.w3docs.com/snippets/angularjs/bind-value-between-service-and-controller-directive.html

Updating angular.js service object without extend/copy possible?

I have 2 services and would like to update a variable in the 1st service from the 2nd service.
In a controller, I am setting a scope variable to the getter of the 1st service.
The problem is, the view attached to the controller doesn't update when the service variable changes UNLESS I use angular.extend/copy. It seems like I should just be able to set selectedBuilding below without having to use extend/copy. Am I doing something wrong, or is this how you have to do it?
controller
app.controller('SelectedBuildingCtrl', function($scope, BuildingsService) {
$scope.building = BuildingsService.getSelectedBuilding();
});
service 1
app.factory('BuildingsService', function() {
var buildingsList = [];
var selectedBuilding = {};
// buildingsList populated up here
...
var setSelectedBuilding = function(buildingId) {
angular.extend(selectedBuilding, _.find(
buildingsList, {'building_id': buildingId})
);
};
var getSelectedBuilding = function() {
return selectedBuilding;
};
...
return {
setSelectedBuilding: setSelectedBuilding,
getSelectedBuilding: getSelectedBuilding
}
});
service 2
app.factory('AnotherService', function(BuildingsService) {
...
// something happens, gives me a building id
BuildingsService.setSelectedBuilding(building_id);
...
});
Thanks in advance!
When you execute this code:
$scope.building = BuildingsService.getSelectedBuilding();
$scope.building is copied a reference to the same object in memory as your service's selectedBuilding. When you assign another object to selectedBuilding, the $scope.building still references to the old object. That's why the view is not updated and you have to use angular.copy/extend.
You could try the following solution to avoid this problem if you need to assign new objects to your selectedBuilding:
app.factory('BuildingsService', function() {
var buildingsList = [];
var building = { //create another object to **hang** the reference
selectedBuilding : {}
}
// buildingsList populated up here
...
var setSelectedBuilding = function(buildingId) {
//just assign a new object to building.selectedBuilding
};
var getSelectedBuilding = function() {
return building; //return the building instead of selectedBuilding
};
...
return {
setSelectedBuilding: setSelectedBuilding,
getSelectedBuilding: getSelectedBuilding
}
});
With this solution, you have to update your views to replace $scope.building bindings to $scope.building.selectedBuilding.
In my opinion, I will stick to angular.copy/extend to avoid this unnecessary complexity.
I dont believe you need an extend in your service. You should be able to watch the service directly and respond to the changes:
app.controller('SelectedBuildingCtrl', function($scope, BuildingsService) {
// first function is evaluated on every $digest cycle
$scope.$watch(function(scope){
return BuildingsService.getSelectedBuilding();
// second function is a callback that provides the changes
}, function(newVal, oldVal, scope) {
scope.building = newVal;
}
});
More on $watch: https://code.angularjs.org/1.2.16/docs/api/ng/type/$rootScope.Scope

Dirty checking with shared service between controllers, One way works the other does not?

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.

Categories

Resources