Angular ng-repeat with nested json objects? - json

I have a JSON object, represented as such:
{
"orders" : [
{
"ordernum" : "PRAAA000000177800601",
"buyer" : "Donna Heywood"
"parcels" : [
{
"upid" : "UPID567890123456",
"tpid" : "TPID789456789485"
},
{
"upid" : "UPID586905486090",
"tpid" : "TPID343454645455"
}
]
},
{
"ordernum" : "ORAAA000000367567345",
"buyer" : "Melanie Daniels"
"parcels" : [
{
"upid" : "UPID456547347776",
"tpid" : "TPID645896579688"
},
{
"upid" : "UPID768577673366",
"tpid" : "TPID784574333345"
}
]
}
]
}
I need to do a repeater on the second level of this, a list of the "upid" numbers.
I know already how to get the top level
<li ng-repeat="o in orders">{{o.ordernum}}</li>
But I am unclear on the sequence to loop a level down. For example, this is wrong:
<li ng-repeat="p in orders.parcels">{{p.upid}}</li>
I also know how to nest repeaters to get this, but in this case i don't need to display the top level at all.
CLARIFICATION
The goal here is to have one list with the 4 "upid" numbers (there are 2 for each parcel, and there are 2 parcels in the order).

Actually its same answer of #sylwester. The better way to put it in filter. And you can reuse it by passing propertyName parameter.
In your case we passed parcels
JS
myApp.filter('createarray', function () {
return function (value, propertyName) {
var arrayList = [];
angular.forEach(value, function (val) {
angular.forEach(val[propertyName], function (v) {
arrayList.push(v)
});
});
return arrayList;
}
});
HTML
<ul>
<li ng-repeat="o in ordersList.orders | createarray: 'parcels'">{{o.upid}}</li>
</ul>
Here is working Fiddle

