Dynamic routing with Angular - how to work with this JSON? - javascript

I'm currently trying to pull in some JSON from an API with Angular's http method. I can retrieve the data fine, however I can't target specific ID's - to be clear what I'd like to do is have say /#/work/1 and /#/work/2, and be able to use the data relevant to that ID. I have looked into using $routeParams to do this but I have only gotten so far.
My JSON looks exactly like this:
[
{
"id": 1,
"title": "New post",
"body": "oh yeah"
},
{
"id": 2,
"title": "another one",
"body": "test test test",
}
]
and I'm trying to route dynamically to each ID, set up in my config app like so:
myApp.config(function($routeProvider){
$routeProvider.
when('/',{
templateUrl: 'partials/home',
controller: 'mainController'
}).
when('/work/:id', {
templateUrl: 'partials/project',
controller: 'postController'
})
});
This is linked in my view like this, as I'm using ng-repeat all of the data for this view is succesfully being printed - it runs through my JSON fine
<div class = "images" ng-repeat = 'data in projects">
<a href = "#/work/{{ data.id }}">
<p> {{ data.title }} </p>
</a>
</div>
and my controller is set up like this, as you can see I'm calling a function defined in my factory to make the http request here:
var postController = angular.module('postControllers', []);
postControllers.controller('projectController', ['$scope', '$routeParams', 'workPost',
function($scope, $routeParams, workPost) {
$scope.projectID = $routeParams.id;
workPost.getData().success(function(data){
$scope.data = data;
});
}]);
so my factory is set up like so:
var workServices = angular.module('workServices', ['ngResource']);
projectServices.factory('workPost', function($http) {
return {
getData: function() {
return $http({
url: 'somelink/todata.json',
method: 'GET'
})
}
}
});
and finally I'm trying to access the data associated with the ID in my view like so:
<p> {{ data.body }} </p>
This does not work, however repeating it or specifying which item in the array I want (like data[1].title) does
Clicking the href routes me correctly to #/work/id, so my config is fine. I'm just not sure how to get the relevant data (i.e. title, body) associated with only that ID? I was thinking it may be the way my JSON is structured, perhaps I need to nest the parameters under an object for each?
Thanks for looking.

When you do $scope.data = data;, you are setting the entire array of objects into the scope's data variable.
What you want to do is find the object with id matching the route param's id from the array and set that object as scope's data. You can use Array.prototype.find() to do just that.
workPost.getData().success(function(data){
$scope.data = data.find(function (element) {
return element.id === $scope.projectID;
});
});

Your issue is that you are working with only one endpoint which is a static resource file that has all data in it.
You need to loop over that data and filter out the one you need by matching properties in the array of objects with the $routeParams value.
When you find the match assign it to the scope

Related

ng-options not updating when model changed

I have a JSON object that looks like:
[
{
"empName": "John",
"ID": 1
},
{
"empName": "Sam",
"ID": 2
},
{
"empName": "Ben",
"ID": 3
}
]
In the view I want a dropdown where the user selects one of the names. I'm using ng-options to achieve this:
<select ng-options="item as item.empName for item in employees track by item.ID" ng-model="emp.selected">
</select>
If I hard-code the JSON into the variable employees in my controller the select renders. However if I use:
$.getJSON("path to JSON file",function(json){
$scope.employees = json;
});
The select is not populated. I've tried adding in $scope.$apply() to no avail. Any suggestions?
Update 1
Taking on board the answer from Iso I'm still left with the select not binding. For example If my javascript is:
app.controller('Ctrl', ['$scope', '$http', function($scope, $http) {
$scope.employees = [
{
"empName": "John",
"ID": 1
},
];
$http.get(" --PATH TO JSON-- ").then(function (res) {
$scope.employees = res.data;
console.log($scope.employees);
});
}]);
The select is still only populated with the first name 'John' despite the fact that the console.log returns the full object with all three names.
You need to either call $scope.$evalAsync(), or use $http instead of jQuery here (which I would recommend):
$http.get("path to JSON file").then(function (res) {
$scope.employees = res.data;
});
The reason for this is that $http is aware of AngularJS' digest cycle, whereas jQuery is not.
Refrain from using $scope.$apply() (which can fail if a digest cycle is in progress), and $timeout (which is heavy since it provokes a digest cycle of all your scopes).
Wrap your code inside $timeout:
$.getJSON("path to JSON file", function (json) {
$timeout(function () {
$scope.employees = json;
})
});
The call to $apply may fail when the digest cycle is already in progress.
But consider using the $http instead of using jQuery to pull data.
You should use promises with the $q library in Angular.
var getEmployees = function () {
var deferred = $q.defer();
$.getJSON("path to JSON file", function (json) {
deferred.resolve(json);
});
return deferred.promise;
}
getEmployees().then(res){
$scope.employees = res.data;
}
EDIT
If you use $timeout is not really as correct a solution, as it doesn't give you control over the synchronicity. However, using $http to make your calls comes with promises built in, I believe.

