Angular $scope losing child data in certain situation - angularjs

I'm having an issue where, when I simply console.log($scope.visitors) my data is fine, but when I console.log($scope.visitors) and then try to open a AngularUI Bootstrap dialog, the $scope.visitors.optionalFields object is empty.
Now, I've spent some time trying to replicate the issue in jsbin and I wasn't able to get it to happen, so it may be difficult to answer; just hoping somebody might have an idea what could be causing it.
So I have an array of objects like this:
$scope.visitors = [
{
company:"one",
optionalFields: {
passportNumber:"ppt",
contactNumber:"tel",
licensePlate:"1234"
},
firstName:"some",
lastName:"guy",
}
]
I ng-repeat through these with visitor in visitors and each one has a button with ng-click="editVisitorDialog(visitor)"
Now, if I make editVisitorDialog like this:
$scope.editVisitorDialog = function (visitor) {
console.log($scope.visitors);
console.log(visitor);
}
Then both $scope.visitors and visitor look good, they have all of their properties.
However, if I simply add to this function (i.e., the log calls are still at the top of the function), each visitor in $scope.visitors will have an empty optionalFields object.
$scope.editVisitorDialog = function (visitor) {
console.log($scope.visitors); // missing the optionalFields items
console.log(visitor); // missing the optionalFields items
var modalInstance = $modal.open({
templateUrl: 'app/checkin/edit_visitor/edit_visitor.html',
controller: EditVisitorController,
resolve: {
visitor: function () {
return visitor;
}
}
});
modalInstance.result.then(function (result) {
console.log(result)
}, function () {
});
}

When you call console.log() with an object it doesn't really log the object. It's rather like an inspection tool where you can see the current state, not the state at the time console.log() was called.
Try console.log(visitor.optionalFields.passportNumber) instead and I guarantee that it will log "ppt" (in your example).
That in turn means that somehwere in the added code the optional fields get lost.

Related

how to refresh data in cache on state reload- angular 1.5 / ui-router

I am working on a task where I have a bunch of widgets on a dashboard. When the user changes the customer on the dashboard the $state is reloaded, and the widgets' position should be saved to a cache, but the data relevant to the widgets should be refreshed. There are two relevant components, a header component where the customer is changed, and the dashboard component where the widgets are. I'd like to accomplish this without touching the header component.
Previously, I wrote a function to remove all data related properties from the cache after saving in the cache, but I think that might not be the right approach and too complicated. what is the best way to accomplish this?
so suppose each widget looks like this:
{
widgetType: chart,
position: someCoordinatesThatStayTheSameOnStateChange,
chart: containsDataThatNeedsToBeRefreshed,
chartData: containsDataThatNeedsToBeRefreshed
}
this is a $resolve'd function in my routes file which exposes $ctrl.widgets in the dashboard controller:
widgets($http, $q, CacheFactory, WidgetsService) {
'ngInject';
let mockWidgetData = WidgetsService.getAllWidgets();
let widgetsCache = CacheFactory.get('widgets');
if (!widgetsCache) {
CacheFactory.createCache('widgets', {
storageMode: 'localStorage',
});
}
return mockWidgetData;
},
relevant places where I save data in a cache in the dashboard controller:
let widgetsCache = CacheFactory.get('widgets');
let widgetsCacheItems = widgetsCache.get('widgets');
gridsterConfig.resizable.stop = function(event, $element, widget) {
widgetsCache.put("widgets", $ctrl.widgets);
//clearDataFromCache()
}
gridsterConfig.draggable.stop = function(event, $element, widget) {
widgetsCache.put("widgets", $ctrl.widgets);
//clearDataFromCache()
}
$ctrl.toggleVisibility = function(widget) {
widget.hidden = !widget.hidden;
widgetsCache.put("widgets", $ctrl.widgets);
//clearDataFromCache()
}
old function I wrote:
function clearDataFromCache() {
let widgetObjectsInCache = widgetsCache.get('widgets');
widgetObjectsInCache.forEach((widget) => {
if (widget.chart) delete widget.chart;
if (widget.chartConfig) delete widget.chartConfig;
})
console.log(widgetObjectsInCache, "THE CACHE AFTER REMOVING ANY DATA RELATED STUFF");
}
You can use resolve function for state to refetch the data you want. Please check https://medium.com/opinionated-angularjs/advanced-routing-and-resolves-a2fcbf874a1c?swoff=true#.bkudisx52

