React - Table onClick collapse - javascript

I have data with parents and children and want to render this as a table. When you click on a parent the children should collapse.
I tried doing this by giving children a className of Parent. So clicking the parent would select all classes of the same name.
I read that in React you should not make the changes in the DOM. As if it wasn't hard enough already!
Here is my table. (While were at it, I'm rendering a new tbody per parent, because React cries about returning proper react elements.. I would really like it to be just all s, if possible)
render() {
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>value</th>
</tr>
</thead>
{this.props.data.items.map(function (parent) {
return (
<tbody>
<tr>
<td >{parent.name}</td>
</tr>
{parent.children.map(function (child) {
return (
<tr>
<td>{child.name}</td>
<td>{child.value}</td>
</tr>
)
})}
</tbody>
)
})}
</table>
)
}
}
Data looks like this
[{
"name": "Food", "children": [
{"name": "Apple", "value": 5.6},
{"name": "Banana", "value": 2.2},
{"name": "Peer", "value": 1.6},
]
},
{
"name": "Drink", "children": [
{"name": "Cola", "value": 5.6},
{"name": "Juice", "value": 2.2},
{"name": "Water", "value": 1.6},
]
}
]

You need an onClick event handler on the parent td <td><button value={parent.name} onClick={this.handleClick}>{parent.name}</button></td>
Then you will need a handleClick method in your component that fires a redux action and updates the state. You will need a new property in your data structure to store whether the parent is open or closed. Something like [{"name": "Food", "children": [...], "open": true}].
Now you can check for the property in the parent and only display the children based on the bool
{parent.open ? parent.children.map(...) : ''}
Final result would look a bit like this
handleClick(event) {
this.props.toggleParent(event.target.value);
}
render() {
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>value</th>
</tr>
</thead>
{this.props.data.items.map(function (parent) {
return (
<tbody>
<tr>
<td><button value={parent.name} onClick={this.handleClick}>{parent.name}</button></td>
</tr>
{parent.open ? parent.children.map(function (child) {
return (
<tr>
<td>{child.name}</td>
<td>{child.value}</td>
</tr>
)
}.bind(this) : ''}
</tbody>
)
})}
</table>
)
}

Related

Multiple v-for loops in same vue file