You can just create new array 'parcels' like in demo below:
var app = angular.module('app', []);
app.controller('homeCtrl', function($scope) {
$scope.data = {
"orders": [{
"ordernum": "PRAAA000000177800601",
"buyer": "Donna Heywood",
"parcels": [{
"upid": "UPID567890123456",
"tpid": "TPID789456789485"
}, {
"upid": "UPID586905486090",
"tpid": "TPID343454645455"
}]
}, {
"ordernum": "ORAAA000000367567345",
"buyer": "Melanie Daniels",
"parcels": [{
"upid": "UPID456547347776",
"tpid": "TPID645896579688"
}, {
"upid": "UPID768577673366",
"tpid": "TPID784574333345"
}]
}]
};
$scope.parcels = [];
angular.forEach($scope.data.orders, function(order) {
angular.forEach(order.parcels, function(parcel) {
$scope.parcels.push(parcel)
})
})
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="homeCtrl">
<ul>
<li ng-repeat="o in parcels">{{o.upid}}</li>
</ul>
</div>
</div>

Seems like you just need a double-nested for loop -
<ul>
<div ng-repeat="o in orders">
<li ng-repeat="p in o.parcels">{{p.upid}}</li>
</div>
</ul>
The HTML might be a little ugly here, but I'm not sure what exactly you are going for. Alternatively you could just create a new array of the parcels via mapping.

Searching a lot for nice and simple solution for iterating dynamically. I came up with this
JAVASCRIPT (angular): a person is an example of nested object. the is_object function will be use in the HTML view.
$scope.person = {
"name": "john",
"properties": {
"age": 25,
"sex": "m"
},
"salary": 1000
}
// helper method to check if a field is a nested object
$scope.is_object = function (something) {
return typeof (something) == 'object' ? true : false;
};
HTML: define a template for simple table. the 1st TD is the key which is displayed. another TD (2 or 3, but never both) will be show the value if its not an object (number / string), OR loop again if its an object.
<table border="1">
<tr ng-repeat="(k,v) in person">
<td> {{ k }} </td>
<td ng-if="is_object(v) == false"> {{ v }} </td>
<td ng-if="is_object(v)">
<table border="1">
<tr ng-repeat="(k2,v2) in v">
<td> {{ k2 }} </td>
<td> {{ v2 }} </td>
</tr>
</table>
</td>
</tr>
</table>

The reason that <li ng-repeat="p in orders.parcels">{{p.upid}}</li> does not work the way you expect is because the parcels array is an object inside each individual order in your order array, i.e. it is not an object of the orders array itself.
If your orders array is defined on the $scope of a controller, then you create the array on the $scope variable:
$scope.allParcels = $scope.orders
.map(function (elem) {
return elem.parcels;
}) // get an array where each element is an array of parcels.
.reduce(function (previousValue, currentValue) {
return previousValue.concat(currentValue);
}); // concat each array of parcels into a single array of parcels
then on the template, you can use <li ng-repeat='p in allParcels'>{{p.upid}}</li>
If, however, you do not want to place the array on the $scope, I believe you can do something similar to this:
<li ng-repeat="p in orders
.map(function (elem) {
return elem.parcels;
})
.reduce(function (previousValue, currentValue) {
return previousValue.concat(currentValue);
})">{{p.upid}}</li>
although I'm not 100% sure that Angular will evaluate the .map/.reduce in the ng-repeat expression (also having an array generated this way in an ng-repeat is ill-advised since angular would have to constantly generate a new array via map/reduce on each $digest cycle).

Related

How to manually focus input

I want to replicate a common item list renaming feature, where you have a list of layers and if you double click a layer, it changes the layer item to an input and that input is automatically focused with its text selected as well.
In my example, I am not able to focus() the DOM element by its ref because it says it is not defined. It only works if I click a second time on the element once its changed to an input. How do I set this autofocus?
<div v-for="(item, i) in items">
<div #click="changeToInput(i)" v-if="!item.input">{{item.name}}</div>
<input ref="input" v-model="item.name" onfocus="select()" v-else>
</div>
changeToInput(i) {
this.items[i].input = true;
//this.$refs.input.focus()
}
Here is the complete example : https://codesandbox.io/s/reverent-khayyam-2x8mp?file=/src/App.vue:481-573
Two solutions:
First one: uses v-if + this.$nextTick:
v-if will insert/destroy the component when the binding expression is true/false, so in current cycle, input hasn't been in Dom tree. You have to use nextTick to wait for next cycle to get the Dom element of Input. And this.$refs.input will be one array based on how many v-if=true, so you have to filter out the this.items to find out correct index (that is why I used one combination of Array.slice and Array.filter).
Updated: The order of the elements of this.$refs.input1 is the order VNode is created. For example: clicks input2 -> input3 -> input1, the order of this.$refs.input1 is [2, 3, 1], not [1, 2, 3].
Second one: uses v-show + this.$nextTick:
It will make things easier, because v-show only update the css styles for Dom elements, it will not add/remove component instance (Vnode) from VNode tree. So the this.$refs.input will always equal this.items.length.
new Vue ({
el:'#app',
data() {
return {
items1: [
{ name: "Joe", input: false },
{ name: "Sarah", input: false },
{ name: "Jeff", input: false }
],
items2: [
{ name: "Joe", input: false },
{ name: "Sarah", input: false },
{ name: "Jeff", input: false }
],
refSort: {}
};
},
methods: {
changeToInput1(i) {
this.items1[i].input = true;
let refCount = (this.$refs.input1 && this.$refs.input1.length) || 0
refCount < this.items1.length && (this.refSort[i] = refCount)
this.$nextTick(() => {
// the purpose uses this.refSort is record the order of this.$refs.input (Its order is same as the creating order of Ref), you can uncomment below line to see the root cause
//console.log(this.$refs.input1[0] && this.$refs.input1[0].value, this.$refs.input1[1] && this.$refs.input1[1].value, this.$refs.input1[2] && this.$refs.input1[2].value)
this.$refs.input1[this.refSort[i]].focus()
})
},
changeToInput2(i) {
this.items2[i].input = true;
this.$nextTick(() => this.$refs.input2[i].focus())
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<h3>Uses v-if: <p>{{items1}}</p></h3>
<div v-for="(item, i) in items1">
<div #click="changeToInput1(i)" v-if="!item.input">{{item.name}}</div>
<input ref="input1" v-model="item.name" onfocus="select()" v-else>
</div>
<h3>Uses v-show: <p>{{items2}}</p></h3>
<div v-for="(item, i) in items2">
<div #click="changeToInput2(i)" v-show="!item.input">{{item.name}}</div>
<input ref="input2" v-model="item.name" onfocus="select()" v-show="item.input">
</div>
</div>

Using ng-repeat with dynamic value

I have HTML that is rendered based on loaded metadata. It ends up forming a table of data based on a scoped value in the controller.
In the controller:
$scope.arrayOfCustomObjects = [{ entry: data1 },{ entry: data2 }];
The metadata is contained in a .json file, with the following format (I added two here, in reality there are hundreds, each one basically ends up describing an HTML element):
loadedMetaData:
{
"field_1": {
"index": 0,
"type": "selectbox",
"nameId": "arrayOfCustomObjects" <-- this is the string name of a scoped variable in a controller.
},
"field_2": {
"index": 1,
"type": "textinput",
"nameId": "textField"
}
}
And the HTML looks like this:
<div ng-repeat="field in loadedMetaData>
<div ng-repeat="item in field.nameId">
<!-- build out HTML for each -->
</div>
</div>
When I run this, it doesn't work (it never iterates over $scope.arrayOfCustomObjects). If I add a line to display {{field.nameId}} is displays 'arrayOfCustomObjects' but I think it's just the string, not the value it represents.
If I change the HTML to this it does work:
<div ng-repeat="field in loadedMetaData>
<div ng-repeat="item in arrayOfCustomObjects">
<!-- build out HTML for each -->
</div>
</div>
...but is there any way to keep the abstraction I'm going for? I'd like to be able to define the target array in the metadata so I don't have to define the controller-specific names in the HTML itself.
Suppose that your loadedMetadata is like:
[
"arrayOfCustomObjects",
// other 'data' object keys
]
Then you could have an object like this that stores your actual data:
data = {
"arrayOfCustomObjects": [
// some items
]
}
Then, in the template you do:
<div ng-repeat="field in loadedMetaData>
<div ng-repeat="item in data[field]">
<!-- display the items in arrayOfCustomObjects -->
</div>
</div>
This takes the value as string
var field = { "nameId": "arrayOfCustomObjects" }
change this to
var field = { "nameId": arrayOfCustomObjects }
Use a bracket notation property accessor with the this keyword:
<div ng-repeat="(fieldKey, fieldValue) in loadedMetaData>
<div ng-repeat="item in this[fieldValue.nameId]">
<!-- build out HTML for each -->
</div>
</div>
From the Docs:
It is possible to access the context object [($scope)] using the identifier this and the locals object using the identifier $locals.
— AngularJS Developer Guide - Expression Context
To achieve expected result, use below changes from your plunker code
Using scope function to convert string to scope variable
Rendering that variable inside inner ng-repeat
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.arrayOfCustomObjects = [1,2,3]
$scope.loadedMetaData = {
"field_1": {
"index": 0,
"type": "selectbox",
"nameId": "arrayOfCustomObjects"
},
"field_2": {
"index": 1,
"type": "textinput",
"nameId": "textField"
}
}
$scope.getVariable = function(val){
return $scope[val]
}
});
<html>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
<body>
<div ng-app="myApp" ng-controller="myCtrl as ctrl">
<div>
<div ng-repeat="field in loadedMetaData">
<div ng-repeat="item in getVariable(field.nameId)">
{{item}}
</div>
</div>
</div>
</div>
</body>
</html>
codepen - https://codepen.io/nagasai/pen/XGLZzX

angular2 how to access a json child

I would like to access a json child using Angular2, here is what I tried:
my json :
orderList=
{
"ref1": {
"Id": "57987"
"order": [
{
"amount": 4900,
"parent": "CIDPT8AO"
}
]
}
}
in the view:
<tr *ngFor="let order of orderList">
<td>{{order[0]}}</td> // NOT OKAY
</tr>
I would like to access "Id" and "parent", any idea?
First of all: you're missing a , after the "Id" line.
You're trying to access a JSON object like an array. Not sure if that works with Angular... If the error is not solved by adding the , I mentioned above, try declaring your orderList as an array.
If you're bound to this format you could do it like that:
orderListValues = Object.keys(orderList).map(function(_) { return j[_]; })
Then you should be able to access the values like that:
<tr *ngFor="let order of orderListValues">
<td>{{order.Id}}</td>
</tr>
Example:
orderList = {
"ref1": {
"Id": "57987",
"order": [
{
"amount": 4900,
"parent": "CIDPT8AO"
}
]
}
};
orderListValues = Object.keys(orderList).map(function(_) { return orderList[_]; });
console.log(orderListValues);
Your orderList is an object, you need
<tr *ngFor="let order of orderList.ref1.order">
<td>{{order}}</td>
</tr>
You can try this approach :
HTML
<table>
<tr *ngFor="let order of obj">
<td>{{order.Id}}</td>
</tr>
</table>
Component
var orderList=
{
"ref1": {
"Id": "57987",
"order": [
{
"amount": 4900,
"parent": "CIDPT8AO"
}
]
}
};
var obj = orderList[Object.keys(orderList)[0]];
NgForOf provides several exported values that can be aliased to local variables, for example:
index: number -> The index of the current item in the iterable.
first: boolean -> True when the item is the first item in the iterable.
<tr *ngFor="let ref of orderList">
<td>{{order.id}}</td>
<span *ngFor="let order of ref.order; let first = isFirst">
<td *ngIf="isFirst">{{order.parent}}</td>
</span>
</tr>
https://angular.io/api/common/NgForOf

How to implement nested data form in angular2

Here is Json schema :
{
"_id" : ObjectId("59031d77fd5e1c0b3c005d15"),
"resume_data" : {
"work_experience" : [
{
"company" : "example",
"website" : "example.com",
"position" : "Internship",
"highlights" : "Learn To Create API In Laravel Framework. and also Learn Angular 2 for Front end Development.",
"project_experience" : [
{
"projectName" : "Fb Project",
"teamMember" : "5",
"technology" : "PHP,Laravel-5,Angular-2,MongoDb",
"projectPosition" : "Back-end Developer"
}
]
}
]
}
}
Here is image:
I have reference of this answer but i don't know about nested form data. can anyone explain how to implement it.
Here is your code, which sets the data you are receiving from backend, here I have stored it in a variable data.
Please notice, this is a shortened version of your form, but the basics are there, you only need to add the few missing properties in corresponding form arrays.
The build of the empty form looks is just a FormArray named work_experience matching your json structure:
this.myForm = this.fb.group({
work_experience: this.fb.array([])
})
We add the fields when you are receiving the data, call a function called setWorkExperience in the callback when receiving data:
setWorkExperience(){
// get the formarray
let control = <FormArray>this.myForm.controls.work_experience;
// iterate the array 'work_experience' from your JSON and push new formgroup with properties and the inner form array
this.data.work_experience.forEach(x => {
// add the rest of your properties also below
control.push(this.fb.group({company: x.company, project_experience: this.setFormArray(x)}))
})
}
setFormArray is called from the previous function, where we patch the data with from project_experience to the inner form array:
setFormArray(x) {
// create local array which is returned with all the values from the 'project_experience' from your JSON
let arr = new FormArray([])
x.project_experience.map(y => {
// add the rest of your properties below
arr.push(this.fb.group({projectName: y.projectName}))
})
return arr;
}
The template would then look like this:
<form [formGroup]="myForm">
<!-- Outmost array iterated -->
<div formArrayName="work_experience">
<div *ngFor="let a of myForm.get('work_experience').controls; let i=index">
<h3>COMPANY {{i+1}}: </h3>
<div formGroupName="{{i}}">
<label>Company Name: </label>
<input formControlName="company" /><span><button (click)="deleteCompany(i)">Delete Company</button></span><br><br>
<!-- inner formarray iterated -->
<div formArrayName="project_experience">
<div *ngFor="let b of myForm.controls.work_experience.controls[i].controls.project_experience.controls; let j=index">
<h4>PROJECT {{j+1}}</h4>
<div formGroupName="{{j}}">
<label>Project Name:</label>
<input formControlName="projectName" /><span><button (click)="deleteProject(a.controls.project_experience, j)">Delete Project</button></span>
</div>
</div>
<button (click)="addNewProject(a.controls.project_experience)">Add new Project</button>
</div>
</div>
</div>
</div>
</form>
In the template you can see the buttons for add and delete of projects and companies. Adding and deleting companies are straightforward, where initCompany() returns a formGroup:
deleteCompany(index) {
let control = <FormArray>this.myForm.controls.work_experience;
control.removeAt(index)
}
addNewCompany() {
let control = <FormArray>this.myForm.controls.work_experience;
control.push(this.initCompany())
}
In the add project we pass as parameter from the template the current formArray control, to which we just push a new FormGroup:
addNewProject(control) {
control.push(this.initProject())
}
In the delete function we pass the current formarray as well as the index of the project we want to delete:
deleteProject(control, index) {
control.removeAt(index)
}
That should cover pretty much everything.
Plunker
Please Check it Out This
Plunker Here
Json Store Like This
{
"name": "",
"work_experience": [
{
"name": "asdasd",
"project_experience": [
{
"Project_Name": "asdasdasd"
},
{
"Project_Name": "asdasdasd"
}
]
}
]
}

Depth First Search using nested ng-Repeat

I have an object with variable key names, which also contain objects. I would like to do a depth-first search (DFS) over the object.
An example object:
object = { "var1" : {
"var2a" : {
"someKey" : someValue
},
"var2b" : {
"someKey" : someOtherValue
}}}
My attempt at a DFS is as follows (which will not work):
<div ng-repeat = "child in object">
<div ng-repeat = "grandChild in child ">
<td> {{grandChild.aKey}}</td>
</div>
</div>
I have seen the approach of others in this previous question, however this will not work for me due to variable key names.
Additionally, the depth of the object is known.
If we use this code snippet to check what the result of child is:
<div ng-repeat = "child in object">
<td> {{child}}</td>
<div ng-repeat = "grandChild in child ">
<td> {{grandChild.aKey}}</td>
</div>
</div>
We get the first instance of child to be:
{"var2a" : {
"someKey" : someValue
}}
EDIT: Could it be possible to utilise this JavaScript DFS snippet that will output to console?
angular.forEach(object,function(child,value){
angular.forEach(child,function(grandChild,value){
console.log(grandChild.someKey)
});
});
So, it turns out that this is not actually possible using nested ng-Repeats. It must be done in JavaScript. This actually works in line with the 'Angular JS' mentality of keeping logic out of the HTML file.
Here is the DFS in JavaScript:
angular.forEach(object,function(key,value){
if (value!='$id'){
angular.forEach(key,function(keyChild,valueChild){
console.log(valueChild)
}
});
});
This will return "someKey" : someOtherValue and "someKey" : someOtherValue