I am building an node js app within which I have one page on angular js. On one of the pages, all the users are listed down(as hyperlinks) and I click on one of them to access the information about that user.
So, if the first page's URL(i.e. all the users) is /users, I want the second URL to look like /users/:id. So, in a way I want to mimic the REST API format.
The user's info has been found and once the user-id has been found, I m trying to use $location.path(index).
Initially I thought using something like ng-if would help me in rendering using the same page. So, on my .jade file, I have added this condition:
div(ng-show='loadAllUsers()')
$scope.loadAllUsers = function(){
console.log($scope.index == -1)
return $scope.index == -1;
};
$scope.index has been initialized to -1. So, I thought something like this would work when a new id value has been generated. But, the problem is it doesn't seem to reload the whole page or div.. I tried using window.location.href as well. That, even though updated the URL but even that breaks...
So, is there a way on doing this on the same jade file by any chance? If not , then how exactly should I go about this problem?
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope, UserService) {
$scope.userDetails={};
//get the list of users from data base. here i am taking some dummy values.
$scope.users=[{
id:1,
name:"Rahul"
},
{
id:2,
name:"Ajay"
},
{
id:3,
name:"Mohit"
}];
// get user details
$scope.getUserDetails=function(userId){
UserService.get({id:userId},function(userDetails){
$scope.userDetails=userDetails;
});
};
});
// No need to change the page URL. Above function will
// call 'UserService' factory. UserService will communicate
// with REST API with URL pattern 'app/rest/users/:id'.
// ':id' will be replaced by the id passed by above function.
// like 'app/rest/users/2'
angular.module('myApp').factory('UserService',function ($resource) {
return $resource('app/rest/users/:id', {}, {
get':{method:'GET', isArray: false}
});
});
<div ng-app="myApp" ng-controller="myCtrl">
<div>
<table>
<tr>
<th>Id</th>
<th>Name</th>
</tr>
<tr ng-repeat="user in users">
<td>{{user.id}}</td>
<td ng-click="getUserDetails(user.id)">{{user.name}}</td>
<tr>
</table>
</div>
<div>
<h3>User Details</h3>
id : userDetails.id <br/>
name : userDetails.name <br/>
// other details
</div>
</div>
Related
I've been trying to solve this for hours, and have tried to find a working solution on stack overflow and other sites, but none worked so far.
The Issue
I am building a travelogue web app that allows users to log and view their journeys (e.g. a road trip). At the moment I am implementing the feature that lets users view a particular journey in a separate view which they have selected from a list of journeys. I pass down the id of the selected journey and retrieve an Object from MongoDB. I implemented this using POST. It works in that the _id of the selected journey is passed in the request, then used to identify the document with Model.findById - then the response yields the data. The data is bound to $scope.selection.
But while $scope.selection contains the data (when logged to console), I cannot seem to bind it to the view (called view_journey). Meaning, whenever I want to access, e.g. selection.name in my view_journey.html, the expression or ng-bind is left empty.
app.js
$scope.viewJourneybyId = function(id) {
var selectOne = { _id : id };
$http.post('http://localhost:8080/view_journey', selectOne).
success(function(data) {
$scope.selection = data;
$scope.$apply();
console.log("POST found the right Journey");
console.log($scope.selection);
}).error(function(data) {
console.error("POST encountered an error");
})
}
server.js
app.post("/view_journey", function(request, response, next) {
Journeys.findById(request.body._id, function(error, selection) {
if (error)
response.send(error)
response.json({ message: 'Journey found!', selection });
});
});
index.html
<tr ng-repeat="journey in journeys">
<td>
<a href="#/view_journey" ng-click="viewJourneybyId(journey._id)">
{{journey.name}}</a>
</td>
<td>...</td>
</tr>
view_journey.html
<div class="panel panel-default">
<div class="panel-heading">
<h2 ng-bind="selection.name"></h2>
<!-- For Debugging -->
ID <span ng-bind="selection._id">
</div>
<div class="panel-body">
<table class=table>
<caption>{{selection.desc}}</caption>
...
</table>
</div>
</div>
Feedback
This is my first question on stack overflow, so please also tell me if I phrased my question in a way that could be misunderstood, and whether or not I should supply more details, e.g. console output. Thank you ;)
After fiddling with your code I can confirm that when triggering the route you are getting a new instance of the controller that has a new, clean scope. This is the expected behavior with AngularJS.
You can verify this by adding a simple log message as the first line of your controller:
console.log($scope.selected);
You will notice that it always logs out "undefined" because the variable has never been set (within viewJourneyById). If you leave that logging in and test the code you will see the logging fire in viewJourneyById but then immediately the "undefined" as it loads the view_journey.html template into ng-view and creates the new instance of mainCtrl. The presence of the "undefined" after the new view loads shows that the controller function is being executed again on the route change.
There are a couple of ways to address this. First you could create a factory or service, inject it into your controller, and have it store the data for you. That is actually one of the reasons they exist, to share data between controllers.
Factory:
travelogueApp.factory('myFactory',function() {
return {
selected: null,
journeys: []
};
});
Controller:
travelogueApp.controller('mainCtrl', ['$scope','$http','$location','myFactory', function ($scope, $http, $location, myFactory) {
// put it into the scope so the template can see it.
$scope.factory = myFactory;
// do other stuff
$scope.viewJourneybyId = function(id) {
var selectOne = { _id : id };
$http.post('http://localhost:8080/view_journey', selectOne)
.success(function(data) {
$scope.factory.selection = data;
console.log("POST found the right Journey");
console.log($scope.factory.selection);
})
.error(function(data) {
console.error("POST encountered an error");
})
}
}]); // end controller
Template:
<div class="panel panel-default">
<div class="panel-heading">
<h2>{{factory.selection.name}}</h2>
</div>
<div class="panel-body">
<table class=table>
<caption>{{factory.selection.desc}}</caption>
...
</table>
</div>
</div>
More or less something like that. Another way to do it would be to construct the link with the journey id as part of the query string and then in the controller check for the presence of the journey id and if you find one, look up the journey. This would be a case of firing the route, loading a new instance of the controller and then loading the data once you're on the view_journey page. You can search for query string parameters in the controller like this:
var journey_id = $location.search().id;
Either way works. The factory/service method allows you to minimize web service calls over time by storing some data. However, then you have to start considering data management so you don't have stale data in your app. The query string way would be your quickest way to solve the problem but means that every route transition is going to be waiting a web service call, even if you are just going back and forth between the same two pages.
I am pretty new to angular and I am working on a data entry web page. The web page has three tabs. Vendor, Products and Types. I started working on the Types tab first. I'd be happy if I could just display the results of a GET request to my Rest API. My Rest API works:
# curl http://192.168.1.115:8080/type
[
{"_id":"56415e7703aba26400fcdb67","type":"Skiing","__v":0},
{"_id":"56417a8503aba26400fcdb68","type":"Bannana","__v":0},
{"_id":"56417a8d03aba26400fcdb69","type":"Carrot","__v":0},
{"_id":"56417a9603aba26400fcdb6a","type":"Beer","__v":0}
]
Here's the pertinent part of my html UPDATED I now have st-safe-src=all_typesbut still no joy ...
<div ng-controller="typeCtrl" class="tab-pane" id="types-v">
<p>The number {{3 + 4}}.</p>
<p>message is {{message}}</p>
<table st-table="displayedCollection" st-safe-src="all_types" class="table table-striped">
<tbody>
<tr ng-repeat="x in displayedCollection">
<td>{{x.type}}</td>
</tr>
</tbody>
</table>
</div> <!-- end Types Tab -->
... and here is my typeCtrl.js ...
app.controller("typeCtrl", function($scope,$http) {
$scope.type_to_look_for = "";
$scope.message = "this is the message. (from typeCtrl)";
$scope.itemsByPage=15;
$scope.all_types = function () {
$http.get("http://192.168.1.115:8080/type").then(function(response) {
console.log(response);
console.log(response.data);
return response.data;
});
}
});
... but when I click on the Types tab my data does not display. I looked developer console and I do not even see the GET request kickoff. And my web page looks like this ...
... what am I doing wrong?
There is nothing that calls all_types. Run http.get and assign the response to all_types
app.controller("typeCtrl", function($scope,$http) {
$scope.type_to_look_for = "";
$scope.message = "this is the message. (from typeCtrl)";
$scope.itemsByPage=15;
$http.get("http://192.168.1.115:8080/type").then(function(response) {
$scope.all_types = response;
});
}
});
My understanding is that you want a get request to be fired whenever you click on the Types tab, right? If so, just use ng-click to call your all_types function as follows:
<div ng-controller="typeCtrl" ng-click="all_types()" class="tab-pane" id="types-v" >
Also, you do not need to return response.data in your controller. Just assign the data to a scope object and use it in the template.
And finally, I would suggest wrapping all your ajax calls in factories and then inject those factories in your controllers.
Here is your code
<div ng-controller="typeCtrl" class="tab-pane" id="types-v">
<p>The number {{3 + 4}}.</p>
<p>message is {{message}}</p>
<table st-table="types" class="table table-striped"><!-- Do not need st-safe-src -->
<tbody>
<tr ng-repeat="x in types"><!-- Use the Collection name as types-->
<td>{{x.type}}</td>
</tr>
</tbody>
</table>
</div>
Controller Code
app.controller('typeCtrl', function($scope, $http) {
$scope.type_to_look_for = "";
$scope.message = "this is the message. (from typeCtrl)";
$scope.itemsByPage=15;
$http.get("http://192.168.1.115:8080/type").then(function(response) {
console.log(response.data);
$scope.types = response.data;
});
});
Here is working the plunker
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.
First off, I read the plethora of other questions and answers regarding ng-click, ng-repeat, and child and parent scopes (especially this excellent one.)
I think my problem is new.
I'm trying to call a function using ng-click within a table. The app allows for the sorting of Soundcloud likes. The problem is that when I try to call the ng click function using new data, it still tries to call the function using the old data. Let me explain better with the example:
Controller:
function TopListCtrl($scope, $http) {
$scope.sc_user = 'coolrivers';
$scope.getData = function(sc_user) {
var url = 'http://api.soundcloud.com/users/'+ $scope.sc_user +'/favorites.json?client_id=0553ef1b721e4783feda4f4fe6611d04&limit=200&linked_partitioning=1&callback=JSON_CALLBACK';
$http.jsonp(url).success(function(data) {
$scope.likes = data;
$scope.sortField = 'like.title';
$scope.reverse = true;
});
}
$scope.getData();
$scope.alertme = function(permalink) {
alert(permalink);
};
}
HTML
<div id="topelems">
<p id="nowsorting">Now sorting the Soundcloud likes of <input type=text ng-model="sc_user"><button class="btn-default" ng-click="getData(sc_user);">Sort</button></p>
<p id="search"><input ng-model="query" placeholder="Filter" type="text"/></p>
</div>
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Song</th>
<th>Artist</th>
<th>Likes</th>
<th>Played</th>
<th>Tags</th>
</tr>
</thead>
<tr ng-repeat="like in likes.collection | filter:query | orderBy:sortField:reverse">
<td width="30%"><a href="{{ like.permalink_url }}">{{like.title}}</td>
(Trying to call it here) <td>{{like.user.username}}</td>
<td>{{like.favoritings_count}}</td>
<td>{{like.playback_count}}</td>
<td>{{like.tag_list}}</td>
</tr>
</table>
</div>
</body>
</html>
So I have that function getData. I use it to load the data in using the soundcloud api. I have a form at the top that uses getData to load a new user's Soundcloud data in. I also call the getData function in the controller so that there is an example on the page upon loading.
The problem is when I try to load a new user's data from a <td> I want to be able to click on the user to see and sort their likes.
How do I 'clear' the function or the global namespace (am I even refering to the right thing)? How can I reuse the getData function with a new variable?
Working Jsfiddle for this
In your getData function you have this line:
var url = 'http://api.soundcloud.com/users/'+ $scope.sc_user +'/favorites.json...
but you are passing in the variable sc_user to your getData function and should be using it like this (no $scope):
var url = 'http://api.soundcloud.com/users/'+ sc_user +'/favorites.json...
That being said... your initial data load fails because you are calling:
$scope.getData();
and not:
$scope.getData($scope.sc_user);
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