Refresh data on click outside controller in angular - angularjs

I am trying to grasp the idea behind angular and ran into my first obstacle involving accessing data from outside the scope (the app?)
Here is a very simple example of what I'm trying to do:
HTML:
<div class=logo>
<a href='/refresh'>Refresh</a>
</div>
<div ng-app ng-controller="threadslist">
<div class='thread_list_header'>
<table class='thread_list_table'>
<tr class='table_header'>
<!--1--><td class='table_c1'></td>
<!--2--><td class='table_c2'>{{name}}</td>
<!--3--><td class='table_c3'>Cliq</td>
<!--4--><td class='table_c4'>Last Poster</td>
<!--5--><td class='table_c5'><a class="clickme">Refresh</a></td>
</tr></table>
</div>
<table class='thread_list_table' >
<tr class="thread_list_row" ng-repeat="user in users">
<!--1--><td class='table_options table_c1'></td>
<!--2--><td class='table_subject table_c2' title="">{{user.subject}}</td>
<!--3--><td class='table_cliq table_c3'></td>
<!--4--><td class='table_last table_c4'><span class="thread_username"><a href=#>{{user.username}}</a></span></td>
<!--5--><td class='table_reply table_c5'><abbr class="thread_timestamp timeago" title=""></abbr></td>
</tr>
</table>
</div>
JS:
function threadslist($scope, $http) {
$scope.name = 'Ricky';
// Initialising the variable.
$scope.users = [];
$http({
url: '/cliqforum/ajax/ang_thread',
method: "POST",
}).success(function (data) {
console.log(data);
$scope.users = data;
});
// Getting the list of users through ajax call.
$('.table_header').on('click', '.clickme', function(){
$http.get('/cliqforum/ajax/ang_thread').success(function(data){
$scope.users = data;
});
});
}
This is the part I can't figure out. My logo is supposed to clear whatever filter is on the current 'user' data. However, it sits outside the scope (and I imagine I shouldn't expand the scope to be the entire page?)
I have read something about scope.$spply but can't quite figure out what I'm supposed to do:
$('.logo').on('click', 'a', function() {
scope.$apply(function () {
$scope.users = data;
});
});
It's not quite necessary that I do it THIS way...I would just like to do what is correct!
Thanks!

and I imagine I shouldn't expand the scope to be the entire page?
Why not? That's definitely the way to do it. Just include the logo into the scope and you can then access it from your application, and use ng-click to add a click handler.
In fact, you should avoid using jQuery click handlers within your application. You could transform your JavaScript like so:
$scope.tableHeaderClick = function() {
$http.get('/cliqforum/ajax/ang_thread').success(function(data){
$scope.users = data;
});
});
Then update the HTML like so:
<tr class='table_header' ng-click="tableHeaderClick()">

it is an angular anti-pattern to include DOM elements in controller. you want to use the ng-click directive to respond to click events
see this plnkr:
http://plnkr.co/edit/KRyvifRYm5SMpbVvWNfc?p=preview

Related

AngularJS Communication between ng-repeat and controller

