Related
I have a factory, "itemData", that holds a service.
app.factory('itemData', function () {
var itemData = {
Title: 'I should be different',
getTitle: function () {
return itemData.Title;
},
setTitle: function (title) {
return itemData.Title=title;
},
// This is the function to call all of the sets for the larger object
editItem: function (entry)
{
itemData.setTitle(entry);
}
};
return itemData;
});
I have 2 controllers (in different files) associated with 2 separate views.
The first:
// IndexCtrl
app.controller("IndexCtrl", ['$scope','$window','itemData',
function($scope, $window, itemData) {
var entry = "I'm different";
// After a submit is clicked
$scope.bttnClicked = function (entry) {
itemData.editItem(entry);
console.log(itemData.getTitle(); //<--itemData.Title has changed
// moves to page with other controller
$window.location.href = "edit.html";
};
}
]);
and the second, which is not doing what I want:
app.controller("editItemCtrl", ['$scope', '$window', 'itemData',
function ($scope, $window, itemData){
$scope.item = {
"Title": itemData.getTitle(), //<--- Title is "I should be different"
}
}]);
Do you have some kind of router in place in your Angular app? When you change the location href, is it actually causing full page reload?
Services are held in memory and therefore will not maintain state across page reloads.
I consider that the expression ['$scope', '$window', 'itemData',
function ($scope, $window, itemData) force to initialize the dependency injection and for this reason you had two different object.
if you see the doc https://docs.angularjs.org/guide/controller there is a section called Setting up the initial state of a $scope object in this section was described the case of scope but I consider that may be extended at the list of the service in the [...] even in the angular.module(moduleName,[...]) if the [....] are filled the functin return a new module and only for empty square bracket return the current module instance
I hop that this can help you
I have written a custom drop down option selector, all well and good, it has functions to go and get data (from a passed in url) to populate a list.
Now what I want to do is reuse this component but...
When I add it into another part of my application, but use a different data set, it duplicates the data and runs the controllers functions multiple times.
As far as I can understand 1 have two problems, services are singletons so when I run the function to populate some data, because there is only one instance of the service it just adds it to the current data set.
then the other problem is that controllers do have instances, so now there are two of them, its running the functions in each one.
So the easy solution would be to copy the component and call it a different name, while this might fix the problem, if I wanted to reuse it 10 times, that's 10 copies of the same component, not good.
I come from a OOP Java background, so I'm probably trying to use those techniques in a language that doesn't support it ;)
So I know I have to rethink how to do this, but I've hit a bit of a wall, how is it best to approach this?
Here is (hopefully) a JSFiddle that illustrates what I'm running itno
var app = angular.module('myApp',[]);
app.directive('mySelector', function () {
return {
scope: {
mydata: '='
},
restrict: 'EA',
template:'<select ng-model="timePeriodSelection" ng-options="timePeriodOption.name for timePeriodOption in timePeriodOptions"><option value="">Choose time period</option></select>',
controller: function($scope, myService) {
//$scope.name = 'Superhero';
console.log('test',$scope.mydata);
myService.setData($scope.mydata);
$scope.timePeriodOptions = myService.getData();
console.log('test2',myService.getData());
}
};
});
app.factory('myService', function() {
var _data=[];
return {
setData: function(value){
for (var a=0;a<value.length;a++){
_data.push(value[a]);
}
},
getData: function(){
return _data
}
}
});
https://jsfiddle.net/devonCream/ess9d6q6/
I can't show you the code I have for commercial reasons, but imagine what I'm passing in is actually a url and I have a service that gets the data then stores it in the array in the service/factory, each time it runs it just keeps adding them up! The code is a mock up demo.
Something like a custom drop down should be a directive and nothing else. That said, there are a ton of ways you could achieve what you're trying to do with a directive. Checkout the directive walkthroughs, they're really helpful.
In some way or another you'll probably want to have an isolate scope, use a template, and add a link function.
Example where the items are always the same:
angular.module('myApp')
.directive('myDropdown', function () {
return {
scope: {},
template: '' +
'<div class="my-dropdown">' +
'<div class="my-dropdown-item" ng-repeat="item in items">{{item.text}}</div>' +
'</div>',
link: function (scope, element, attrs) {
scope.items = ['Item 1', 'Item 2', 'Item 3'];
}
};
});
Example where you pass the items to each instance:
angular.module('myApp')
.directive('myDropdown', function () {
return {
scope: {
items: '='
},
template: '' +
'<div class="my-dropdown">' +
'<div class="my-dropdown-item" ng-repeat="item in items">{{item.text}}</div>' +
'</div>'
};
});
--UPDATE--
Example where you get the data once in a service:
angular.module('myApp')
.service('dataService', function ($http) {
var items;
$http.get('http://ab.com/dropdown-items')
.then(function (res) {
items = res.data;
});
return {
items: items
};
})
.directive('myDropdown', function (dataService) {
return {
scope: {},
template: '' +
'<div class="my-dropdown">' +
'<div class="my-dropdown-item" ng-repeat="item in items">{{item.text}}</div>' +
'</div>',
link: function (scope, element, attrs) {
scope.items = dataService.items;
}
};
});
So unless someone can tell me differently, the only way I can see to solve problem this to hold the data in the controller, therefore isolating the data to its own controller instance., holding the data anywhere else just causes instance issues.
I've refactored my 'fiddle' to show 2 different data sources (in this example I've used 2 factories as models), that bring the data back into the controller to be processed and then stored.
Normally if I wasn't reusing the component I would put this logic into the factory, but doing so gives me the problem I had to start with.
Also in my 'real' project I check the variable to 'see' what 'instance' have been triggered and call methods from that, all seems a bit clunky but it seems the only reliable way I can get it to work maybe Angular 2 will resolve these issues.
Anyway my link to my jsfiddle
var app = angular.module('myApp',[]);
app.directive('mySelector', function () {
return {
scope: true,
bindToController: {
mydata: '#mydata',
timePeriodOptions: '='
},
controllerAs: 'selCtrl',
restrict: 'EA',
template:'<select ng-model="timePeriodSelection" ng-options="timePeriodOption.name for timePeriodOption in selCtrl.timePeriodOptions"><option value="">Choose time period</option></select>',
controller: 'selCtrl'
};
});
app.controller('selCtrl', function($scope, myService) {
var selCtrl = this;
selCtrl.timePeriodOptions = [];
if (angular.isString(selCtrl.mydata)){
processData();
}
function processData(){
var value = myService.getData(selCtrl.mydata);
for (var a=0;a<value.length;a++){
selCtrl.timePeriodOptions.push(value[a]);
}
};
});
app.factory('myService', function(dataModel1,dataModel2) {
return {
getData: function(model){
var _data = []
if (model === "1"){
_data = dataModel1.getData();
}else{
_data = dataModel2.getData();
}
console.log('data ',_data);
return _data
}
}
});
app.factory('dataModel1', function() {
var _data=[{"name":1,"value":1},{"name":2,"value":2},{"name":3,"value":3}];
return {
getData: function(){
return _data
}
}
});
app.factory('dataModel2', function() {
var _data=[{"name":4,"value":4},{"name":5,"value":5},{"name":6,"value":6}];
return {
getData: function(){
return _data
}
}
});
JQuery to Directive
I want to call a method from the scope of this directive but can't seem to work it out (if possible).
$("my-directive").first().scope().loadData();
Directive Looks Something Like This
I would like to call the loadData function from the directive code below.
app.directive("myDirective", function () {
return {
restrict: "E",
templateUrl: "..."
scope: {},
controller: function ($scope, $element, $attrs) {
var self = this;
$scope.loadData = function () {
...
};
}
};
});
Scope is accessible inside the directive
You can get any child of the element of which directive is applied and get scope of it.
$('my-directive').first().children(":first").scope().loadData()
Strajk's answer is correct!
When Code is Added Dynamically setTimeout Needed
In the following example detail row has a nested directive (random-testees). I get the scope from that to dynamically compile the directive (dynamic and late-bound). The setTimeout is needed because it seems to take a bit before the
var detailRow = e.detailRow;
// Compile the directive for dyanmic content.
angular.element(document).injector().invoke(function ($compile) {
var scope = angular.element(detailRow).scope();
$compile(detailRow)(scope);
});
// Get some data from directive.
var testId = detailRow.find("random-testees").attr("testid");
// Wait, and call method on the directive.
setTimeout(function () {
var randomTesteesScope = $("random-testees").first().children(":first").scope();
randomTesteesScope.loadTestees(this);
}.bind(testId),200);
Not Very Confident About This
This seems a little brittle since I was getting mixed results with a 100 ms timeout sometimes and error when the randomTesteesScope returned undefined.
I have got a page layout with two controllers at the same time, one holds the data displayed as kind of side navigation, based on data stored the browsers local storage and at least one other controller, which is bind to a route and view.
I created this little wire frame graphic below which show the page layout:
The second controller is used for manipulating the local stored data and performs actions like adding a new item or deleting an existing one. My goal is to keep the data in sync, if an item got added or deleted by the 'ManageListCrtl' the side navigation using the 'ListCtrl' should get updated immediately.
I archived this by separating the local storage logic into a service which performs a broadcast when the list got manipulated, each controller listens on this event and updates the scope's list.
This works fine, but I'm not sure if there is the best practice.
Here is a stripped down version of my code containing just the necessary parts:
angular.module('StoredListExample', ['LocalObjectStorage'])
.service('StoredList', function ($rootScope, LocalObjectStorage) {
this.add = function (url, title) {
// local storage add logic
$rootScope.$broadcast('StoredList', list);
};
this.delete = function (id) {
// local storage delete logic
$rootScope.$broadcast('StoredList', list);
};
this.get = function () {
// local storage get logic
};
});
angular.module('StoredListExample')
.controller('ListCtrl', function ($scope, StoredList) {
$scope.list = StoredList.get();
$scope.$on('StoredList', function (event, data) {
$scope.list = data;
});
});
angular.module('StoredListExample')
.controller('ManageListCtrl', function ($scope, $location, StoredList) {
$scope.list = StoredList.get();
$scope.add = function () {
StoredList.add($scope.url, $scope.title);
$location.path('/manage');
};
$scope.delete = function (id) {
StoredList.delete(id);
};
$scope.$on('StoredList', function (event, data) {
$scope.list = data;
});
});
I don't see anything wrong with doing it this way. Your other option of course is to just inject $rootScope into both controllers and pub/sub between them with a $rootScope.$broadcast and a $rootScope.$on.
angular.module('StoredListExample')
.controller('ListCtrl', function ($scope, $rootScope) {
$scope.list = [];
$rootScope.$on('StoredList', function (event, data) {
$scope.list = data;
});
});
angular.module('StoredListExample')
.controller('ManageListCtrl', function ($scope, $rootScope, $location) {
$scope.list = [];
$scope.add = function () {
//psuedo, not sure if this is what you'd be doing...
$scope.list.push({ url: $scope.url, title: $scope.title});
$scope.storedListUpdated();
$location.path('/manage');
};
$scope.delete = function (id) {
var index = $scope.list.indexOf(id);
$scope.list.splice(index, 1);
$scope.storedListUpdated();
};
$scope.storedListUpdated = function () {
$rootScope.$broadcast('StoredList', $scope.list);
};
});
Additionally, you can achieve this in a messy but fun way by having a common parent controller. Whereby you would $emit up a 'StoredListUpdated' event from 'ManageListCtrl' to the parent controller, then the parent controller would $broadcast the same event down to the 'ListCtrl'. This would allow you to avoid using $rootScope, but it would get pretty messy in terms of readability as you add more events in this way.
It is always a better practice to use a common service that is a singleton for sharing the data between the 2 controllers - just make sure you use only references and not creating a local object in one of the controllers that should actually be in the service
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>