calling controller function from within a promise in external JS

Ionic Tabs, root of tabs HTML has "RootTabCtrl", and "Tab1" (with "Tab1_Ctrl") has a form, other tabs are disabled.
User submits form on Tab1
Tab1 Controller function kicks off.
Controller Function calls an external function (not in controller).
External function triggers, which executes a promise
In promise "results", returned data is processed
If X in returned data is true, trigger "RootTabCtrl" function to enable the other disabled tabs.
I can track console messages triggering every step of the way.
This all works except for this following odd behavior. "RootTabCtrl" doesn't enable the disabled tabs until the user clicks the form submit a second time...even though I see console messages saying it is in (at the end) of the RootTabCtrl function. I see all the same console messages from the first click - but on the 2nd time is when the disabled tabs get enabled again.
If I move step 6 outside of the promise in Step 4, and put it after step 3 (and before the promise), then all the tabs get enabled on the 1st click. However, this is no longer taking into account the value of X to determine if other tabs should be re-enabled or not.
What can I look for, or am not aware of, that would be causing this?
app.js:
.state('tab', {
url: "/tab",
abstract: true,
templateUrl: "templates/tabs.html",
controller: 'TabsCtrl'
})
// Each tab has its own nav history stack:
.state('tab.tab1', {
url: '/map',
views: {
'tab-1': {
templateUrl: 'templates/tab-1.html',
controller: 'tab1Ctrl'
}
}
})
controllers:
.controller('TabsCtrl', function($scope,$rootScope,constants) {
$scope.constants = constants ;
$scope.tabControl = {
disableRides : true,
disableBikes : true,
disableTransit : true
}
var refreshFinalizer = $rootScope.$on('updateTabsRefresh', function (event, data) {
console.log("Refresher 1") ;
$scope.tabControl.disableTab2 = false;
$scope.tabControl.disableTab3 = false ;
console.log("Refresher 2") ;
});
$scope.$on('$destroy', function () {
console.log("Destroy") ;
refreshFinalizer ();
});
})
.controller('tab1Ctrl', function($scope,$rootScope) {
$scope.setInfo= function() {
getGoogle(document.getElementById('form_data').value,0);
}
$scope.enableTabs = function(type) {
console.log("Here enableTabs1") ;
$rootScope.$broadcast('updateTabsRefresh');
console.log("Here enableTabs2") ;
}
})
Tab1 has a form, upon click, it executes $scope.setInfo. Then getGoogle() is in an outside JS function, its a call to google maps, and if specific data X is true, then enable all the other tabs using tab1Ctrl $scope.enableTabs() :
function getGoogle(userInfo,clear) {
console.log("setInfo 1") ;
geoCoder.geocode({'address': userInfo}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
if (results[0]) {
// extra code removed
startPointSet = 1;
// tab1Ctrl html has id of "Tab1"
angular.element(document.getElementById('Tab1')).scope().enableTabs();
/* alternative method, worked the same as previous line but with same problem
var $sBody = angular.element(document.body) ;
var $sRootScope = $sBody.injector().get('$rootScope') ;
$sRootScope.$broadcast('updateTabsRefresh') ;
*/
console.log("setInfo 2") ;
}
} else {
// extra code removed
console.log(response) ;
}
});
}
}
All of the above works...except that when the call to enableTabs (within the google response), even though it correctly calls enableTabs and I can see the console.log messages firing from within enableTabs - the other tabs don't "enable" until the form button is clicked a 2nd time (and then I see all the console messages again). I tried 2 different methods, from within getGoogle(), both worked exactly the same - 1st clicked fired all functions correctly, but tabs did not enable. 2nd click fired all the functions and then the tabs got enabled.
Try this previous answer. Are you using $http calls? If so I have never actually had to do that so it seems like something else might be the root cause.
UPDATE
How about creating an Angular Service for this GetGoogle function call. Then you will still be inside of angular and can inject $rootScope and anything else that you need. You will need to inject this service into your tab1Ctrl (the myGoggleService below). I would also probably just pass the form back on the ng-submit:
Html
ng-submit="setInfo(formNameGoesHere)"
Controller
.controller('tab1Ctrl', function($scope, $rootScope, myGoogleService) {
$scope.setInfo = function(form) {
myGoogleService.getGoogle(form);
}
$scope.enableTabs = function(type) {
console.log("Here enableTabs1");
$rootScope.$broadcast('updateTabsRefresh');
console.log("Here enableTabs2");
}
});
Service: If you haven't created any already you will need to register it in your app.js like any directives you have and also put them in your index.html page like a regular controller.
.service('myGoggleService', [ '$rootScope', function ($rootScope) {
this.getGoogle = function(userInfo,clear) {
console.log("setInfo 1") ;
geoCoder.geocode({'address': userInfo}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
$rootScope.$broadcast("updateTabsRefresh");
} else {
}
});
}
}}]);
I did copy your code from above and then just removed some of the extra stuff just to get the point across. Obviously could not run the code so there might be some mistakes or slight changes needed, but hopefully this will get you close.