I've just started using AngularJS, and as a project I decided to build a simple app for managing bookmarks. I want to be able to maintain a list of bookmarks and add/remove items. I'm using Django with Django REST framework, and Angular.
So far I've written a service to grab the bookmarks from the database, and I can print them to the console from my controller, but ng-repeat doesn't seem to be seeing them.
Here's my code for the service:
.factory('BookmarkService', ["$http", function ($http) {
var api_url = "/api/bookmarks/";
return {
list: function () {
return $http.get(api_url).then(function (response) {
return response.data
})
}
}
}]);
And for the controller:
.controller("ListController",
["$scope", "BookmarkService", function ($scope, BookmarkService) {
$scope.bookmarks = BookmarkService.list();
console.log($scope.bookmarks);
}]);
And here's the HTML:
<div ng-controller="ListController as listCtrl">
<md-card>
<md-card-content>
<h2>Bookmarks</h2>
<md-list>
<md-list-item ng-repeat="bookmark in listCtrl.bookmarks">
<md-item-content>
<div class="md-tile-content">
<p>{[{ bookmark.name }]} - {[{ bookmark.url }]}</p> // used interpolateProvider to allow "{[{" instead of "{{"
</div>
<md-divider ng-if="!$last"></md-divider>
</md-item-content>
</md-list-item>
</md-list>
</md-card-content>
</md-card>
</div>
When I print to the console from the controller I can see a promise object but ng-repeat isn't repeating:
image of promise object
I'd really appreciate if someone could help me to find my mistake and to understand why it is happening. I'm still not entirely comfortable with how all these parts fit together.
Thanks for your time!
There's two problems that I see with the code in question.
The first is that using the controller as syntax (ng-controller="ListController as listCtrl") requires properties to be bound to the controller instance and not to the scope if you address them using the controller name. In your case,
.controller("ListController",
["BookmarkService", function (BookmarkService) {
this.bookmarks = BookmarkService.list();
console.log(this.bookmarks);
}]);
The second is that you are assigning a promise to your $scope.bookmarks property. The repeater is expecting an array of objects to iterate over. You really want to assign the value resolved by the promise to $scope.bookmarks.
Instead of this
$scope.bookmarks = BookmarkService.list();
Do this
BookmarkService.list().then(function(result){
this.bookmarks = result;
});
The final version of your controller should look something like this
.controller("ListController",
["BookmarkService", function (BookmarkService) {
this.bookmarks = BookmarkService.list();
console.log(this.bookmarks);
}]);
This is simple. Ng-repeat is not working with promises. So, you can go with two ways:
BookmarkService.list().then(function(responce){
$scope.bookmarks = responce.data;
});
Another way is to create own repiter ^)
Ng-repeat doc

Angular $scope not getting variables

I have a simple html file that make make search on Algolia and returns result. I can console the result but can not access $scope.users from view. How can I grab that $scope.users in view.
here is my app.js file
var myApp = angular.module('myApp', []);
myApp.controller('usersController', function($scope) {
$scope.users = [];
var client = algoliasearch('my_app_id', 'my_app_key');
var index = client.initIndex('getstarted_actors');
index.search('john', function searchDone(err, content) {
$scope.users.push(content.hits);
console.log(content.hits);
});
});
Here is my html view file
<div class="results" ng-controller="usersController">
<div ng-repeat="user in users">
<h3>{{ user.name }}</h3>
</div>
</div>
note: ng-app="myApp" attribute given in html tag.
It's most likely because your index.search call isn't triggering an Angular $digest cycle - manually trigger one with either $apply or a $timeout
index.search('john', function searchDone(err, content) {
$scope.users.push(content.hits);
$scope.$apply();
});
The $apply() could throw $digest already in progress errors - another way with a $timeout
myApp.controller('usersController', function($scope, $timeout) {
index.search('john', function searchDone(err, content) {
$timeout(function() {
$scope.users.push(content.hits);
});
});
});
try calling $scope.$apply() to update your bindings
index.search('john', function searchDone(err, content) {
$scope.users.push(content.hits);
console.log(content.hits);
$scope.$apply();
});
algoliasearch doesn't look like it's an injected service and therefore not native to the angular framework. Try calling $scope.$apply().

Angularjs how can I reload the content

I have this code that loads the content when the page load,
Now I want to know how to reload the content by clicking the button.
Can you show me how to do it with example please?
Javascript code:
.controller('InterNewsCtrl', function($scope, NewsService) {
$scope.events = [];
$scope.getData = function() {
NewsService.getAll().then(function (response) {
$scope.events = response;
}), function (error) {
}
};
$scope.getData(); // load initial content.
})
Html code:
<ons-toolbar fixed-style>
<div class="left">
<ons-back-button>Voltar</ons-back-button>
</div>
<div class="right">
<ons-toolbar-button><ons-icon icon="ion-android-refresh"></ons-icon></ons-toolbar-button>
</div>
<div class="center">Internacional</div>
</ons-toolbar>
I think you're asking how to just retrieve new events from the backend. If that's correct, you don't need to reload the entire page.
You already have a function called getData which goes and retrieves you data via your service. Assuming your service doesn't cache the data, just call getData from your button:
<ons-toolbar-button ng-click="getData()"><ons-icon icon="ion-android-refresh"></ons-icon></ons-toolbar-button>
P.S. if you do explicitly have the cache set to true in your service, you can remove the cached data with $cacheFactory.get('$http').removeAll();.
For reloading same page in angular Js without post back
first remove that url from template cache if you call $route.reload() without removing it from $templateCache it will get it from cache and it will not get latest content
Try following code
$scope.getdata=function()
{
var currentPageTemplate = $route.current.templateUrl;
$templateCache.remove(currentPageTemplate);
$route.reload();
}
and Call it as following
<input type="button" ng-click="getdata();" value ="refresh"/>
Hope this will help
Please reffer this