AngularJS not showing $scope

I know this question has been answered but given solutions didn't work for me. Everything looks okey, but the contento between curly brackets it's not showing on screen.
<div ng-controller="Hello">
<p>The ID is {{greeting.ip}}</p>
<p>The content is {{greeting.ip}}</p>
</div>
The controller is the folling:
'use strict';
angular.module('sbAdminApp')
.controller('Hello',['$scope','$http', function ($scope, $http) {
$http.get('http://ip.jsontest.com/')
.then(function (data) {
$scope.greeting = data;
console.log($scope.greeting);
});
}]);
And in my app.js, im declaring this:
.state('dashboard.test', {
templateUrl: 'views/test.html',
url: '/test',
controller: 'Hello',
resolve: {
loadMyFiles: function ($ocLazyLoad) {
return $ocLazyLoad.load({
name: 'sbAdminApp',
files: ['scripts/controllers/helloWorld.js']
})
}
}
})
The JSON URL from where I'm getting the data is this.
And here are the pics with the output from the console:
$scope.greeting = data; This line is wrong.
It should be $scope.greeting = data.data;
EDIT:
Because you are assigning your response from the server to that variable and you need only the data returned from the API call. You response object contains stuff like headers and status code and the data property which actually contains your the data you need like id and stuff. If you want to get the id from your response object you can do it like this: greeting.data.id.
I always name my response variable res or response so not to mess it up. Like this:
$http.get('http://ip.jsontest.com/')
.then(function (res) {
$scope.greeting = res.data;
console.log($scope.greeting);
});
}]);
The problem is you're doing the following:
$scope.greeting = data;
But according to your JSON it should be:
$scope.greeting = data.data;
The data variable holds the whole JSON object, and you want the data key from it.

Angular Routing/Dynamic Content

