I have two controllers one nested inside another. ParentController has one object which I am using in ChildController as it is directly available in child. Now, in child, I am calling $http service and then updating this object. I think it should update the view.
I tried calling $scope.$apply() promise success function, but I think I don't really understand how to use this one. How to update the DOM without refreshing the page?
I also read about calling $http service inside $scope.$apply(). How to do that?
$scope.addVideo = function (data) {
console.log('scope tutorial', tutorial);
$http.post('/tutorials/' + tutorial.id + '/videos/', $scope.formData)
.then((response) => {
$window.tutorial.videos.push(response.data.video);
window.location = '/tutorials' + tutorial.id;
})
}
Now, In the UI, I am using TutorialsController which takes care of adding Tutorial. I mean it's a form and it only works with form. Then there is VideoController that displays vidoes in this tutorial.
<div ng-controller="TutorialsController">
// add video in this tutorial logic
<ol ng-repeat="vid in tutorial.videos" ng-controller="VideoController">
<li>{{vid.title}}</li><button>Delete</button>
</ol>
</div>
When the video is added by parent, I want to update child and when child VideoController removes a video with a delete button, I want to remove it from the parent scope.
$scope.addVideo = function (data) {
console.log('scope tutorial', tutorial);
$http.post('/tutorials/' + tutorial.id + '/videos/', $scope.formData)
.then((response) => {
̶$̶w̶i̶n̶d̶o̶w̶.̶t̶u̶t̶o̶r̶i̶a̶l̶.̶v̶i̶d̶e̶o̶s̶.̶p̶u̶s̶h̶(̶r̶e̶s̶p̶o̶n̶s̶e̶.̶d̶a̶t̶a̶.̶v̶i̶d̶e̶o̶)̶;̶
$scope.tutorial.videos.push(response.data.video);
̶w̶i̶n̶d̶o̶w̶.̶l̶o̶c̶a̶t̶i̶o̶n̶ ̶=̶ ̶'̶/̶t̶u̶t̶o̶r̶i̶a̶l̶s̶'̶ ̶+̶ ̶t̶u̶t̶o̶r̶i̶a̶l̶.̶i̶d̶;̶
})
}
Considering you don't have full code written, I took some guesses and prepared something for you. You should try NOT to use $scope.$apply unless you are absolutely sure that you need it, most of the time angular will trigger digest cycle for you so there's no need to use $scope.$apply. Main thing is you could use $scope.$emit to emit value of video you want to delete.
If you are not familiar with $emit check this for more details (you could also check $broadcast which will send event from top to bottom - opposite of $emit):
https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$emit
So, in your case put in your VideoController something like:
$scope.deleteVideo = function(video) {
$scope.$emit('deleteVideo', video);
};
And in your TutorialsController you will need to listen to changes similar to this - data parameter will have your video object from child controller:
$scope.$on('deleteVideo', (event, data) => {
$scope.tutorial.videos = $scope.tutorial.videos.filter(video=>!angular.equals(video,data));
});
This above will just delete video from $scope - so you will need to to add your $http logic.
I simplified your video I added someProperty on each video object and title which you use to display title, right now everything is there just to show how you can easily communicate between controllers via $emit/$broadcast.
Here's that simplified example on fiddle: https://jsfiddle.net/pegla/d1rb96us/2/
Also I would advise you to check this Todd Motto's styleguide and start using component based architecture. It's currently best AngularJS style guide online: https://github.com/toddmotto/angularjs-styleguide
Hope it helps.
Cheers!
Related
I am trying to call an API end point once a user clicks a button holding a myNavigator.pushPage() request. However,I can not get the $scope data generated from the $http.get request to be passed to the new page.
If I test using console.log('test'); inside the .success of the $http.get request I successfully get the log info in the console but any data held in $scope.var = 'something'; does not gets passed to the page! Really confused!
$scope.historyDetails = function(id){
var options = {
animation: 'slide',
onTransitionEnd: function() {
$http.get('http://xxx-env.us-east-1.elasticbeanstalk.com/apiget/testresult/testId/'+id).success(function(data) {
$scope.testscore = 'something'; // this is not getting passed to page!
console.log('bahh'); // But I see this in console
});
}
};
myNavigator.pushPage("activity.html", options);
}
Page:
<ons-page ng-controller="HistoryController">
...
<span style="font-size:1.2em">{{testscore}} </span><span style="font-size:0.5em;color:#555"></span>
...
</ons-page>
Yes, that's so because both pages has different controllers, resulting in different scopes. One can not access variables from one scope to another.
Hence one solution in this case can be using rootScope service.
Root Scope is parent scope for all scopes in your angular application.
Hence you can access variable of root scopes from any other scope, provided that you are injecting $rootScope service in that controller.
to know more about rootScope check this link.
Good luck.
Update 1:
check these articles
http://www.dotnet-tricks.com/Tutorial/angularjs/UVDE100914-Understanding-AngularJS-$rootScope-and-$scope.html
https://toddmotto.com/all-about-angulars-emit-broadcast-on-publish-subscribing/
As Yogesh said the reason you're not getting your values is because if you look at $scope.testscore and try to find where is the $scope defined you will see that it's an argument for the controller function (thus it's only for that controller).
However we can see that the controller is attached to the page and you are pushing another page.
So in that case you have several options:
Use the $rootScope service as Yogesh suggested (in that case accept his answer).
Create your own service/factory/etc doing something similar to $rootScope.
(function(){
var historyData = {};
myApp.factory('historyData', function() {
return historyData;
});
})();
Technically you could probably make it more meaningful, but maybe these things are better described in some angular guides.
If you have multiple components sharing the same data then maybe you could just define your controller on a level higher - for example the ons-navigator - that way it will include all the pages. That would be ok only if your app is really small though - it's not recommended for large apps.
If this data is required only in activity.html you could just get it in that page's controller. For example:
myApp.controller('activityController', function($scope, $http) {
$http.get(...).success(function(data) {
$scope.data = data;
});
}
But I guess you would still need to get some id. Anyway it's probably better if you do the request here, now you just need the id, not the data.
You could actually cheat it with the var directive. If you give the activity page <ons-page var="myActivityPage"> then you will be able to access it through the myActivityPage variable.
And the thing you've been searching for - when you do
myNavigator.pushPage("activity.html", options);
actually the options is saved inside the ons-page of activity.html.
So you can do
myNavigator.pushPage("activity.html", {data: {id: 33}, animation: 'slide'});
And in the other controller your id will be myActivityPage.options.data.id.
If you still insist on passing all the data instead of an id - here's a simple example. In the newer versions of the 2.0 beta (I think since beta 6 or 7) all methods pushPage, popPage etc return a promise - which resolve to the ons-page, making things easier.
$scope.historyDetails = function(id){
myNavigator.pushPage("activity.html", {animation: 'slide'}).then(function(page) {
$http.get('...' + id).success(function(data) {
page.options.data = data;
});
});
});
Side note: You may want to close the question which you posted 5 days ago, as it's a duplicate of this one (I must've missed it at that time).
I am using AngularJS 1.3. Assume I have created several routes in my application. But when user hits a specifc route/url & then tries to move to another route/url, I want to fire some event. I do not want to fire this event on every URL change.
So only when user comes out of this url http://localhost:9000/data/55677c/edit, I want to fire one function available in XYZ controller.
Here is my scenario:
I have a page which looks like this:
<div class="well">
<button id='edit-btn' type="button" ng-click='saveContent()'>
<div ng-include="'components/grid/comOne.html'"></div>
</div>
components/grid/comOne.html page contains one grid and it has its own controller which takes care of data management of the grid.
This grid is shown in two pages. One in editable mode and one is non-ediatble mode. While user is in editable mode and try to move out of the page without saving the info, I need to fire an event in order to discard ant changes user has made to the grid data.
Please suggest
If the listening controller is a parent controller you could $emit the event.
Or you could have a common service like this:
angular.module('x').factory('CommonLogic', function(){
var pageChangeListeners = [];
return {
listenToPageChange: listenToPageChange
};
function listenToPageChange(callback){
pageChangeListeners.push(callback);
}
function pageChanged(){
for(var i = 0; i < pageChangeListeners.length; i++){
pageChangeListeners[i]();
}
}
});
then when leaving that url (track that via $routeChangeStart) you can call: commonLogic.pageChanged()
In the controller where you want to take action just:
commonLogic.listenToPageChange(function(){..}).
Obviously this should be improved to avoid duplicate registration of the listener ... etc.
I hope I'm not overcomplicating this. Could you describe your use case in more detail ?
I guess you want to use $routeChangeStart:
$rootScope.$on( "$routeChangeStart", function(event, next, current) {
});
You can put this in the scope of your current controller which might be edit as your url says.
From the docs:
$routeChangeStart
Broadcasted before a route change. At this point the route services starts resolving all of the dependencies needed for the route change to occur. Typically this involves fetching the view template as well as any dependencies defined in resolve route property. Once all of the dependencies are resolved $routeChangeSuccess is fired.
The route change (and the $location change that triggered it) can be prevented by calling preventDefault method of the event. See $rootScope.Scope for more details about event object.
Type:broadcast
Target:root scope
First of all a plunk: http://embed.plnkr.co/C866y3LHCE7QfBGuFQPQ/preview
So here I'm getting a set of posts via Ajax and displaying them using ngRepeat. Then (when the posts are done rendering) I want to scroll the page to a specific post (let's say post №18). Seems simple, but I can't get it working.
The problem seems to be that angular can't find that post after it receives the data from Ajax, so the position() and scrollTop() functions are not working.
You have to wait until after the view has been updated with your new model, use $timeout waiting 0 milliseconds to scroll immediately after the DOM is ready
plunkr
$scope.getPosts = function() {
$http.get(data_url).success(function(data){
$scope.posts = data;
$timeout(function() {
var post = $('#pid_18');
console.log('pid_18', post);
$('body').scrollTop(post[0].offsetTop);
}, 0);
});
I think the problem is that the render is not finish then if you have 2 element it's could be work but not with 1000.
So the best way is to know when the rendering is finish, a related post :
Calling a function when ng-repeat has finished
Having trouble with always rendering google maps in my Ionic app. When I first land on a view from a list of items on the previous view, the map always renders in its complete state. However, if I go back to the previous view and tap a different business, or even the same one, it appears as if the map is only rendering 25% of the complete map. I'm having this issue on both the emulator and on my iPhone.
Example
Code
getData.getBusinesses()
.then(function(data) {
// get businesses data from getData factory
})
.then(function(data) {
// get businesses photo from getData factory
})
.then(function(data) {
// get some other business stuff
})
.then(function() {
// get reviews for current business from separate async call in reviews factory
})
.then(function() {
// instantiate our map
var map = new GoogleMap($scope.business.name, $scope.business.addr1, $scope.business.city, $scope.business.state, $scope.business.zip, $scope.business.lat, $scope.business.long);
map.initialize();
})
.then(function() {
// okay, hide loading icon and show view now
},
function(err) {
// log an error if something goes wrong
});
What doesn't make sense to me is that I'm using this exact code for a website equivalent of the app, yet the maps fully load in the browser every time. The maps also fully load when I do an ionic serve and test the app in Chrome. I did also try returning the map and initializing it in a following promise, but to no avail.
I've also tried using angular google maps, but the same issue is occurring. I think I might want to refactor my gmaps.js (where I'm creating the Google Maps function) into a directive, but I don't know if that will actually fix anything (seeing as angular google maps had the same rendering issue).
I don't think the full code is necessary, but if you need to see more let me know.
EDIT
It seems that wrapping my map call in a setTimeout for 100ms always renders the map now. So I guess the new question is, what's the angular way of doing this?
I'm seeing similar issues with ng-map in Ionic. I have a map inside of a tab view and upon switching tabs away from the map view and back again, I would often see the poorly rendered and greyed out map as you describe above. Two things that I did that may help fix your issue:
Try using $state.go('yourStateHere', {}, {reload: true}); to get back to your view. The reload: true seemed to help re-render the map properly when the map was within the tab's template.
After wrapping the map in my own directive, I found the same thing happening again and wasn't able to fix it with the first suggestion. To fix it this time, I started with #Fernando's suggestion and added his suggested $ionicView.enter event to my directive's controller. When that didn't work, I instead added a simple ng-if="vm.displayMap" directive to the <ng-map> directive and added the following code to add it to the DOM on controller activation and remove it from the DOM right before leaving the view.
function controller($scope) {
vm.displayMap = true;
$scope.$on('$ionicView.beforeLeave', function(){
vm.displayMap = false;
});
}
Hope that helps.
don't use setTimeout on this!
You need to understand that the map is conflicting with the container size or something (example: map is loading while ionic animation is running, like swiping).
Once you understand this, you need to set map after view is completely rendered.
Try this on your controller:
$scope.$on('$ionicView.enter', function(){
var map = new GoogleMap($scope.business.name,
$scope.business.addr1, $scope.business.city,
$scope.business.state, $scope.business.zip,
$scope.business.lat, $scope.business.long);
map.initialize();
});
My single page application loads a home page and I want to display a series of ideas. Each of the ideas is displayed in an animated flash container, with animations displayed to cycle between the ideas.
Ideas are loaded using $http:
$scope.flash = new FlashInterface scope:$scope,location:$location
$http.get("/competition.json")
.success (data) ->
$scope.flash._init data
However, to benefit from history navigation and UX I wish to update the address bar to display the correct url for each idea using $location:
$location.path "/i/#{idea.code}"
$scope.$apply()
I am calling $apply here because this event comes from outwith the AngularJS context ie Flash. I would like for the current controller/view to remain and for the view to not reload. This is very bad because reloading the view results in the whole flash object being thrown away and the preloader cycle beginning again.
I've tried listening for $routeChangeStart to do a preventDefault:
$scope.$on "$routeChangeStart", (ev,next,current) ->
ev.preventDefault()
$scope.$on "$routeChangeSuccess", (ev,current) ->
ev.preventDefault()
but to no avail. The whole thing would be hunky dory if I could figure out a way of overriding the view reload when I change the $location.path.
I'm still very much feeling my way around AngularJS so I'd be glad of any pointers on how to structure the app to achieve my goal!
Instead of updating the path, just update query param with a page number.
set your route to ignore query param changes:
....
$routeProvider.when('/foo', {..., reloadOnSearch: false})
....
and in your app update $location with:
...
$location.search('page', pageNumber);
...
From this blog post:
by default all location changes go through the routing process, which
updates the angular view.
There’s a simple way to short-circuit this, however. Angular watches
for a location change (whether it’s accomplished through typing in the
location bar, clicking a link or setting the location through
$location.path()). When it senses this change, it broadcasts an
event, $locationChangeSuccess, and begins the routing process. What
we do is capture the event and reset the route to what it was
previously.
function MyCtrl($route, $scope) {
var lastRoute = $route.current;
$scope.$on('$locationChangeSuccess', function(event) {
$route.current = lastRoute;
});
}
My solution was to use the $routeChangeStart because that gives you the "next" and "last" routes, you can compare them without the need of an extra variable like on $locationChangeSuccess.
The benefit is being able to access the "params" property on both "next" and "last" routes like next.params.yourproperty when you are using the "/property/value" URL style and of course use $location.url or $location.path to change the URL instead of $location.search() that depends on "?property=value" URL style.
In my case I used it not only for that but also to prevent the route to change is the controller did not change:
$scope.$on('$routeChangeStart',function(e,next,last){
if(next.$$route.controller === last.$$route.controller){
e.preventDefault();
$route.current = last.$$route;
//do whatever you want in here!
}
});
Personally I feel like AngularJS should provide a way to control it, right now they assume that whenever you change the browser's location you want to change the route.
You should be loading $location via Dependency Injection and using the following:
$scope.apply(function () {
$location.path("yourPath");
}
Keep in mind that you should not use hashtags(#) while using $location.path. This is for compability for HTML5 mode.
The $locationChangeSuccess event is a bit of a brute force approach, but I found that checking the path allows us to avoid page reloads when the route path template is unchanged, but reloads the page when switching to a different route template:
var lastRoute = $route.current;
$scope.$on('$locationChangeSuccess', function (event) {
if (lastRoute.$$route.originalPath === $route.current.$$route.originalPath) {
$route.current = lastRoute;
}
});
Adding that code to a particular controller makes the reloading more intelligent.
Edit: While this makes it a bit easier, I ultimately didn't like the complexity of the code I was writing to keep friendly looking URL's. In the end, I just switched to a search parameter and angular handles it much better.
I needed to do this but after fussing around trying to get the $locationChange~ events to get it to work I learned that you can actually do this on the route using resolve.
$routeProvider.when(
'/page',
{
templateUrl : 'partial.html',
controller : 'PageCtrl',
resolve : {
load : ['$q', function($q) {
var defer = $q.defer();
if (/*you only changed the idea thingo*/)
//dont reload the view
defer.reject('');
//otherwise, load the view
else
defer.resolve();
return defer.promise;
}]
}
}
);
With AngularJS V1.7.1, $route adds support for the reloadOnUrl configuration option.
If route /foo/:id has reloadOnUrl = false set, then moving from /foo/id1 to /foo/id2 only broadcasts a $routeUpdate event, and does not reload the view and re-instantiate the controller.