Nested scopes in AngularJS without nested DOM elements? Creating a view/activity "stack"

I'm developing a simple CRUD application to view end edit generic objects/rows in a database. Each type of CRUD-related action (browse, view, create, edit, delete, search...) has a corresponding type of view. Instances of that view would be something like "browse table A" or "edit row 45 of table B".
Views have actions that can create a child view, for example the user may be browsing a table and click "edit" on a particular row. This makes the views a stack, with only the topmost view actually displayed to the user. A view is pushed onto the stack when a new action is triggered, and popped when that action completes or the user cancels.
Right now my code looks something like this:
app.js:
angular.module('MyApp', [])
.controller('AppController', function($scope) {
var viewStack = [];
$scope.currentView = function() {
return viewStack[viewStack.length-1];
};
$scope.pushView = function(view) {
viewStack.push(view);
};
$scope.popView= function() {
viewStack.pop();
};
$scope.pushBrowseView = function(table) {
var view = {
type: "browse",
table: table,
rows: [],
refresh: function() {
// Load data into view.rows via AJAX
},
// ...
};
view.refresh();
$scope.pushView(view);
};
$scope.pushCreateView = function(table) {
var view = {
type: "create",
table: table,
newRow: {},
// ...
};
$scope.pushView(view);
};
$scope.pushEditView = function(table, row) {
var view = {
type: "edit",
table: table,
row: row,
// ...
};
$scope.pushView(view);
};
// More view types...
})
.controller('BrowseController', function($scope) {
$scope.create = function() {
$scope.pushCreateView($scope.currentView().table);
};
$scope.edit = function(row) {
$scope.pushEditView($scope.currentView().table, row);
};
})
.controller('CreateController', function($scope) {
$scope.submit = function() {
if(newRow.isValid()) {
// POST to server
window.alert('Row submitted');
$scope.popView();
} else {
window.alert('Not valid');
};
})
.controller('EditController', function($scope) {
// Similar to CreateController...
})
// More controllers for other view types
page.html:
<body ng-app="MyApp" ng-controller="AppController">
<!-- Stuff -->
<button ng-click="popView()">Back</button>
<!-- More Stuff -->
<div id="theview" ng-switch="currentView().type">
<div ng-switch-when="browse" ng-controller="BrowseController">
<button ng-click="create()">New Row</button>
<table>
<!-- header goes here -->
<tr ng-repeat="row in currentView().rows">
<td><button ng-click="edit(row)">Edit</button></td>
<td ng-repeat="column in currentView().table.columns">
{{ row[column] }}
</td>
</tr>
</table>
</div>
<div ng-switch-when="create" ng-controller="CreateController">
<form>
<div ng-repeat="column in currentView().table.columns">
<label>{{ column }}</label>
<input
name="{{ column }}"
ng-model="currentView().newRow[column]">
</div>
</form>
<button ng-click="submit()">Submit</button>
</div>
<div ng-switch-when="edit" ng-controller="EditController">
<!-- Kinda like "create" -->
</div>
<!-- And the rest of the view types -->
</div>
</body>
It's a lot fancier that that obviously but that's the gist of it. The problem is that there is one root scope for the app, with one child scope for each type of view. Since there could be more than one instance of each type of view on the stack at once, the state of each view needs to be completely stored in an object in viewStack instead of in that view type's $scope.
This doesn't at all seem like the proper way of doing things. It would make sense for there to be one scope per view instance, each one being a child of the scope of the view beneath it on the stack. That way I could have events naturally broadcast from the top view back through the others. I also wouldn't have to prefix every state variable with currentView(). But the only way I can think of doing this is with a recursive directive which potentially creates deeply-nested DOM elements that I don't want.
This seems like a pretty common design pattern. Is there a better way of doing it?
Going by what you are describing, I think each of your views would be better suited to a directive with isolated scope instead of putting each view into a viewStack object on a common scope.
In your example code you are getting the table information from your AppController and storing them on the $scope for your views to use. I can't see how those views are getting to the scope, but assuming they are coming from a web service some where, you could move the storage and fetching of that data to the directive itself. Eg,
app.directive('exampleView', ['myViewService`, function(myViewService) {
return {
restrict: 'E',
scope: {
'myTable': '=',
'myRows' : '=',
'onPopView': '&'
},
templateUrl: 'url/to/specific/view/template.html',
link: function($scope,element, attributes) {
//Logic for processing values and saving back to server/whatever as required
$scope.submit = function() {
if(newRow.isValid()) {
// POST to server
window.alert('Row submitted');
} else {
window.alert('Not valid');
}
};
if($scope.onPopView) {
// Tell parent controller that view is discarded?
$scope.onPopView();
}
}
};
}]);
The onPopView might not be appropriate for what you are doing, but used it as an example for creating a event/callback for parent controller to hook into. This allows you separate responsibilities between the directive and parent controller(s).

