Properly using an Angular MVC setup - javascript

Feel free to clear up any misunderstandings you see here; I'm not a front end guy.
Now, I've read that much of the logic shouldn't exist in the controller, and that it should be put somewhere else. But most places I look that show code don't specify which file it belongs in. On this project that I've inherited I have 4 files that deal with the main functionality:
A controller - autoDeploy.js
A service - autoDeploy.service.js
A module - autoDeploy.module.js
Directives - directives.js
directives.js just contains the templates that I want to inject into the DOM upon the click of a button, the directives will be referenced later.
autoDeploy.module.js does all of the module.config and $stateProvider routing stuff; I don't touch it beyond my initial modification to point to the new page I'm making so it can be properly routed to.
autoDeploy.service.js: In the examples I've seen, the .factory()'s (or .service()) last parameter usually opens up as a function, and all of the functionality in the file happens inside there. Mine isn't like that, it's an iife with the factory being a standalone command followed by what looks like a constructor. Here's what I have:
(function () { //start iife
'use strict';
angular.module('gms.autoDeploy')
.factory('AutoDeployService', ["$http", "$q", "$log", "$cookies", "APP_CONFIGS", "SweetAlert", "$timeout", "GridSettingsService", "APP_USER", AutoDeployService]);
function AutoDeployService($http, $q, $log, $cookies, APP_CONFIGS, $timeout, SweetAlert, GridSettingsService, APP_USER) {
//return service object at bottom of function
function returnOne() {
return "one";
}
var service = {
returnOne: returnOne
};
return service;
} //end AutoDeployService
}()); //end iife
Why did the original developer...
Use as an iife?
Return the service variable after making it a function mapping?
Put all of the functionality in the constructor like function?
My guess to the above 2nd and 3rd answers is that it is the constructor for the service and the controller can some how know it's a usable object because we pass it in as a parameter to the controller as you can see on the top line of the code below. I also don't know much about the parameters for the "constructor", but I can look those up later.
Now for the controller. Unlike the service above, the controller declaration does open up as a function in the parameters of .controller() as I've seen other places. Here it is, simplified (methods with similar functionality are cut out):
angular.module("gms.autoDeploy").controller('AutoDeployController', ['$scope', '$compile', 'AutoDeployService',
function ($scope, $compile, AutoDeployService) {
var vm = this;
init();
function init() {
vm.isInstantiated = true;
vm.data = {
"parameter": []
};
}
// calling a function from the service to show that we can pass
// data from controller to service
vm.change = function () {
vm.message = AutoDeployService.returnOne("not one");
};
//function assigned to button click on the DOM allowing
// the directive to inject the template where the <tib-copy> tag is
vm.addCopy = function (ev, attrs) {
var copy = angular.element(document.createElement('copy'));
var el = $compile(copy)(vm);
angular.element(document.getElementsByTagName("tib-copy")).append(copy);
vm.insertHere = el;
};
// This method extracts data from the following fields dynamically added by the click of a button:
// - TIBCO Server(s)
// - TIBCO Domain(s)
// - TIBCO Application(s)
vm.getTibPromotionData = function () {
// Add all TIBCO servers
var servers = document.getElementsByName("tibServer");
var tibSvrList = [];
for (var i = 0; i < servers.length; i++) {
tibSvrList.push(servers[i].value);
}
// Add all TIBCO domains
var domains = document.getElementsByName("tibDomain");
var tibDomainList = [];
for (i = 0; i < domains.length; i++) {
tibDomainList.push(domains[i].value);
}
// Add all applications
var tibApps = document.getElementsByName("tibApp");
var tibAppList = [];
for (i = 0; i < tibApps.length; i++) {
tibAppList.push(tibApps[i].value);
}
// Add the processed data to the final JSON
vm.data.parameter.push({
"name": "TIBCO_Promotion",
"value": JSON.stringify("[{\"server\":[" + tibSvrList + "]},{\"domain\":[" + tibDomainList + "]},{\"application\":[" + tibAppList + "]}]")
});
};
}
]);
Please, let me know if you see anything in the controller that should belong in the autoDeploy.service.js file. Furthermore, if anyone has experience with this file naming convention, I'd love to hear an explanation as to why there are files named *.service.js and *.module.js, and if the *.service.js file has anything to do with the concept of services and factories, or if it's intended to be conceptual, as if it's just a reference to being the back end services component.