I'm trying to find out what the best practice is for dynamic content in an angular app. I have an array that contains a set of phone numbers and i want to create a page/view base on the country of the phone numbers. So all German phone numbers should be listed under #/app/numbers/germany for example.
The array that holds the phone numbers will be fetched at page load - so it's ready for use and filtration.
Normally I would create a filtration based on the url parameters like ?country=Germany, but I don't suppose this is the right way to do it.
I use a filter for removing duplicates from the view in order to create a list over all countries (which should hold the link to the numbers under each country):
.filter('unique', function(){
return function(collection, keynam){
var output = [];
keys = [];
angular.forEach(collection, function(item){
var key = item[keyname];
if(keys.indexOf(key) === -1) {
keys.push(key);
output.push(item);
}
});
return output;
};
})
So basically I want to know what the best practice in this scenario is - using (dynamic) routes, load data based on URL or something entirely different?
Solution
I've found a solution by using $stateParams from the routing. My dynamic state:
.state('app.single', {
url: '/numbers/:country',
views: {
'menuContent': {
templateUrl: 'templates/country.html',
controller: 'CountryCtrl'
}
}
})
In the controller I assign the $stateParams to a scope variable like this:
.controller('CountryCtrl', function($scope, $stateParams, Numbers) {
//Load Firbase ref and get data
$scope.numbers = Numbers;
$scope.currentCountry = $stateParams.country;
})
And finally in the view I use $scope.currentCountry to filter out the numbers that match the current state/route:
ng-repeat="item in numbers | filter:{Country:currentCountry}"
The great thing about this is that i don't need to load data more than once, but I can rely on controller logic.
If you have a service (PhoneNumberSvc) with the function "getNumbers(country)" that filters the phone numbers by country:
app.module('appName')
.service('PhoneNumberSvc', [
'$http',
function ( $http ) {
this.getNumbers = function ( country ) {
return $http.get('numbers.json')
.then(function ( response ) {
var return_data = [];
angular.forEach(response.data, function ( item ) {
if ( item.country = country ) {
return_data.push(item);
}
});
return return_data;
});
};
}
]);
Then you could do something like this in your config:
$routeProvider.when('/app/numbers/:country', {
templateUrl: 'yourview.html',
controller: 'YourController',
resolve: {
data: function ( $route, PhoneNumberSvc ) {
var country = $route.current.params.country;
return PhoneNumberSvc.getNumbers(country);
}
}
});
Then, in your controller, be sure to inject the parameter "data":
angular.module('appName')
.controller('YourController', [
'$scope',
'data',
function ( $scope, data ) {
$scope.numbers = data;
}
]);
I would load only the data you need:
At first let's declare an angular route
$routeProvider
.when('/numbers/:country',{
templateUrl : '/foo/bar/baz/numbers.html',
controller : 'NumbersController'
})
Then in the NumbersController you can use the country parameter to query the backend and fetch the array of numbers related to the requested country
app.controller("NumbersController",function($scope,$http,$routeParams){
$http({
url: "some_url",
method: "GET",
params: {country: $routeParams.country}
}).success(function(response){
//Handle the data here
}).error(function(response){
//Handle errors here
});
});
The first benefit of this approach is that you don't have to load the entire array, but only what you need.
Furthermore you don't have to do filtering and parsing and other more complex operations.
There is not one single way to solve your problem, but this is a common approach in the angularJS world

AngularJS - Everything depends on my login service?

New to Angular here.
I've created a login service, such that when a user logs in, I store things like user name, email, ID, profile picture, etc. within a hash.
Other controllers, can retrieve this information by adding a dependency for this login service, and then accessing the correct property. Example
function MyController(loginservice) {
this.getUsername = function() {
return loginService.userData.userName;
}
this.getUserId = function() {
return loginService.userData.userId;
}
this.getProfilePictureUrl = function() {
return loginService.userData.profilePictureUrl;
}
}
And this works fine... However pretty much every directive and every component and every page is now depending on the loginservice, because they need that info in some form or another.
I suppose an alternative approach is to make the components themselves agnostic of the loginservice, and simply pass the required data as attributes. E.g.
<my-directive username="myController.getUsername()" userId="myController.getUserId()">
</my-directive>
<my-profile picturePath="myControllere.getProfilePicUrl()" username="myController.getUsername()" userId="myController.getUserId()">
</my-directive>
However, this seems a bit overkill.
Any thoughts here?
You are really overcomplicating things by making functions and element attributes for each property of the userData.
If you need the userData in controller just assign the whole object once. Then in view you can add the applicable properties needed to display
In controller or directive:
this.user = loginService.userData
In view:
My Name is {{myController.user.userName}}
<img ng-src="{{myController.user.profilePictureUrl}}">
Or:
<my-directive user="myController.user"></my-directive>
As noted above in comments you can easily inject the user service into the directive also and avoid having to create the same scope item in controller and directive attributes and then in directive scope.
Additionally none of those getter functions you have would be created in the controller, rather they would be in the service so they would also be made available to other components. The structure shown in question is backwards
For example from your current controller code:
this.getUserId = function() {
return loginService.userData.userId;
}
Should really look more like:
this.userId = loginService.getUserId();//method defined in service
I've got a similar setup. I'd recommend using ui-router resolutions, which you can then use to resolve dependencies like user data at the parent, then access in child controllers and views.
There are two key points here:
1) To access 'user' data in child views, you can simply reference the object in parent scope.
2) To access 'user' data in child controllers, you can inject the resolve object.
Here's my setup. This is an example of scenario 1 - accessing data from a child view:
// the root state with core dependencies for injection in child states
.state('root', {
url: "/",
templateUrl: "components/common/nav/nav.html",
controller: "NavigationController as nav",
resolve: {
user: ['user_service', '$state', '$mixpanel',
function(user_service, $state, $mixpanel) {
return user_service.getProfile()
.success(function(response) {
if (response.message) {
$state.go('logout', { message: String(response.message) });
}
if (response.key) {
$mixpanel.people.set({
"$name": response.profile.name,
"$email": response.profile.email
});
}
})
.error(function(response) {
$state.go('logout', { message: String(response.message) });
});
}]
}
})
In my NavigationController, I can define scope to allow child views to access 'user' like so:
function NavigationController($auth, user) {
if ($auth.isAuthenticated()) {
this.user = user.data; // reference 'this' by using 'nav' from 'NavigationController as nav' declaration in the config state - * nav.user is also usable in child views *
}
}
Then you specify the 'root' state as the parent, such as:
.state('welcome', {
parent: "root",
url: "^/welcome?invite_code",
templateUrl: "components/common/welcome/welcome.html",
controller: "WelcomeController as welcome"
})
As for scenario 2, injecting 'user' into a controller, now that the 'welcome' state is a child of the 'root' state, I can inject 'user' into my WelcomeController:
function WelcomeController(user) {
var user_profile = user.data;
}

