Resolving object dependencies on client using Restangular - angularjs

I have problems with restoring my entity relations at the (AngularJS) client after retrieving them via REST using Restangular. I searched a lot, but could not find a clean solution. I have some years of programming experience, but I'm quite new to the AngularJS ecosphere and the REST paradigm.
The task
At my Spring Data backend I have two entities, Article and ArticleGroup, in a 1:n relationship. The resulting REST json (via Spring Data Rest) looks like this:
Article
{
"_embedded": {
"articles": [
{
"id": 1,
"name": "A Drink",
"price": 2.90,
"created": null,
"modified": null,
"active": false,
"displayOrder": 0,
"_links": {
"self": {
"href": "http://localhost:8080/articles/1"
},
"articleGroup": {
"href": "http://localhost:8080/articles/1/articleGroup"
}
}
},
...
ArticleGroup
{
"_embedded": {
"articleGroups": [
{
"id": 1,
"name": "Drinks",
"printer": null,
"_links": {
"self": {
"href": "http://localhost:8080/articleGroups/1"
},
"articles": {
"href": "http://localhost:8080/articleGroups/1/articles"
}
}
},
...
Now I want to display a tabgroup containing the articleGroup.name as the tab's label and a table of the articles in this articleGroup as its content:
<tabset>
<tab ng-repeat="group in articleGroups" heading="{{group.name}}">
<table class="table">
<tr ng-repeat="article in group.articles">
<td ng-click="increase(article)">{{article.name}}</td>
<td>{{article.price | currency }}</td>
...
</tr>
</table>
</tab>
</tabset>
I retrieve the articleGroups easily in my AngularJS controller using Restangular:
Restangular.all('articleGroups').getList()
.then(function(groups) {
$scope.articleGroups = groups;
});
This works fine and the tabs show up nicely. Now I can do so for the articles as well, but here I come upon the problem which I'm dealing with for days now.
The problem
How can I filter the articles for their articleGroup so every portion of articles appears in the right tab? What is the right expression in the ng-repeat above where I just put "group.articles" as a placeholder now?
This sounds very easy, but my problem is that I have no operational identification of an Article's articleGroup to filter the right articles in the ng-repeat for each tab. What I tried:
using the self.href as an id à la article._links.articleGroup.href == articleGroup._links.self.href, but this doesn't work as you can see in the JSON above those two links are set up differently
adding (database) ids to the JSON, but of course an article does not contain the id of its articleGroup, but only the link relation
looping through articleGroups and retrieving each group's articles into an array with the articleGroup.id as the key, but I can't get this to work out as I either get undefined errors by JavaScript or infinite loops
Help
Before I continue fiddling along for hours, it would be great if you could give me hints on which direction to take and what a "clean" and methodologically sound approach to the task would be. How can I bring articles and their corresponding group back togehter?
It seems like such an easy (and frequent) problem, but I tried for hours and days to get it running and fear I'm missing something. I looked at many examples but none of them helped me to have a breakthrough.
I'm happy to provide any further information as needed. Thanks a lot in advance!

Well, I solved it. It turned out that I got lost in the handling of asynchronous requests, especially between having a data object itself and a promise for a request, i.e. placeholders which provide access to the data as soon as it is available - very well explained in this post.
Restangular always returns promises. So I came up with the following solution which works exactly as I wanted to:
For the controller code:
Restangular.all('articleGroups').getList()
.then(function(groups) {
$scope.articleGroups = groups;
for(i=0;i<groups.length;i++) {
$scope.articles[groups[i].id] = groups[i].getList("articles")
}
});
So when the articleGroups arrive from the server, I walk through all of them and put them into the array $scope.articles with the articleGroup.id as key. Notice that group[i].getList("articles") returns a promise for the articles relation. With Restangular's $object property, I receive the articles data array in the ng-repeat:
<tabset>
<tab ng-repeat="group in articleGroups" heading="{{group.name}}">
<table class="table">
<tr ng-repeat="article in articles[group.id].$object | orderBy:'id':false ">
<td>{{article.name}}</td>
<td>{{article.price | currency }}</td>
</tr>
</table>
</tab>
</tabset>
Now, all articles of the selected articleGroup's tab are displayed on the tab. While this seems fine, I'm still not sure if this is the best solution in terms of style or performance. But at least it's a step ahead. Comments very welcome.

Related

Is it possible to filter JSON in Angular using multiple drop down menus

I have built an app for a client using my basic / beginner knowledge of Angular. It took me a lot longer than most would but it has been a great learning experience for me.
The app imports JSON data which displays products with each returned result linking to a page with more information.
The products are manually filtered i.e. I Have built pages for all possible filters (the long hard way). This is displayed as 4 initial options then you go to another page with another 4 options based off the previous option then a final page with 4 further options from here a list is returned based on the the previous selections. This way has led to a large collection of JSON files all based off the filter combinations available.
Background story aside my client now wants to be able to filter all data using drop downs on the opening page. Is this possible?
My JSON data looks like below as an example
{"Level":"student","Style":"barb","Handle":"left","ItemCode":"5.25: 606603, 5.75: 606607","title":"Jaguar Prestyle Relax Leftie","Size":"5.25, 5.75","Price":"£50.00","Description":"One micro-serrated to prevent hair bunching or slippage, these are a particularly good choice for barbers. These scissors are ergonomically designed to reduce strain on your hands and wrists, with an offset handle, finger rest and finger ring inserts for extra comfort. Made from stainless steel with a matt satin finish.","picture":"img/stubarbleft/1.jpg"}
What I would like to do is display the returned data which is not an issue I can do that part. At the top of the page I would like to have a set of dropdown filters so the top one filters the returned data but LEVEL then the second one by STYLE (keeping the level the same as selected) and third option by HANDLE (again keeping the previous options in mind)
Is this possible to do. I am out of my depth a bit but trying to learn as I go.
Thanks for your time guys
Sure, it's possible. I've prepared a self explanatory code snippet that should help you out.
var myApp = angular.module('myApp', []);
myApp.controller('MyCtrl', ['$scope',
function($scope) {
$scope.model = {
selectedStyle: "",
selectedLevel: "",
filterObject: { style : "", level : ""},
recordDetails: undefined,
options: {
styles: ["", "style1", "style2"],
levels: ["", "level1", "level2"]
},
data: [{
"id": 1,
"level": "level1",
"style": "style1",
"price": 100
}, {
"id": 2,
"level": "level2",
"style": "style2",
"price": 200
}, {
"id": 3,
"level": "level1",
"style": "style2",
"price": 300
}, {
"id": 4,
"level": "level2",
"style": "style1",
"price": 400
}]
};
$scope.showDetails = function (record) {
$scope.model.recordDetails = record;
}
}
]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
<div ng-app="myApp" ng-controller="MyCtrl">
<select name="styleSelect" id="styleSelect" ng-options="option for option in model.options.styles" ng-model="model.filterObject.style"></select>
<select name="levelSelect" id="levelSelect" ng-options="option for option in model.options.levels" ng-model="model.filterObject.level"></select>
Current filter object: {{model.filterObject}}
<div ng-repeat="record in model.data | filter:model.filterObject">
<a href="#" ng-click="showDetails(record)">
<ul>
<li>{{record.id}}</li>
<li>{{record.level}}</li>
<li>{{record.style}}</li>
</ul>
</a>
</div>
<div>
Record details:
{{model.recordDetails}}
</div>
</div>
Please let me know whether anything needs further explanation.

AngularJS ng-options from CakePHP Data

I have some json data I receive from a cakePHP server. this is a snippet of what is found in my current $scope.catalog.ConstructionMethod ...
[
{
"ConstructionMethod": {
"id": "99",
"title": "All Plywood",
"price": "0.12"
}
},
{
"ConstructionMethod": {
"id": "139",
"title": "Plywood Sides",
"price": "0.05"
}
}
]
I am trying to create a select box like so...
<select ng-model="Project.ConstructionMethod" ng-options=" ... ">
I have tried the following with no success ...
ng-options="item.title as item.ConstructionMethod for item in catalog.ConstructionMethod"
ng-options="item.ConstructionMethod.title for item in catalog.ConstructionMethod"
I am able to iterate through using ng-repeat just fine. :s
<p ng-repeat="item in catalog.ConstructionMethod">item.ConstructionMethod.title</p>
Any pointers or advice would be greatly appreciated... Thank you.
Angular's select requires an ng-model to work.
Try with the following code:
<select ng-model="result" ng-options="item.ConstructionMethod as item.ConstructionMethod.title for item in catalog.ConstructionMethod"></select>
Let me know if it worked :)
Expounding on aleberguer's answer (which pointed out I needed a model) – I was still having a hard time having the correct options selected when staring my app. The whole "using select as and track by don't work together" thing... so this solution not only populated the options but enabled them to be selected when opening the app.
<select ng-model="result" ng-options="item.ConstructionMethod.title for (k, item) in catalog.ConstructionMethod track by item.ConstructionMethod.id"></select>

Filter collection by child

I have the following fictional object which I'm trying to filter:
{
"0":{
"boy":{
"age":"32",
"name":"Daniel Grey"
}
},
"1":{
"boy":{
"age":"23",
"name":"John Doe"
}
}
}
And then, the ng-repeat directive looks like this:
<ul>
<li ng-repeat="person in people">{{person.boy.name}}<li>
</ul>
My question is, how do I filter people by "name"? I've tried:
<input type="text" ng-model="name">
<ul>
<li ng-repeat="person in people | filter:name">{{person.boy.name}}<li>
</ul>
... but nothing happens [ ng-model seems disconnected from the view! ].
Any response is much appreciated!
Thank you!
Updated answer as per OP's updates
Looking at your fiddle, your $scope.people is essentially an array with one big JSON object, with multiple nested boy objects. This is hard to work with. If you have control over the construction of the JSON object, I will suggest converting into an array of multiple JSON objects, which may look something like:
$scope.people = [
{
"name":"Daniel Grey",
"age":"32",
"gender": "male"
},
{
"name":"John Doe",
"age":"23",
"gender": "male"
}
];
Notice how I converted the boy key into the gender attribute.
If you really have absolutely no control over the data structure, you may have to come up with a custom filter to parse through the nested structure.
Take a look at this fiddle. A few things to pay attention to:
I have to specify people[0] in ng-repeat to retrieve the one big JSON object in your array.
The custom nameFilter searches the .boy.name attribute only.
Original Answer
If you want your filter by just name, you will have to specify the specific attributes in your ng-model directive. So in your case, it will be
<input type="text" ng-model="search.boy.name">
<ul>
<li ng-repeat="person in people | filter:search">{{person.boy.name}}<li>
</ul>
But first you will need to fix your JSON object.
UPDATE:
Live demo on fiddle. I did notice that the search-by-name-only filter doesn't work with angularjs 1.2.1, but works with angularjs 1.2.2.

List items by type in AngularJs using ng-repeat

I have some items, lets say they are Cars. I am using ng repeat to list them.
Cars also have tires. Let's say I want to list all of the tire options, but there are a several different types of tires, so we want to separate them by their type {summer, road, racing, etc....):
<div class="col-md-4" ng-repeat="individualCar in allCars" ng-show="CarTypeFilter[individualCar.type]">
<div class="">
{{individualCar.name}}
</div>
<ul>
<!-- This is the list of all one type of tires -->
<li class="TireType1" ng-repeat="tire in individualCar.tires" ng-show="CarTireFilter[tire.type]">
{{tire.name}}
</li>
<!-- This is another list of all another type of tires -->
<li class="TireType2" ng-repeat="tire in individualCar.tires" ng-show="CarTireFilter[tire.type]">
{{tire.name}}
</li>
</ul>
</div>
This isn't quite working, since there isn't a separate list of tire types per Car on the Controller $scope, and Im not sure if I need to build one to reference. How do I filter or list the types of tires separately using ng-repeat?
I plan on styling them differently, hence the distinction needed.
Example JSON:
{
"jokes":[
{
"id:":1,
"name":"Tesla",
"type":"CAR",
"Tires":[
{
"id":1,
"type": "summer",
"name":"Goodyear"
},
{
"id":2,
"type": "summer",
"name":"Kuhmo"
},
{
"id":3,
"type": "winter",
"name":"Perelli"
},
{
"id":4,
"type": "racing",
"name":"Cooper"
}
]
},
{...}
]
}
In the controller, I have an associative array that stores the different types {summer, racing, etc) as the key, and a boolean value, so that when they want to see each option via a checkbox, the items show in the ng-repeat:
$scope.CarTireFilter = {"summer":true, "racing":false, ... };
If you are wanting to only show items in the list by some specific atrribute then a filter is what you need. See the docs for Filter & OrderBy.
Do not us ng-show rather use this:
div class="col-md-4" ng-repeat="individualCar in allCars|
filter: SET CONDITIONS HERE |
orderBy: SET CONDITIONS HERE">
The other thing I would like suggest is that if you are going to be adding styling based on the various types. You might want to look into doing this in a more angular way using ng-style. Look at the docs ng-style. This approach would allow you add the needed styles based on type and not necessarily need to have separate li tags with different classes. you should try and let angular handle this.

Push rows in a table rendered with ng-repeat in angular

I want to push in an extra row inline in a table when the client clicks on the row. The data should not be prefetched, since I expect there to be at most 30 rows but where each row has associated data that would be unreasonable to fetch in one get.
My approach so far is to use ng-repeat to iterate my collection and render a table. When the client presses the row, the client expects details about the row to be shown inline as an extra row under the pressed row.
<tr ng-repeat="court in courts">
<td>{{court.name}}</td>
<td>{{court.number}}</td>
<td>{{court.nrOfPlayers}}</td>
<td>
<a href ng:click="toggle(court.number)">Details</a> <!-- click toggles extra row with data loaded async -->
</td>
</tr>
<!-- extra row here -->
I have managed to show the details beneath the table with a ng-show in a hacky way, but that is not what I want.
How do you accomplish this with angular.js? What is the angular way to do this?
Here is a fiddle with a stupid squash court example http://jsfiddle.net/HByEv/
I think a possible solution could be http://jsfiddle.net/HByEv/2/.
There is also an alternative for the "No players" message commented in the fiddle if you want to also get rid of the extra <tr ng-show="..."></tr>.
Edit:
As pointed in the comments, in AngularJS 1.2+ you can now use ng-repeat-start and ng-repeat-end to solve this problem.
Jossef Harush provided a fiddle: http://jsfiddle.net/3yamebfw/
One sample
var app = angular.module('test-app', []);
app.controller('MyController', function($scope, $rootScope, $timeout){
$scope.copy = {
p1: ['c1 p1', 'c1 p2'],
p3: ['c3 p1', 'c3 p2', 'c3 p3', 'c3 p4', 'c3 p5']
}
$scope.courts = [
{
"number": 1,
"name": "the best court",
"nrOfPlayers": 2
}, {
"number": 2,
"name": "the bad court",
"nrOfPlayers": 0
}, {
"number": 3,
"name": "the other court",
"nrOfPlayers": 5
}
];
$scope.loadPlayers = function(court){
//Implement your logic here
//Probably using ajax
$timeout(function(){
$scope.players = $scope.copy['p' + court.number] || [];
}, Math.random() * 2000);
}
$scope.shouDetails = function(court){
if(court.nrOfPlayers) {
delete $scope.players;
$scope.loadPlayers(court);
} else {
$scope.players = [];
}
}
})
Demo: Fiddle
Well. In fact, the main issue with your design is that you want to show thousands of rows in the same table. It will work, but it might be hard to render in some browsers (IE). For each rows, you will have a few bindings and each binding add watchers. You should always try to minimize the amount of binding in the page. I suggest you to use a pagination system in your array. Pagination on a list using ng-repeat
If you really want to do what you want without prerendering the rows, you will have to edit the dom, which is not a good practice in angular when avoidable. In my case, i would place the data somewhere else on the page, in a static zone. When i did something like this, I added a twitter bootstrap modal in my page and when the user clicked on "more info" the modal was opened with the info of the selected object.

Resources