Using Angular Promises and Defers effectively

In Angular (and all SPA JS Frameworks), it is assumed that page navigation be extremely quick and "seamless" to the user. The only bottleneck to this speed is the use of API calls to retrieve data from our server. Therefore, it seems reasonable to find a solution where we can render an entire page, except for the physical data being presented, while we wait for our API call to get a response.
I am fairly new to Angular and I am running into a few problems that are keeping me from achieving the above goal and functionality. Let me take you through the 2 problems I came across while learning Angular
Problem 1: API response is too slow for Angular
At first, I was simply calling my API from within the controller. I have multiple tables of data that are going to be populated after I receive my response. Before the response is gotten, my tables have a simple <tr> that contains a .gif "spinner" that represents the loading of data. I have a $scope.gotAPIResponse property that is set to false initially and then after the API responds, it gets set to true.
I use this boolean in order to show and hide the "spinner" and the table data respectably. This causes the HTML to render successfully and everything looks fine to the user. However, a closer look at the JavaScript console and you can see that Angular threw many errors because it was attempting to $compile my templates BEFORE the API call got a response. Errors are never a good thing, so I had to improvise.
Inside the 'LandingController'
function init() {
$scope.gotAPIResponse = false;
// Get Backup data
API.Backups.getAll(function (data) {
console.log(data);
$scope.ActiveCurrentPage = 0;
$scope.InactiveCurrentPage = 0;
$scope.pageSize = 25;
$scope.data = data.result;
$scope.gotAPIResponse = true;
});
}
Here is the HTML template that throws errors.. both native template and pagination directive throws errors
<tbody>
<tr ng-hide="gotAPIResponse"><td colspan="6"><p class="spinner"></p></td></tr>
<tr ng-if="gotAPIResponse" ng-repeat="active in data.ActiveBackups | paginate:ActiveCurrentPage*pageSize | limitTo:pageSize">
<td><a ui-sref="order.overview({ orderId: active.ServiceId })" ng-click="select(active)">{{ active.Customer }}</a></td>
<td>{{ active.LastArchived }}</td>
<td>{{ active.ArchiveSizeGB | number:2 }}</td>
<td>{{ active.NumMailboxes }}</td>
<td>{{ active.ArchivedItems }}</td>
<td><input type="button" class="backup-restore-btn" value="Restore" /></td>
</tr>
</tbody>
</table>
<div pagination backup-type="active"></div>
Problem 2: Using resolve property is blocking everything
I then looked towards the resolve property that is available in the $stateProvider (ui-router) and $routeProvider (basic Angular). This allowed me to defer the instantiation of the Controller until the $promise on my API call was received.
This worked how it should have and I no longer received any errors because the API response was available before the HTML template could be rendered. The problem with this however, was that when you attempted to go to that state or route, the entire view would be unavailable for a split second until the $promise was received. This would appear to "lag" or "block" to the user (which just looks terrible)
Using resolve property
$stateProvider
.state('dashboard', {
url: '/',
controller: 'LandingController',
resolve: {
res: ['API', function(API){
return API.Backups.getAll(function (data) {
return data.result;
}).$promise;
}]
}
})
Then this controller is instantiated after API call, but the time between this is like 1-2 seconds...way too slow!
app.controller('LandingController', ['$scope', 'API', 'res', function ($scope, API, res) {
$scope.data = res.result;
...
Solution: Right in between
In order to solve this, we need a solution that is right in between the 2 that I have already tried. We need to instantiate the controller as quick as possible so $scope.gotAPIResponse can be set and therefore cause the "spinner" and rest of the DOM to show, however, we need to defer just specific parts of the HTML template and other Directives so that Errors are not thrown.
Is there an effective solution to this problem that people are using in production?
Update
In the end, this ended up being a much simpler fix. After some discussion we refactored the view a bit in order to avoid the ng-repeat executing before the ng-if which wasn't the desired behavior.
So instead of this:
<tbody>
<tr ng-if="!gotAPIResponse">
<!-- show spinner -->
</tr>
<tr ng-if="gotAPIResponse" ng-repeat="stuf in stuffs">
<!-- lots o' DOM -->
</tr>
</tbody>
We pulled the ng-if up one level to this:
<tbody ng-if="!gotAPIResponse">
<tr >
<!-- show spinner -->
</tr>
</tbody>
<tbody ng-if="gotAPIResponse">
<tr ng-repeat="stuf in stuffs">
<!-- lots o' DOM -->
</tr>
</tbody>
This way the ng-if isn't being duplicated for each row, and will halt further processing until the condition is satisfied.
Original Answer:
This is actually simple to achieve by using a directive and listening to some route change events on the $rootScope
The idea is to toggle a property on scope that can be used to Show/Hide some element. In this case a simple "Loading..." text, but in your case it could easily be used to show a spinner.
var routeChangeSpinner = function($rootScope){
return {
restrict:'E',
template:"<h1 ng-if='isLoading'>Loading...</h1>",
link:function(scope, elem, attrs){
scope.isLoading = false;
$rootScope.$on('$routeChangeStart', function(){
scope.isLoading = true;
});
$rootScope.$on('$routeChangeSuccess', function(){
scope.isLoading = false;
});
}
};
};
routeChangeSpinner.$inject = ['$rootScope'];
app.directive('routeChangeSpinner', routeChangeSpinner);
Then inside your HTML you would simply do this:
<div class="row">
<!-- I will show up while a route is changing -->
<route-change-spinner></route-change-spinner>
<!-- I will be hidden as long as a route is loading -->
<div ng-if="!isLoading" class="col-lg-12" ng-view=""></div>
</div>
Voila! Simple way to show a loading indicator while changing routes.
Full Punker Example
This is what you can do (example below):
Encapsulate your HTML in a directive
Create data in your controller (which you receive from the server side) and initialize it to an empty or initial value
Pass this data to your directive to have an isolated scope
Now fetch the data in your controller explicitly using the $http service and on resolution of the promise set the data tied to the directive to the data retrieved from the server
The directive will automatically update when the $http service code runs in the controller
Here's an explanation of the example I provided:
The controller initializes data to [1,2] but then calls the function changedata() which calls $http service to get the weather in New York and adds that to the data array. Hence you see "Sky is Clear" (or something else depending on when you run this example) rendered in your directive.
Here's the plnkr showing directive update with $http
// index.html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js#1.2.x" src="https://code.angularjs.org/1.2.20/angular.js" data-semver="1.2.20"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<input type="button" ng-click="changedata()" value="Change"/>
<div myview data="model.data"></div>
</body>
</html>
// app.js
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope, $http) {
$scope.name = 'World';
$scope.model = {
data: ["1", "2"]
};
$scope.changedata = function() {
// updating data explicitly
$scope.model.data = [4, 5, 6];
url = "http://api.openweathermap.org/data/2.5/weather"
return $http.get(url, {
params: {
q: "New York, NY"
}
}).then(function(result) {
console.log("result=");
console.log(result);
if (result !== null) {
$scope.model.data.push(result.data.weather[0].description);
return result.data.weather[0].description;
}
return "Weather not found";
});
};
// updating data implicitly
$scope.changedata();
});
app.directive("myview", function() {
return {
restrict: 'EA',
template: '<table><tr ng-repeat="d in data"><td>{{d}}</td></tr></table>',
scope: {
data: '='
}
};
});
Your first problem of getting errors when the template renders is because data does not exist until after the promise resolves. A simple $scope.data = { ActiveBackups:null } before initiating your API call should solve that. When angular evaluates the ng-repeat expression active in data.ActiveBackups it will no longer be trying to reference the undefined attribute data.
If you have a longer loading request then you can have more control over the UX by not using resolves. Otherwise resolves tend to make for simpler controller code.

Resources