Angular parse JSON into directive

Basically I am using angular routing for my pages and its respective template. Every pages has form in which it has more HTML fields(input/select/textarea). I am trying to create reusable Directive to create html field like below
app.directive('field',function(){
return {
restrict : "E",
scope : {
},
link : function(scope,elem,attr){
var content;
scope.Options = {
id: scope.$id+'_'+elem.attr('id'),
label : elem.attr('label'),
placeholder : elem.attr("placeholder"),
};
scope.contentUrl = 'templates/fields/'+elem.attr('template')+'.html';
},
template: '<div ng-include="contentUrl"></div>'
}
})
Now from my respective page HTML, I will use this directive. For example from customer page HTML has,
<field id="NAME" template="text" label="First Name" placeholder="Enter First Name"></field>
So far so good. Field is generated as expected. Now I wanted to prepopulate the customer JSON data into directive respective fields.
I tried to create factory service to get JSON data and inject this service to my customer controller like below
Factory service
app.factory('dataService', function($http) {
return {
getCustomerData: function() {
//return the promise directly.
return $http.get('offline/customer.json')
.then(function(result) {
//resolve the promise as the data
return result.data;
});
}
}
});
customerController
app.controller('customerController', ['$scope', 'dataService',function($scope,dataService) {
dataService.getCustomerData();//need to parse this data into field directive
}]);
Am I doing right way? How do we parse respective page data into their page fields created by directive?
First of all, I think, you need to bind fetched data with controller's scope:
app.controller('customerController', ['$scope', 'dataService',function($scope,dataService) {
dataService.getCustomerData().then(function ( data ) {
$scope.data = data; // let data == { someField: 42 }
};
}]);
And after that, you need to use data from scope into angular's template:
<field id="NAME" template="text" label="First Name" placeholder="Enter First Name">{{someField}}</field>
To prepopulate your fields, you need to use Angular binding i.e ngModel. Using ng-include in your directive is redundant, you can use directly the template attribute in your directive.
I would do it that way :
app.directive('customtext',function() {
return {
restrict:'E',
require:'ngModel',
scope:{
thevalue:'='
},
template:'<input type="text" ng-model="thevalue"/>',
}
});
and use :
<customtext thevalue="name" />
And now you can populate the controller's scope and the bind will be done this way :
app.controller('customerController', ['$scope','dataService',function($scope,dataService) {
var data = dataService.getCustomerData();
$scope.name = data.name;
}]);
You will need to create a directive for each field you want to create.
ps: the JSON that get through $http is automatically converted as an object. You don't need to use JSON.parse.

Categories