I'm trying to populate a table using three for loops from my api.
const events = await app.$axios.get('/api/events/')
const markets = await app.$axios.get('/api/markets/')
const markets = await app.$axios.get('/api/runners/')
I need to make sure that the table rows index is runners since the runners column will have more runners than markets and markets will have more rows than events.
Like so.
Event Market
Runner
Runner
Runner
Market
Runner
Runner
When I try to do multiple for for loops in the same vue file I get this error.
duplicate attribute key
Why do I get this error from using id in separate loops?
My Question is how do I populate the table based on runner index for each row?
Here's my code so far.
<template>
<div class="course-list-row">
<th style="width:5%"> Start date </th>
<th style="width:5%"> Event name</th>
<th scope="col">Market</th>
<th scope="col">Runner</th>
<tr v-for="(event, index) in events" :key=id
v-for="(market, index) in markets" :key=id
v-for="(runner, index) in runners" :key=id>
<td>{{ event.start_time }} </td>
<td>{{ event.event_name }} </td>
<td>{{ market.name }} </td>
<td>{{ runner.name }} </td>
<td />
</tr>
</div>
</template>
<script>
export default {
async asyncData({
app
}) {
try {
const events = await app.$axios.get('/api/events/')
const markets = await app.$axios.get('/api/markets/')
const runners = await app.$axios.get('/api/runners/')
return {
events: events.data.results,
markets: markets.data.results,
runners: runners.data.results,
error: false
}
} catch (e) {
console.log('error', e)
return {
events: [],
markets: [],
runners: [],
error: true
}
}
},
};
</script>
<style>
th,
td {
font-family: ‘Lato’, sans-serif;
font-size: 12px;
font-weight: 400;
padding: 10px;
text-align: left;
width: 0%;
border-bottom: 1px solid black;
}
</style>
The api responses have key of id as below.
/api/events
{
"count": 80,
"next": null,
"previous": null,
"results": [{
"id": 103,
"sport_id": "9",
"event_name": "Guilherme Clezar vs Uladzimir Ignatik",
"start_date": "12-11-19",
"status": "open",
"event_id": 1273804660340017
}]
}
/api/markets
{
"count": 128,
"next": "http://localhost:8000/api/markets/?page=2",
"previous": null,
"results": [{
"id": 151,
"market_id": 1273804670840017,
"market_name": "Moneyline",
"status": "open",
"volume": 1107.5453,
"event": 103
}]
}
Key is used to calculate differences in DOM when re rendering. Keys have to be unique in scope a parent DOM node (I guess).
Try prefixing your keys with something.
v-for="(event, index) in events" :key="`event_${id}`"
EDIT:
Actually the reason is probably that your id is not defined anywhere. Replace it with index.
Besides, are you sure you can use three loops on the same element? Never tried it, im always concatenating things in a computed prop... Interesting approach!
As pointed out in the comments its not possible to have multiple v-for statements on the same element, you would need something like this instead:
<div class="course-list-row">
<th style="width:5%"> Start date </th>
<th style="width:5%"> Event name</th>
<th scope="col">Market</th>
<th scope="col">Runner</th>
<tr v-for="(event, index) in events" :key=id>
<td>{{ event.start_time }} </td>
<td>{{ event.event_name }} </td>
<td></td>
<td></td>
</tr>
<tr v-for="(market, index) in markets" :key=id>
<td></td>
<td></td>
<td>{{ market.name }} </td>
<td></td>
</tr>
<tr v-for="(runner, index) in runners" :key=id>
<td></td>
<td></td>
<td></td>
<td>{{ runner.name }} </td>
</tr>
</div>
OR
To avoid using verbose templating as above, you could merge all three arrays into a single array, where each object has an id and a type and render as follows:
<tr v-for="(entry, index) in combined" :key=id>
<td>{{entry.type == 'event' ? event.start_time : ''}} </td>
<td>{{ entry.type == 'event' ? event.event_name : ''}} </td>
<td>{{ entry.type = 'market' ? market.name : ''}}</td>
<td>{{ entry.type == 'runner' ? runner.name : ''}}</td>
</tr>
Though I think this has already been covered in the comments I'll quickly recap some of the problems with the original code.
An HTML element can't have the same attribute multiple times. While Vue templates are not technically HTML they do try to conform to roughly the same parsing rules, so duplicates are discarded. The original code has both v-for and key attributes duplicated on the <tr>.
Another problem is the key value. Writing :key=id will try to set the key to equal the value of a property called id. But where is this property defined? By default Vue will be looking for it on the Vue instance itself. I suspect what you meant was for it to use the id property of the event, market or runner. For that you would need to write :key="event.id", etc..
The question does leave some ambiguity as to what precisely it is you're trying to achieve but the code below is my best guess for what you want:
new Vue({
template: `
<table border>
<thead>
<th>Start date</th>
<th>Event name</th>
<th>Market</th>
<th>Runner</th>
</thead>
<tbody>
<template v-for="event in events">
<tr :key="'event' + event.id">
<td>{{ event.start_date }}</td>
<td>{{ event.event_name }}</td>
</tr>
<template v-for="market in marketsFor[event.id]">
<tr :key="'market' + market.id">
<td></td>
<td></td>
<td>{{ market.market_name }} </td>
</tr>
<tr v-for="runner in runnersFor[market.id]" :key="'runner' + runner.id">
<td></td>
<td></td>
<td></td>
<td>{{ runner.runner_name }}</td>
</tr>
</template>
</template>
</tbody>
</table>
`,
el: '#app',
data () {
return {
events: [
{
"id": 103,
"event_name": "Guilherme Clezar vs Uladzimir Ignatik",
"start_date": "12-11-19"
}, {
"id": 104,
"event_name": "Event 2",
"start_date": "13-11-19"
}
],
markets: [
{
"id": 151,
"market_name": "Moneyline",
"event": 103
}, {
"id": 152,
"market_name": "Market 2",
"event": 103
}, {
"id": 153,
"market_name": "Market 3",
"event": 104
}
],
runners: [
{
"id": 1,
"runner_name": "Runner 1",
"market": 151
}, {
"id": 2,
"runner_name": "Runner 2",
"market": 151
}, {
"id": 3,
"runner_name": "Runner 3",
"market": 152
}, {
"id": 4,
"runner_name": "Runner 4",
"market": 153
}
]
}
},
computed: {
marketsFor () {
const markets = {}
for (const market of this.markets) {
const event = market.event
markets[event] = markets[event] || []
markets[event].push(market)
}
return markets
},
runnersFor () {
const runners = {}
for (const runner of this.runners) {
const market = runner.market
runners[market] = runners[market] || []
runners[market].push(runner)
}
return runners
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app"></div>
I've had to rename some properties, e.g. start_time to start_date and market.name to market.market_name. The properties in the template didn't seem to match the example data.
The core of my solution is a pair of computed properties that generate simple object maps that can be used to find the corresponding children at each level. There are various alternatives to this but it seems that the heart of any solution is finding a way to navigate the hierarchy from within the template. The template in my example could be simplified but at the expense of moving the complexity into the JavaScript portion of the component.

Binding array items in Vue.js

I have an arry which contains some array and proporties in it.
Now I want to iterate through the array and create new rows for each of items.
This is what I am doing.
var orderDetails = [{
"ID": "1",
"NAME": "Item1",
"Orders": [
"ORD001"
],
"Purchases": [
"RhynoMock",
"Ember"
],
"ISENABLED": "false"
},
{
"ID": "2",
"NAME": "Item2",
"Orders": [
"ORD002",
"ORD008"
],
"Purchases": [
"StufferShop"
],
"ISENABLED": "false"
}
];
var app = new Vue({
el: '#ordersDiv',
data: {
allOrders: orderDetails
}
});
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="ordersDiv">
<table>
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr v-for="order in allOrders">{{ order.NAME }}</tr>
</table>
</div>
Getting the error,
[Vue warn]: Error in render: "TypeError: Cannot read property 'NAME' of undefined"
Your HTML is invalid, and it seems that Vue's template compiler or DOM renderer has trouble with it: <tr> must only have <td> or <th> child elements, it cannot have text node children.
Try this instead:
<tr v-for="order in allOrders">
<td>{{ order.NAME }}</td>
</tr>

AngularJS: Read Json data from HTML table

Using angularjs here:
I am creating a table which has 2 static columns (ID & Comment) and rest columns are dynamically created by clicking a button. User can enter data in the rows of this table. I want to read/extract the data from the table that the user has entered.
I have created a jsfiddle at: http://jsfiddle.net/ajnh8bo5/7/
Click on add tab, give a name, and then click add columns
Reading json as:
$scope.read = function() {
return JSON.stringify($scope.targetTable);
};
Currently my generated json just gives information about the dynamic columns that was added. See json below:
{
"id":0,
"name":"Table 1",
"columns":[
{
"id":0,
"label":"Column 0",
"$$hashKey":"object:11"
}
],
"rows":[
{
"0":"2",
"$$hashKey":"object:9",
"undefined":"1",
"id":1037
}]
The above json is generated when I add just one dynamic columns. Here under rows array "0":"2" means that the first dynamic column has a value of "2". But it does not shows the data that was entered for ID & Comment column. I know I am missing something here but not sure how to proceed.
Another thing which is less important at this moment is also the json to spit out the column name as well.
---updated---
Adding desired output:
{
"columns": [
{
"id": 0,
"label": "ID"
},
{
"id": 1,
"label": "Column 1"
},
{
"id": 2,
"label": "Column 2"
},
{
"id": 4,
"label": "Comment"
}
],
"rows": [
{
"Id": "1",
"Column 1": "2",
"Column 2": "3",
"Comment": "user comment"
}
]
}
The above json shows I have 2 static columns Id & Comment. Column1 & Column2 are the dynamic ones.
If anyone cant provide me any solution based on the above JSON. Can anyone just let me know how can I just output static columns ID & Comment in my json.
--Updated with relevant code---
Below is the HTML table:
<table class="table table-bordered" ng-if="targetTable">
<thead>
<tr>
<th>ID #</th>
<th contenteditable="true" ng-repeat="c in targetTable.columns" ng-model="c.label"></th>
<th class="comment-fixed-width" ng-model="comment">Comment</th>
<td class="center add-column fixed-width"><a ng-href ng-click="addColumn()">+ Add Column</a></td>
<td class="comment-fixed-width" contenteditable="true" ></td>
</tr>
</thead>
<tbody>
<tr ng-repeat="r in targetTable.rows">
<td class="fixed-width" contenteditable="true" ng-model="r[column.id]"></td>
<td class="fixed-width" contenteditable="true" ng-repeat="column in targetTable.columns" ng-model="r[column.id]" ng-blur="!r.id? addNewRow(r[column.id], r): undefined"></td>
<td class="comment-fixed-width" contenteditable="true" ></td>
<td class="blank fixed-width" colspan="2" ng-model="r[column.id]"></td>
</tr>
</tbody>
</table>
AngularJs Code:
function createTable() {
tableCounter++;
return {
id: currentTableId++,
name: `Table ${currentTableId}`,
columns: [],
rows: [{}],
uniqueIdCounter: 1037
}
While creating a new tab I am creating an instance of table as:
$scope.tables.push(createTable());
$scope.tables = [];
$scope.targetTable = null;
//When I click add dynamic column button I use the below code.
$scope.addColumn = function() {
if (tableCounter) {
var columns = $scope.targetTable.columns,
id = columns.length;
$scope.targetTable.columns.push({
id: columns.length,
label: `Column ${id}`
});
}
};
//Below code is for adding a new row
$scope.addNewRow = function(value, row) {
if (tableCounter) {
if (!value || value === "" || typeof value !== 'string') return;
$scope.targetTable.rows.push({});
row.id = $scope.targetTable.uniqueIdCounter++;
}
};
Anyone with inputs?
I didn't want to overhaul your code, so I just made some small tweaks in your HTML code:
JSFiddle: http://jsfiddle.net/0oerpd5u/
<tbody>
<tr ng-repeat="r in targetTable.rows">
<td class="fixed-width" contenteditable="true" ng-model="r.id"></td>
<td class="fixed-width" contenteditable="true" ng-repeat="column in targetTable.columns" ng-model="r[column.id]" ng-blur="!targetTable.rows[$index+1].id? addNewRow(r[column.id], r): ''"></td>
<td class="comment-fixed-width" contenteditable="true" ng-blur="!targetTable.rows[$index+1].id?addNewRow(r.comment, r):''" ng-model="r.comment">></td>
<td class="blank fixed-width" colspan="2" ng-model="r[column.id]"></td>
</tr>
</tbody>
and in your JS:
var uniqueIdCounter =1037;
function createTable() {
tableCounter++;
return {
id: currentTableId++,
name: `Table ${currentTableId}`,
columns: [],
rows: [{id: uniqueIdCounter}],
uniqueIdCounter: uniqueIdCounter
}
}
$scope.addNewRow = function(value, row) {
if (tableCounter) {
if (!value || value === "" || typeof value !== 'string') return;
$scope.targetTable.rows.push({id :++$scope.targetTable.uniqueIdCounter});
}
};

Dynamic ng-repeat in AngularJS

My angular controller is
$scope.dyna = [
{ "name": "parshuram", "age": 24 },
{ "name": "Tejash", "age": 26 },
{ "name": "Vinayak", "age": 25 }
];
My html
<table>
<tbody>
<tr ng-repeat="test in dyna">
<td>{{test.name}}</td>
<td>{{test.age}}</td>
</tr>
</tboody>
</table>
This works correctly, and outputs
Parshuram 24
Tejash 26
But if an another variable is added to my scope variable, I need to make changes in my html table:
$scope.dyna = [
{ "name": "parshuram", "age": 24 ,"void": true},
{ "name": "Tejash", "age": 26,"void" : false }
];
<table>
<tbody>
<tr ng-repeat= "test in dyna">
<td>{{test.name}}</td>
<td>{{test.age}}</td>
<!-- I don't want to have to add this, the columns should be added dynamically -->
<td>{{test.void}}</td>
</tr>
</tboody>
</table>
In that case, can the columns be generated dynamically, for example by getting all my object variables from the scope?
ng-repeat can loop over object key/values as well:
<table>
<tbody>
<tr ng-repeat= "test in dyna">
<td ng-repeat="(key, value) in test">
{{value}}
</td>
</tr>
</tbody>
</table>
However as noted in the docs linked above, there are a few limitations compared to an ng-repeat that works on arrays:
The JavaScript specification does not define the order of keys
returned for an object, so Angular relies on the order returned by the
browser when running for key in myObj. Browsers generally follow the
strategy of providing keys in the order in which they were defined,
although there are exceptions when keys are deleted and reinstated.
See the MDN page on delete for more info.
ngRepeat will silently ignore object keys starting with $, because
it's a prefix used by Angular for public ($) and private ($$)
properties.
The built-in filters orderBy and filter do not work with objects, and
will throw an error if used with one.
You should be able to do that with (key,value) iteration.
Would be nice to have fiddle to verify but would be something like:
<tr ng-repeat= "test in dyna">
<td ng-repeat="(key,value) in test">{{value}}</td>
</tr>
If at 'runtime', I don't know. Otherwise:
<div ng-controller="MyCtrl">
<table>
<tbody>
<tr ng-repeat= "test in dyna">
<td ng-repeat="key in objectKeys(test)">{{test[key]}}</td>
</tr>
</tbody>
</table>
</div>
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.dyna = [
{ "name": "parshuram", "age": 24 },
{ "name": "Tejash", "age": 26 },
{ "name": "Vinayak", "age": 25 }
];
$scope.objectKeys = function (object) {
var keys = Object.keys(object);
keys.splice(keys.indexOf('$$hashKey', 1))
return keys;
}
}
fiddle
HTML
<html>
<script src="library/angular.min.js"></script>
<script src="practice.js"></script>
<head>
</head>
<body ng-app="app">
<div ng-controller="test1" ng-bind-html="result">
</div>
</body>
</html>
Angularjs
angular.module('app',[]).controller('test1',['$scope','$compile','$sce',function($scope,$compile,$sce){
$scope.dyna = [
{ "name": "parshuram", "age": 24 },
{ "name": "Tejash", "age": 26 },
{ "name": "Vinayak", "age": 25 }
];
$scope.result="<table><tbody>";
for(var i=0;i<$scope.dyna.length;i++){
$scope.result+="<tr>";
for(var key in $scope.dyna[i])
if($scope.dyna[i].hasOwnProperty(key))
$scope.result+='<td>'+$scope.dyna[i][key]+'</td>';
$scope.result+="</tr>";
}
$scope.result+="</tbody></table>";
$scope.result=$sce.trustAsHtml($scope.result);
}]);
This is another way, creating html in controller.
just make it again put another ng-repeat in your loop for the test variable:
$scope.dyna = [{ "name": "parshuram", "age": 24 ,"void": true}, { "name": "Tejash", "age": 26,"void" : false }];
<table>
<tbody>
<tr ng-repeat= "test in dyna">
<td ng-repeat="t in test">{{test[t]}</td> // just like this
</tr>
</tboody>
</table>

Selecting parent row unless child rows exist in Angular Table

I have a table that displays selectable information. Sometimes there are child rows that are selectable.
I want the parent rows to be selectable if they have no children, otherwise only the child rows should be selectable. This is a select-only-one type of table.
Right now the parent rows are selectable, but when the row with the child rows is selected, everything is selected. I am using nested ng-repeats so this complicates matters.
Here is a plunker.
http://plnkr.co/edit/baCIxeJB5JeVAJU8O7Hy?p=preview
The select isn't working in the plunker but it is on my machine... I'm running Angular 1.4.7 and ui_bootstrap 1.1.2. However I think it is enough to see what is going on.
Here is the markup:
<div ng-controller="DemoCtrl">
<table class="table">
<thead>
<tr>
<th>S</th>
<th>R</th>
<th>Se</th>
<th>D</th>
<th>Ser</th>
<th>L</th>
</tr>
</thead>
<tbody ng-repeat="x in pro" ng-class="{'selected':$index == selectedRow}" ng-click="setClickedRow($index)">
<tr >
<td><b>{{x.a}}</b></td>
<td>{{x.b}}</td>
<td><u>{{x.c}}</u></td>
<td>{{x.d}}</td>
<td>{{x.e}}</td>
<td>{{x.f}}</td>
<tr ng-repeat = "details in x.jobs">
<td></td>
<td></td>
<td></td>
<td></td>
<td>{{details.name}}</td>
<td>{{details.jobs}}</td>
</tr>
</tbody>
</table>
</div>
Here is the controller
$scope.setClickedRow = function(index){
$scope.selectedRow = ($scope.selectedRow == index) ? null : index;
};
$scope.pro = [
{
a : "G",
b : "123",
c : "S1",
d : "D6",
e : "O1",
f : "B",
jobs : [
{
"name": "Wakeup",
},
{
"name": "work 9-5",
}
]
},
{
a : "R",
b : "456",
c : "S2",
d : "D5",
e : "O2",
f : "B",
jobs : [
]
},
{
a : "G",
b : "789",
c : "S3",
d : "D4",
e : "O3",
f : "P",
jobs : [
{
"name": "Sleep",
},
{
"name": "get ready for bed",
}
]
},
];
}
tbody
<tbody>
<tr ng-repeat-start="(pIndex, x) in pro" ng-class="{'selected': parentSelected[$index]}" ng-click="setParentClickedRow(x, $index)">
<td>
<b>{{x.a}}</b>
</td>
<td>{{x.b}}</td>
<td>
<u>{{x.c}}</u>
</td>
<td>{{x.d}}</td>
<td>{{x.e}}</td>
<td>{{x.f}}</td>
</tr>
<tr ng-repeat-end ng-repeat="details in x.jobs" ng-class="{'selected': childSelected[pIndex][$index]}" ng-click="setChildClickedRow(pIndex, $index)">
<td></td>
<td></td>
<td></td>
<td></td>
<td>{{details.name}}</td>
<td>{{details.jobs}}</td>
</tr>
</tbody>
controller snippet
$scope.parentSelected = [];
$scope.childSelected = [];
$scope.setParentClickedRow = function(x, index) {
if(!x.jobs || x.jobs.length === 0) {
$scope.parentSelected[index] = !$scope.parentSelected[index];
}
};
$scope.setChildClickedRow = function(parentIndex, index) {
$scope.childSelected[parentIndex] = $scope.childSelected[parentIndex] || [];
$scope.childSelected[parentIndex][index] = !$scope.childSelected[parentIndex][index];
}
Previously, your tbody element was applied class selected. It was a mistake. Also, there was no attempt for checking/distinguishing between a parent row and child rows.
I replaced the ng-repeat with ng-repeat-start and started iterating the tr. It has nothing to do with the logic, it's just that I did't want to do iteration with tbody and generate tbody for each iteration.
The logic is in setParentClickedRow and setChildClickedRow function. In setParentClickedRow function, check if that parent object has child obj(length > 0) that created child rows. If not, we make it selectable by changing parentSelected array. In setChildClickedRow function, we just change childSelected (two dimensional array) to make a child row selectable.
See this Plunker.

Categories

Resources