Angular UI Router Reload Controller on Back Button Press

I have a route that can have numerous optional query parameters:
$stateProvider.state("directory.search", {
url: '/directory/search?name&email',
templateUrl: 'view.html',
controller: 'controller'
When the user fills the form to search the directory a function in the $scope changes the state causing the controller to reload:
$scope.searchDirectory = function () {
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email
}, { reload: true });
};
In the controller I have a conditional: if($state.params){return data} dictating whether or not my service will be queried.
This works great except if the user clicks the brower's forward and/or back buttons. In both these cases the state (route) changes the query parameters correctly but does not reload the controller.
From what I've read the controller will be reloaded only if the actual route changes. Is there anyway to make this example work only using query parameters or must I use a changing route?
You should listen to the event for succesful page changes, $locationChangeSuccess. Checkout the docs for it https://docs.angularjs.org/api/ng/service/$location.
There is also a similar question answered on so here How to detect browser back button click event using angular?.
When that event fires you could put whatever logic you run on pageload that you need to run when the controller initializes.
Something like:
$rootScope.$on('$locationChangeSuccess', function() {
$scope.searchDirectory()
});
Or better setup like:
var searchDirectory = function () {
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email
}, { reload: true });
$scope.searchDirectory = searchDirectory;
$rootScope.$on('$locationChangeSuccess', function() {
searchDirectory();
});
Using the above, I was able to come up with a solution to my issue:
controller (code snippet):
...var searchDirectory = function (searchParams) {
if (searchParams) {
$scope.Model.Query.name = searchParams.name;
$scope.Model.Query.email = searchParams.email;
}
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email,
}, { reload: true });
};...
$rootScope.$on('$locationChangeSuccess', function () {
//used $location.absUrl() to keep track of query string
//could have used $location.path() if just interested in the portion of the route before query string params
$rootScope.actualLocation = $location.absUrl();
});
$rootScope.$watch(function () { return $location.absUrl(); }, function (newLocation, oldLocation) {
//event fires too often?
//before complex conditional was used the state was being changed too many times causing a saturation of my service
if ($rootScope.actualLocation && $rootScope.actualLocation !== oldLocation && oldLocation !== newLocation) {
searchDirectory($location.search());
}
});
$scope.searchDirectory = searchDirectory;
if ($state.params && Object.keys($state.params).length !== 0)
{..call to service getting data...}
This solution feels more like a traditional framework such as .net web forms where the dev has to perform certain actions based on the state of the page. I think it's worth the compromise of having readable query params in the URL.

