I get a nested JSON object back from an API call that looks something along the lines of this:
{
"name": “Main “Folder”,
"children": [
{
"name": “Child Folder 1”,
"children": []
},
{
"name": “Child Folder 2”,
"children": [
{
"name": “Sub Folder 1”,
"children": [
{
“name”: “Sub Sub Folder 1”,
“children”: []
}
]
},
{
"name": “Sub Folder 2” ,
"children": []
}
]
}
]
}
There is no limit on how far the JSON object can be nested so that is unknown to me. I need to have all of the children of the folders to be indented under the parent in the table. I'm not really even sure how to go about doing this. The first thing I tried was doing something like this in my HTML file, but I quickly realized it wasn't going to work.
folders.html
<table>
<thead>
<tr><strong>{{ this.tableData.name }}</strong></tr>
</thead>
<tbody ng-repeat="b in this.tableData.children">
<tr>
<td>{{ b.name }}</td>
<td ng-repeat="c in b.children">{{ c.name }}</td>
</tr>
</tbody>
</table>
folders.js
export default class FoldersController {
constructor($rootScope, $scope, $uibModal) {
this.tableData = {Example Data from top}
}
}
Is there a not too complicated way to go about doing this? Thanks!
You should create a component with a template that contains a table, then you can nest your component inside itself to follow the tree structure logical path:
Your root controller should contain your table data:
angular.module('app').controller('RootCtrl', ['$scope', function($scope) {
// assigning the data to $scope to make it available in the view
$scope.tableData = {Example Data from top};
}]);
Your tree component could be something on this lines:
angular.module('app').component('treeComponent', {
controller: 'TreeCtrl',
bindings: {
tree: '<',
},
templateUrl: 'tree-view.html'
});
your root template should load the first instance of the component:
<div>
<tree-component tree="tableData"></tree-component>
</div>
then the component template should take care of the the recursion when required;
tree-view.html:
<table class="record-table">
<thead>
<tr>
<th>
<strong>{{ $ctrl.tableData.name }}</strong>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="node in $ctrl.tableData.children">
<td>{{node.name}}</td>
<td ng-if="node.children.length > 0">
<tree-component tree="node.children"></tree-component>
</td>
</tr>
</tbody>
</table>
creating indentation then becomes easy using basic css:
.record-table .record-table {
padding-left: 20px
}
I was able to figure out a solution of my own using recursion in the js file. I implemented mindthefrequency's answer as well and it seems to be working just fine. I'm marking it as the best answer because it seems to be the cleaner solution, but I'm posting what I have in case someone wants to take a more js oriented approach.
First, in the js file, use recursion to add all of the nodes and how far each needs to be indented to the table data variable.
folders.js
export default class FoldersController {
constructor($rootScope, $scope, $uibModal) {
this.resp = {Example Data from top}
this.tableData = []
this.createTable(this.resp.children, 0, [
{
name: this.resp.name,
indent: '0px',
},
]);
}
createTable(children, count, data) {
count += 1;
// base case
if (!children || children.length === 0) {
return;
}
for (const child of children) {
const { name } = child;
const returnData = data;
returnData.push({
name: name,
indent: `${count * 25}px`,
});
this.tableData = returnData;
this.createTable(child.children, count, returnData);
}
}
}
Then, in the html file, use angularjs to properly indent each node
folders.html
<table>
<thead>
<tr><strong>Table Header</strong></tr>
</thead>
<tbody ng-repeat="b in vm.tableData">
<tr>
<td ng-style="{'padding-left': b.indent}">{{ b.name }}</td>
</tr>
</tbody>
</table>
I'm making a vue.js front-end feature that could filter data by keywords and sort data in either ascending or descending order. And I found this code online which might meet my requirements. However the format of its data array is different from my retrieved array:
<template>
<div id="app">
<br /><br />
<div>
<input type="text" v-model="filterValue" placeholder="Filter">
<button #click="invertSort()">Sort asc/desc</button>
</div>
<table class="table table-striped">
<thead>
<tr>
<td>Name</td>
<td>Value</td>
</tr>
</thead>
<tbody>
<tr v-for="data in filteredAndSortedData">
<td>{{data.name}}</td>
<td>{{data.val}}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
data() {
return {
testData: [{name:'3', val:'a'}, {name:'2', val:'b'}, {name:'1', val:'c'}, {name:'4', val:'d'}],
error: "",
errorFlag: false,
filterValue: "",
sortAsc: true
};
},
computed: {
filteredAndSortedData() {
// Apply filter first
let result = this.testData;
if (this.filterValue) {
result = result.filter(item => item.name.includes(this.filterValue));
}
// Sort the remaining values
let ascDesc = this.sortAsc ? 1 : -1;
return result.sort((a, b) => ascDesc * a.name.localeCompare(b.name));
}
},
methods: {
invertSort() {
this.sortAsc = !this.sortAsc;
}
},
}
</script>
So in the provided function its array is like testData: [{name:'1', val:'a'}], however, my array which retrieved by using this.testData = response.data in function(response) is like testData: [{"name":"1", "val":"a"}]
That being said, my variable names are double-quoted and cannot use the code I want. Is there a way to convert [{"name":"1", "val":"a"}] to [{name:'1', val:'a'}] (like removing variable names quotes) or sort it without modifying the array?
you can get object key sting using
var a = [{"name":"1", "val":"a"}];
console.log(a[0]["name"])
// if you already know key
console.log(a[0].name)
and this [{"name":"1", "val":"a"}] and [{name:'1', val:'a'}] is same in javascript.
I am working on this Laravel Project where I load a component activities-table with the main Vue instance data called from an API where I use Eloquent ORM Pagination. But the ActivitiesTable.data is undefined:
So, first I have my API on http://app.local/api/activities
public function getActivities()
{
$activities = Activity::orderBy('created_at', 'desc')->paginate(100);
foreach($activities as $k => $activity)
{
$data = [
'user' => isset($activity->user) ? $activity->user->name : NULL,
'avatar' => isset($activity->user) ? $activity->user->avatar : NULL,
'ip' => $activity->remote_addr,
'country' => $activity->country,
'referer' => $activity->referer,
'action' => $activity->request_uri,
'date' => $activity->created_at->toDateTimeString(),
'timeago' => $activity->created_at->diffForHumans()
];
$activities[$k]->custom_data = $data;
}
return $activities;
}
Then I have my main Vue instance method where I call the API on the file app.js:
loadActivities: function(page = 1) {
axios.get('/api/activities?page=' + page)
.then(function (response) {
app.activities = response.data;
});
},
So this is how activities seems on Vue Tool:
So, this is my component that is already registered on app.js:
<template>
<!-- Activities -->
<table id="activities">
<thead>
<tr>
...columns...
</tr>
</thead>
<tbody>
...data...
</tbody>
</table>
<!-- /Activities -->
</template>
<script>
module.exports = {
props: {
rows: {
}
},
data: function() {
return {
data: this.rows
}
},
created: function() {
},
methods: {
paginate: function(page) {
}
}
}
</script>
<style scoped>
</style>
And then here is the <activities-table> component:
<activities-table
:rows="this.activities"
></activities-table>
I get the result in the same Vue Tool:
I already tried to assign this.data = this.rows in the created hook, or try different lifecycle hooks but always is undefined or an empty array.
This is what console.log(this.data) on the Vue component displays:
What am I doing wrong? Thanks in advance.
I have a simple WebApi project to manipulate returned data in various format. I am trying to use KnockoutJs to consume the data at the front end but I am having a variable not defined error that I can't seem to understand why. Below is the simple code I am working on. Please feel free to point out the errors. Thanks
Controller formats
[httpGet]
public Object Data
{
return new {
Time: DateTime.now,
Text: "<b>Salut</b>",
Count: 1
};
}
JS front end
<script>
$(document).ready(function(){
$.ajax("/api/formats", {
success: function(data){
dataObject = ko.observable(data);
ko.applyBindings();
}
};
});
HTML
<tbody>
<tr>
<td>Time</td>
<td data-bind="text: dataObject().Time">
<td>Text</td>
<td data-bind="text: dataObject().Text">
</tr>
</tbody>
At first,your variable dataObject does not have Time and Text properties, so you should check it in your code as following:
var dataObject = ko.observable();
ko.applyBindings();
function doBinding() {
var data = {
Time: "XYZ",
Text: "<b>Salut</b>",
Count: 1
};
dataObject(data);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div>
<div data-bind="text: dataObject() ? dataObject().Time : 'No Data'"></div>
<div data-bind="text: dataObject() ? dataObject().Text : 'No Data'"></div>
<div>
<button onclick="doBinding()">binding</button>
I have two objects. First one have entire school full details of students record. Example like
var first = {
students:
[
{ id:'1', name:"suresh", age:"20", degree:"BSc", status:"obsent"},
{ id:'2', name:"ramesh", age:"21", degree:"BCom", status:"present"},
{ id:'3', name:"rajesh", age:"19", degree:"BA", status:"leave"},
{ id:'4', name:"satish", age:"28", degree:"BL", status:"obsent"}
]
}
Second one have particular class students information about the status of the student for that day. Example like
var second ={
students:
[
{ id:'1',status:"present"},
{ id:'12',status:"obsent"},
{ id:'3',status:"obsent"},
{ id:'14',status:"leave"}
]
}
Now I need to compare the student id and need to display the status based on the result. I have achieved in the following way.
items = first.students.map(function(item){
status =item.status;
second.students.map(function(key){
if(key.id == item.id) { status = key.status }
});
return "<tr><td>"+item.name+"</td><td>"+item.age+"</td><td>"+item.degree+"</td><td>"+status+"</td></tr>";
});
$('table#main tbody').html(items);
The above code is working fine. But if you look at my code, I have used the map functionality multiple times. I feel that I have done something wrong in the performance wise. Is that possible to reduce using the map twice or any other better way to achieve the same result. Please suggest me.
Code Snippet
var first = {
students:
[
{ id:'1', name:"suresh", age:"20", degree:"BSc", status:"obsent"},
{ id:'2', name:"ramesh", age:"21", degree:"BCom", status:"present"},
{ id:'3', name:"rajesh", age:"19", degree:"BA", status:"leave"},
{ id:'4', name:"satish", age:"28", degree:"BL", status:"obsent"}
]
}
var second ={
students:
[
{ id:'1',status:"present"},
{ id:'12',status:"obsent"},
{ id:'3',status:"obsent"},
{ id:'14',status:"leave"}
]
}
items = first.students.map(function(item){
status =item.status;
second.students.map(function(key){
if(key.id == item.id) { status = key.status }
});
return "<tr><td>"+item.name+"</td><td>"+item.age+"</td><td>"+item.degree+"</td><td>"+status+"</td></tr>";
});
$('table#main tbody').html(items);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table id="main" cellspacing="2" border="1">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>Degree</th>
<th>Stauts</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</tbody>
</table>
Due to the way your objects are set up, it looks like that will be O(n) time for the lookup, because you need to loop through the first student array for every student id.
To get around this, you can do a single map where you assign the id as the key of a new intermediate object in the format of:
x = {1: {...}, 2: {...}}
From there, you can now do constant time O(1) lookups:
x[id]
The only extra work is building the intermediate hash, but that will be less computation than what you have above.
See this example below. Note that it does use 2 maps, but it's different than your example because it's not a map within a map which is exponential:
var students = [
{ id:'1', name:"suresh", age:"20", degree:"BSc", status:"obsent"},
{ id:'2', name:"ramesh", age:"21", degree:"BCom", status:"present"},
{ id:'3', name:"rajesh", age:"19", degree:"BA", status:"leave"},
{ id:'4', name:"satish", age:"28", degree:"BL", status:"obsent"}
];
var studentIds = {};
students.forEach(function(student) {
studentIds[student.id] = {name: student.name, age: student.age, degree: student.degree, status: student.status}
});
var second = [
{ id:'1',status:"present"},
{ id:'12',status:"obsent"},
{ id:'3',status:"obsent"},
{ id:'14',status:"leave"}
];
var studentStatuses = second.map(function(student) {
// do whatever you have to do here
return (studentIds[student.id] || {}).status;
});
The complexity will be better if you build an object which keys are id and values are status from second.students then you update status in first.students based on this object:
var first = {
students:
[
{ id:'1', name:"suresh", age:"20", degree:"BSc", status:"obsent"},
{ id:'2', name:"ramesh", age:"21", degree:"BCom", status:"present"},
{ id:'3', name:"rajesh", age:"19", degree:"BA", status:"leave"},
{ id:'4', name:"satish", age:"28", degree:"BL", status:"obsent"}
]
}
var second ={
students:
[
{ id:'1',status:"present"},
{ id:'12',status:"obsent"},
{ id:'3',status:"obsent"},
{ id:'14',status:"leave"}
]
}
var statusById= second.students.reduce(function(m, e) {
m[e.id] = e.status;
return m;
}, {});
items = first.students.map(function(item){
item.status = statusById[item.id] || item.status;
return "<tr><td>"+item.name+"</td><td>"+item.age+"</td><td>"+item.degree+"</td><td>"+item.status+"</td></tr>";
});
$('table#main tbody').html(items);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table id="main" cellspacing="2" border="1">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>Degree</th>
<th>Stauts</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</tbody>
</table>