Related

How to DRY up $scope-manipulating code across controllers in angularJS

Pardon if this question is a total blow-off... Just getting warmed-up into the world angularJS.
I have these two controllers: seekerController and wizardController...
Inside the wizardController, I have a chat Scope object, and I have implemented a bunch of functions that are manipulating this chat Scope object.
Going back to the other controller now, ( seekerController ), I discover that I need to have basically a direct replica of this chat Scope object and all the other functions manipulating it as I have inside wizardController
The obvious way is just to copy all these into my other controller, and my work is done under a minute, but then I'll have a lot of repeated stuffs everywhere...
So: I'm looking for a way where I can have this(the code) in a single place, but still be able to have access to this chat Scope object from both controllers, as well as all the other functions working seamlessly.
Update - add code samples:
//seekerController
angular.module('cg.seeker', [])
.controller('SeekerController', ['$scope', 'seekerService', 'timeService', 'chatService', '$stateParams', 'toastr',
function ($scope, seekerService, timeService, chatService, $stateParams, toastr) {
...
// THE CHAT BUSINESS
$scope.chat = { close: true };
chatService.unreadCount(function(count){
$scope.chat.unreadCount = count;
$scope.$apply();
});
chatService.listDialogs( function (dialogList) {
$scope.chat.dialogList = dialogList.items;
$scope.$apply();
} );
$scope.endChat = function () {
$scope.chat.close = true;
}
$scope.chatBox = function (dialogId, occupants_ids) {
$scope.chat.opponentId = getOpponentId(occupants_ids);
chatService.getMessages( dialogId, function (messageList) {
$scope.chat.messages = messageList.items;
$scope.chat.close = false;
$scope.$apply();
});
}
var getOpponentId = function (opponentId) {
if(typeof(opponentId) != 'object') {
return opponentId;
} else {
return opponentId.filter(function(x) { return x != $scope.seeker.chat_user.chat_id_string; })[0];
}
}
$scope.sendMsg = function (opponentId) {
var msg = {
type: 'chat',
body: $scope.chat.msg,
extension: {
save_to_history: 1,
}
};
chatService.sendMsg(opponentId, msg);
$scope.chat.msg = '';
}
...
I now have an exact replica of the above code in a second controller WizardController. Exactly same, with no changes... and even a third controller have some of these, though not all.
The next level of abstraction to angularjs controllers are
Factory
Service
Provider
You could use a service called maybe chatService which could contain the common code. You can inject the service into any controller which needs the common functionality and invoke the methods on the Service.
Do note that you could use any of the above three options even though I have mentioned just Service in the above statement.
EDIT 1:
You could move the common parts of the code from Controller to Service.
For example:- You could move the construction of msg object from controller to chatService. You controller would be simply -
$scope.sendMsg = function (opponentId) {
chatService.sendMsg(opponentId);
$scope.chat.msg = '';
}
And your chatService would be doing the hard-work.
$chatService.sendMsg = function (opponentId) {
var msg = {
type: 'chat',
body: $scope.chat.msg,
extension: {
save_to_history: 1,
}
};
sendMsg(opponentId, msg);
}
After simplifying the Controllers you could revisit to see if you could use only one controller instead of 3 as they seem to be doing similar function.

Dynamically Update Model From Web Socket

Update (High Level Idea/Concept)
To summarize I would like values to be dynamically updated in a controller which is binded to a Web Socket Service, which value change. Below is my attempt of how to solve the problem and the barrier which I am facing.
I would like to update the view model in the controller, each time that the socket pushes information in the service. To mimic the pushing of messages I am just calling a timeout every second, which in turn increase the value of health within the service, which I verified it works in console.
Though the value of the health within controller is never updated, and I am at a loss of of what I am doing wrong. Below are the snippets of relatable code.
This is the controller
(function () {
'use strict';
angular
.module('xxx')
.controller('DashboardController', ['$scope','$timeout', "$uibModal", "dashboardService", "syncService", DashboardController]);
function DashboardController($scope, $timeout, $uibModal, dashboardService, syncService) {
var vm = this;
// VERSIONS
vm.versions = {};
// HEALTH
vm.health= syncService.health;
// JUMP-START
activate();
//////////////
function activate(){
getVersions();
}
function getVersions(){
dashboardService.getVersions()
.then(function(data) {
vm.versions = data;
return vm.versions;
});
}
}
})();
This is the service
(function () {
'use strict';
angular
.module('xxx')
.service('syncService',['$timeout', syncService]);
function syncService($timeout) {
//Init WebSocket
var self = this;
self.health= 0;
updatedStatus();
///////////
....
function updatedStatus(){
$timeout(updatedStatus, 1000);
self.health += 1;
$timeout(angular.noop);
};
}
})();
Update
I would like to know how to make it work without using $scope. Example like in the accepted answer in the following question
thank you
AFAIK, you'll need to put a watch on the service variable. This is what I did to get this to work:
function DashboardController($scope, $timeout, $uibModal, dashboardService, syncService) {
var vm = this;
// VERSIONS
vm.versions = {};
// HEALTH
vm.syncService = syncService;
$scope.$watch('vm.syncService.health', function(newHealth, oldHealth){
vm.health = newHealth;
})
// JUMP-START
activate();
//////////////
function activate(){
getVersions();
}
function getVersions(){
dashboardService.getVersions()
.then(function(data) {
vm.versions = data;
return vm.versions;
});
}
}
I linked a similar question in the comments and in the answers they also talk about using $broadcast if you need more than one controller to be aware of any changes to the variable's value in your service.

Not able to get the content of model in multiple controllers AngularJS

Hope, my question itself, conveys what I am look for.
Will put the words in detail
1. Created the Module.
var ang = angular.module('myApp', []);
I have a controller called controller1, and includes the 'campaign' factory.
//controllerone.js
ang.controller('controller1', function(campaign){
$scope.campaigns = new campaign();
//Here the whole campaign object is displayed with data, refer the Image 1 attached
console.log($scope.campaigns);
});
ang.factory('campaign', function($http){
var campaign = function(){
this.timePeriodList = buildTimePeriodList();
...
...
this.campaignList = [];
};
Campaigns.prototype.fetchCampaigns = function() {
//Some service call to load the data in this.campaignList
};
});
Now trying to call the same campaign factory in the second controller, getting only the object structure, not getting the data.
//controlertwo.js
ang.controller('controller2', function(campaign){
$scope.campaigns = new campaign();
//Here only the campaign object structure is displayed, but no data for campaignList, ref image 2 attached
console.log($scope.campaigns);
});
Since, factory service is a singleton object, I was expecting for same result as I got in controllerone.js,
Image 1:
Image 2:
In angular factory I would suggest a different approach.Try not to attach anything to the Prototype of a object. Instead you can create an object in the scope of your angular factory, attach what you want to it and return it. For example:
ang.factory('campaign', function ($http) {
var campaign = {};
campaign.method1 = function () {
//..
}
campaign.campaignList = [];
//...
campaign.fetchCampaigns = function () {
//Some service call to load the data in this.campaignList
};
});
//Than in your controllers if campaign is injected you can use it that way:
ang.controller('controller2', function (campaign) {
campaign.fetchCampaigns();// This will fill the list and it will remain filled when other controllers use this factory...
console.log(compaign.campaignList);
});
Anything you dont want to be exposed out of the factory simply do not attach it to the campaign object.
Create campaign in a service. Eg-campaignService
Updated code---
var ang = angular.module('myApp', []);
ang.service('campaignService', function($scope){
var campaign = function(){
this.timePeriodList = buildTimePeriodList();
...
...
this.campaignList = [];
}
return campaign;
});
ang.controller("controller1", ["campaignService",
function($scope, $rootScope, campaignService){
$scope.campaigns=campaignService.campaign();
}
]);
ang.controller("controller2", ["campaignService",
function($scope, $rootScope, campaignService){
$scope.campaigns=campaignService.campaign();
console.log($scope.campaigns);
}
]);

AngularJS automatically updating controller data by service

I'm having some basic problems with angular at the moment. I just wrote a service that reads the temperature of an external device in an interval of five seconds. The service saves the new temperature into a variable and exposes it via a return statement. This looks kind of this (simplified code):
angular.service("tempService", ["$interval", function ($interval) {
//revealing module pattern
var m_temp = 0,
requestTemp = function() {//some logic here},
onResponseTemp = function (temp) {
m_temp = temp;
},
//some other private functions and vars ...
foo = bar;
//request new temperture every 5s, calls onResponseTemp after new data got received
$interval(requestTemp, 5000);
return {
getTemp = function(){return m_temp;}
}
}]);
I use a controller to fetch the data from the service like this:
angular.controller("tempCtrl", ["$scope", "tempService", function ($scope, tempService) {
$scope.temp = tempService.getTemp();
}]);
In my view I access it like this:
<div ng-controller="tempCtrl">
<p>{{temp}}</p>
</div>
But I only get 0 and the value never changes. I have tried to implement a custom Pub/Sub pattern so that on a new temperature my service fires an event that my controller is waiting for to update the temperature on the scope. This approach works just fine but I'm not sure if this is the way to go as angular brings data-binding and I thought something this easy had to work by itself ;)
Help is really appreciated.
Please see here http://jsbin.com/wesucefofuyo/1/edit
var app = angular.module('app',[]);
app.service("tempService", ["$interval", function ($interval) {
//revealing module pattern
var m_temp = {
temp:0,
time:null
};
var requestTemp = function() {
m_temp.temp++;
m_temp.time = new Date();
};
var startTemp = function() {
$interval(requestTemp, 3000);
};
return {
startTemp :startTemp,
m_temp:m_temp
};
}]);
app.controller('fCtrl', function($scope,tempService){
$scope.temp = tempService;
$scope.temp.startTemp();
});
You are returning a primitive from your service, if you want to update an primative you need to reftech it. You should return an object, as on object is passed by reference, you get the actual values in your controller.
do this in your service:
return m_temp;
And this in your controller:
$scope.temp = tempService;
and your view will update as soon as the service gets updated.
Does this help you?
i think you should use $interval in controller ot in service
$interval(tempService.getTemp(), 5000);

Maintain model of scope when changing between views in AngularJS

I am learning AngularJS. Let's say I have /view1 using My1Ctrl, and /view2 using My2Ctrl; that can be navigated to using tabs where each view has its own simple, but different form.
How would I make sure that the values entered in the form of view1 are not reset, when a user leaves and then returns to view1 ?
What I mean is, how can the second visit to view1 keep the exact same state of the model as I left it ?
I took a bit of time to work out what is the best way of doing this. I also wanted to keep the state, when the user leaves the page and then presses the back button, to get back to the old page; and not just put all my data into the rootscope.
The final result is to have a service for each controller. In the controller, you just have functions and variables that you dont care about, if they are cleared.
The service for the controller is injected by dependency injection. As services are singletons, their data is not destroyed like the data in the controller.
In the service, I have a model. the model ONLY has data - no functions -. That way it can be converted back and forth from JSON to persist it. I used the html5 localstorage for persistence.
Lastly i used window.onbeforeunload and $rootScope.$broadcast('saveState'); to let all the services know that they should save their state, and $rootScope.$broadcast('restoreState') to let them know to restore their state ( used for when the user leaves the page and presses the back button to return to the page respectively).
Example service called userService for my userController :
app.factory('userService', ['$rootScope', function ($rootScope) {
var service = {
model: {
name: '',
email: ''
},
SaveState: function () {
sessionStorage.userService = angular.toJson(service.model);
},
RestoreState: function () {
service.model = angular.fromJson(sessionStorage.userService);
}
}
$rootScope.$on("savestate", service.SaveState);
$rootScope.$on("restorestate", service.RestoreState);
return service;
}]);
userController example
function userCtrl($scope, userService) {
$scope.user = userService;
}
The view then uses binding like this
<h1>{{user.model.name}}</h1>
And in the app module, within the run function i handle the broadcasting of the saveState and restoreState
$rootScope.$on("$routeChangeStart", function (event, next, current) {
if (sessionStorage.restorestate == "true") {
$rootScope.$broadcast('restorestate'); //let everything know we need to restore state
sessionStorage.restorestate = false;
}
});
//let everthing know that we need to save state now.
window.onbeforeunload = function (event) {
$rootScope.$broadcast('savestate');
};
As i mentioned this took a while to come to this point. It is a very clean way of doing it, but it is a fair bit of engineering to do something that i would suspect is a very common issue when developing in Angular.
I would love to see easier, but as clean ways to handle keeping state across controllers, including when the user leaves and returns to the page.
A bit late for an answer but just updated fiddle with some best practice
jsfiddle
var myApp = angular.module('myApp',[]);
myApp.factory('UserService', function() {
var userService = {};
userService.name = "HI Atul";
userService.ChangeName = function (value) {
userService.name = value;
};
return userService;
});
function MyCtrl($scope, UserService) {
$scope.name = UserService.name;
$scope.updatedname="";
$scope.changeName=function(data){
$scope.updateServiceName(data);
}
$scope.updateServiceName = function(name){
UserService.ChangeName(name);
$scope.name = UserService.name;
}
}
$rootScope is a big global variable, which is fine for one-off things, or small apps.
Use a service if you want to encapsulate your model and/or behavior (and possibly reuse it elsewhere). In addition to the google group post the OP mentioned, see also https://groups.google.com/d/topic/angular/eegk_lB6kVs/discussion.
Angular doesn't really provide what you are looking for out of the box. What i would do to accomplish what you're after is use the following add ons
UI Router & UI Router Extras
These two will provide you with state based routing and sticky states, you can tab between states and all information will be saved as the scope "stays alive" so to speak.
Check the documentation on both as it's pretty straight forward, ui router extras also has a good demonstration of how sticky states works.
I had the same problem, This is what I did:
I have a SPA with multiple views in the same page (without ajax), so this is the code of the module:
var app = angular.module('otisApp', ['chieffancypants.loadingBar', 'ngRoute']);
app.config(['$routeProvider', function($routeProvider){
$routeProvider.when('/:page', {
templateUrl: function(page){return page.page + '.html';},
controller:'otisCtrl'
})
.otherwise({redirectTo:'/otis'});
}]);
I have only one controller for all views, but, the problem is the same as the question, the controller always refresh data, in order to avoid this behavior I did what people suggest above and I created a service for that purpose, then pass it to the controller as follows:
app.factory('otisService', function($http){
var service = {
answers:[],
...
}
return service;
});
app.controller('otisCtrl', ['$scope', '$window', 'otisService', '$routeParams',
function($scope, $window, otisService, $routeParams){
$scope.message = "Hello from page: " + $routeParams.page;
$scope.update = function(answer){
otisService.answers.push(answers);
};
...
}]);
Now I can call the update function from any of my views, pass values and update my model, I haven't no needed to use html5 apis for persistence data (this is in my case, maybe in other cases would be necessary to use html5 apis like localstorage and other stuff).
An alternative to services is to use the value store.
In the base of my app I added this
var agentApp = angular.module('rbAgent', ['ui.router', 'rbApp.tryGoal', 'rbApp.tryGoal.service', 'ui.bootstrap']);
agentApp.value('agentMemory',
{
contextId: '',
sessionId: ''
}
);
...
And then in my controller I just reference the value store. I don't think it holds thing if the user closes the browser.
angular.module('rbAgent')
.controller('AgentGoalListController', ['agentMemory', '$scope', '$rootScope', 'config', '$state', function(agentMemory, $scope, $rootScope, config, $state){
$scope.config = config;
$scope.contextId = agentMemory.contextId;
...
Solution that will work for multiple scopes and multiple variables within those scopes
This service was based off of Anton's answer, but is more extensible and will work across multiple scopes and allows the selection of multiple scope variables in the same scope. It uses the route path to index each scope, and then the scope variable names to index one level deeper.
Create service with this code:
angular.module('restoreScope', []).factory('restoreScope', ['$rootScope', '$route', function ($rootScope, $route) {
var getOrRegisterScopeVariable = function (scope, name, defaultValue, storedScope) {
if (storedScope[name] == null) {
storedScope[name] = defaultValue;
}
scope[name] = storedScope[name];
}
var service = {
GetOrRegisterScopeVariables: function (names, defaultValues) {
var scope = $route.current.locals.$scope;
var storedBaseScope = angular.fromJson(sessionStorage.restoreScope);
if (storedBaseScope == null) {
storedBaseScope = {};
}
// stored scope is indexed by route name
var storedScope = storedBaseScope[$route.current.$$route.originalPath];
if (storedScope == null) {
storedScope = {};
}
if (typeof names === "string") {
getOrRegisterScopeVariable(scope, names, defaultValues, storedScope);
} else if (Array.isArray(names)) {
angular.forEach(names, function (name, i) {
getOrRegisterScopeVariable(scope, name, defaultValues[i], storedScope);
});
} else {
console.error("First argument to GetOrRegisterScopeVariables is not a string or array");
}
// save stored scope back off
storedBaseScope[$route.current.$$route.originalPath] = storedScope;
sessionStorage.restoreScope = angular.toJson(storedBaseScope);
},
SaveState: function () {
// get current scope
var scope = $route.current.locals.$scope;
var storedBaseScope = angular.fromJson(sessionStorage.restoreScope);
// save off scope based on registered indexes
angular.forEach(storedBaseScope[$route.current.$$route.originalPath], function (item, i) {
storedBaseScope[$route.current.$$route.originalPath][i] = scope[i];
});
sessionStorage.restoreScope = angular.toJson(storedBaseScope);
}
}
$rootScope.$on("savestate", service.SaveState);
return service;
}]);
Add this code to your run function in your app module:
$rootScope.$on('$locationChangeStart', function (event, next, current) {
$rootScope.$broadcast('savestate');
});
window.onbeforeunload = function (event) {
$rootScope.$broadcast('savestate');
};
Inject the restoreScope service into your controller (example below):
function My1Ctrl($scope, restoreScope) {
restoreScope.GetOrRegisterScopeVariables([
// scope variable name(s)
'user',
'anotherUser'
],[
// default value(s)
{ name: 'user name', email: 'user#website.com' },
{ name: 'another user name', email: 'anotherUser#website.com' }
]);
}
The above example will initialize $scope.user to the stored value, otherwise will default to the provided value and save that off. If the page is closed, refreshed, or the route is changed, the current values of all registered scope variables will be saved off, and will be restored the next time the route/page is visited.
You can use $locationChangeStart event to store the previous value in $rootScope or in a service. When you come back, just initialize all previously stored values. Here is a quick demo using $rootScope.
var app = angular.module("myApp", ["ngRoute"]);
app.controller("tab1Ctrl", function($scope, $rootScope) {
if ($rootScope.savedScopes) {
for (key in $rootScope.savedScopes) {
$scope[key] = $rootScope.savedScopes[key];
}
}
$scope.$on('$locationChangeStart', function(event, next, current) {
$rootScope.savedScopes = {
name: $scope.name,
age: $scope.age
};
});
});
app.controller("tab2Ctrl", function($scope) {
$scope.language = "English";
});
app.config(function($routeProvider) {
$routeProvider
.when("/", {
template: "<h2>Tab1 content</h2>Name: <input ng-model='name'/><br/><br/>Age: <input type='number' ng-model='age' /><h4 style='color: red'>Fill the details and click on Tab2</h4>",
controller: "tab1Ctrl"
})
.when("/tab2", {
template: "<h2>Tab2 content</h2> My language: {{language}}<h4 style='color: red'>Now go back to Tab1</h4>",
controller: "tab2Ctrl"
});
});
<!DOCTYPE html>
<html>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular-route.js"></script>
<body ng-app="myApp">
Tab1
Tab2
<div ng-view></div>
</body>
</html>

Categories