AngularJS $location replace() replacing last history entry also

On my AngularJS application I need to save only state changes in the browser history. That's why when I'm changing parameters of $location, I'm using replace() method.
For example, When I'm accessing /page1, it is save in the history. 'centre' parameter is added automatically with replace() so it doesn't add a new history entry:
$location.search('centre', hash).replace();
Every time I move a map, 'centre' changes.
When I go to /page2, the new entry in the history is created. When I move map, 'centre' changes.
The thing is that when I press BACK button, I'm going to /page1 but I need to keep 'centre' the same as it was before, but it changes to what was saved together with history entry of /page1.
How would I fix this issue?
I tried to add:
$window.history.replaceState({}, '', $location.absUrl());
After I do replace(), but didn't work.
I found calling the search & replace inside a 0ms timeout ensures it happens in a separate digest cycle from the main state change and prevents the previous state being replaced.
Something like:
$timeout(function() {
$location.search('centre', hash).replace();
}, 0);
This is how I solved this.
First, we need to save search result as previous and current:
$rootScope.$on('$locationChangeStart', function (event, toURL, fromURL) {
if ($rootScope.search.current) {
$rootScope.search.previous = $rootScope.search.current;
}
$rootScope.search.current = $location.search();
});
Then we need to always change the actual location we are in at the moment.
$rootScope.$on('$locationChangeSuccess', function (event, toURL, fromURL) {
$rootScope.actualLocation = $location.path();
});
And if back of forward button was pressed, center must be changed in the URL (using previous search results) without pushing a new history entry.
$rootScope.$watch(function () { return $location.path() }, function (newLocation, oldLocation) {
if ($rootScope.actualLocation === newLocation) {
$location.search('center', $rootScope.search.previous.center).replace();
}
});
I had the same problem and could only get it working by using the native history api, like so:
window.history.replaceState(hash, null, $location.absUrl());

Accesing javascript variable in angularjs

I am trying to accessing javascript variable and then change it in controller and then use it in route.. but it is showing the same old one.
var userEditid = 0;
app.controller("cn_newuser", function ($scope,$window) {
editUser = function (this_) {
$window.userEditid = this_.firstElementChild.value;
alert(userEditid);
//window.location.path("#/edit_user");
//$window.location.href = "#/edit_user";
}
route
.when("/edit_user", {
templateUrl: "/master/edituser/" + userEditid //userEditid should be 3 or some thing else but is showing 0
})
in abobe route userEditid should be 3 or some thing else but is showing 0
This is because the .when() is evaluated on app instantiation, when this global var (which is really not a great idea anyways) is set to 0, rather than when your controller is run. If you are trying to say, "load the template, but the template name should vary based on a particular variable," then the way you are doing it isn't going to work.
I would do 2 things here:
Save your variable in a service, so you don't play with global vars
Have a master template, which uses an ng-include, which has the template determined by that var
Variable in a service:
app.controller("cn_newuser", function ($scope,UserIdService) {
$scope.editUser = function (this_) {
UserIdService.userEditid = this_.firstElementChild.value;
$location.path('/edit_user');
}
});
also note that I changed it to use $location.path()
Master template:
.when("/edit_user", {
templateUrl: "/master/edituser.html", controller: 'EditUser'
});
EditUser controller:
app.controller("EditUser", function ($scope,UserIdService) {
$scope.userTemplate = '/master/edituser/'+UserIdService.userEditId;
});
And then edituser.html would be:
<div ng-include="userTemplate"></div>
Of course, I would ask why you would want a separate template per user, rather than a single template that is dynamically modified by Angular, but that is not what you asked.
EDITED:
Service would be something like
app.factory('UserIdService',function() {
return {
userEditId: null
}
});
As simple as that.

Resources