I'm creating a table that uses data from a json, the json "policies" change when I click on different links in the page, the thing is that when I click and the state change, I have to generate the table again with the new json, but I get an
Uncaught Error: Invariant Violation: processUpdates(): Unable to find child 1 of element. This probably means the DOM was unexpectedly mutated (e.g., by the browser), usually due to forgetting a <tbody> when using tables, nesting tags like <form>, <p>, or <a>, or using non-SVG elements in an <svg> parent. Try inspecting the child nodes of the element with React ID .0.1.0.2.3.1.1.2.0.1.
The first time the page loads the table is correctly generated.
module.exports = React.createClass({
onPoliciesChange: function (policiesStore) {
this.setState(policiesStore);
},
getInitialState: function () {
return {
policies: []
};
},
componentDidMount: function () {
this.unsubscribeAlertsStore = AlertsStore.listen(this.onPoliciesChange);
},
componentWillUnmount: function () {
this.unsubscribeAlertsStore();
},
cols: [
{ key: 'name', label: 'Name'},
{ key: 'severity', label: 'Severity' },
{ key: 'width', label: 'Width' },
{ key: 'pulsate', label: 'Pulsate' }
],
generateHeaders: function () {
var cols = this.cols; // [{key, label}]
// generate our header (th) cell components
return cols.map(function (colData) {
return <th key={colData.key}> {colData.label} </th>;
});
},
generateRows: function () {
var slf = this;
var cols = this.cols, // [{key, label}]
data = this.data;
//per each item
return this.state.policies.map(function (item) {
// handle the column data within each row
var cells = cols.map(function (colData) {
return <td> {item[colData.key]} </td>;
});
return <tr key={item.id}> {cells} </tr>;
});
},
render: function () {
var headerComponents = this.generateHeaders(),
rowComponents = this.generateRows();
return (
<table className="table table-condensed table-striped">
<thead> {headerComponents} </thead>
<tbody> {rowComponents} </tbody>
</table>
);
}
});
I just move the from the render to the function that creates the rows:
generateRows: function () {
var severity = this.renderSeverity();
var slf = this;
var cols = this.cols, // [{key, label}]
data = this.data;
//per each item
return this.state.policies.map(function (item) {
// handle the column data within each row
var cells = cols.map(function (colData) {
if (colData.key === 'width') {
//return <td> <input type="checkbox" name="vehicle" value="Bike" onChange={slf.onChange.bind(null, id) }/></td>;
return <td> <input type="checkbox" onChange={slf.onChangeWidth.bind(null, item.id) }/></td>;
} else if (colData.key === 'pulsate') {
return <td> <input type="checkbox" onChange={slf.onChangePulsate.bind(null, item.id) }/></td>;
} if (colData.key === 'policyCheck') {
return <td> <input type="checkbox" onChange={slf.onChangePolicy.bind(null, item.id) }/></td>;
} else if (colData.key === 'severity') {
// colData.key might be "firstName"
return <td>{item[colData.key]} {slf.renderSeverity(item.severity) }</td>;
} else {
// colData.key might be "firstName"
return <td> {item[colData.key]} </td>;
}
});
return <tbody><tr key={item.id}> {cells} </tr></tbody>;
});
}
Related
I am new to knockout and I am stuck at a problem for last couple of days - I am sure it is something silly but cant figure out. Any help will be appreciate.
I am trying to select and deselect all rows in a table based on the header check box column. The SelectAll function works and selects/unselects all rows in table but the header remains unckecked?
<tr>
<th><input type="checkbox" data-bind="click: selectAll, checked: AllChecked"></th>
<th>#Html.Vocab("Document")</th>
<th>#Html.Vocab("Notes")</th>
<th>#Html.Vocab("Created")</th>
</tr>
<tbody data-bind="foreach: DocumentRows">
<tr >
<td><input type="checkbox" data-bind="checked: IsSelected"></td>
<td><data-bind="text: Notes"></td>
</tr>
</tbody>
And here is the script:
//Document
class Document {
Id: KnockoutObservable<number>;
Notes: KnockoutObservable<string>;
IsSelected: KnockoutObservable<boolean>;
constructor(data?) {
this.Id = ko.observable(0);
this.Notes = ko.observable("").extend({ defaultValue: "" });
this.IsSelected = ko.observable(false);
if (data != null) {
ko.mapping.fromJS(data, {}, this);
}
}
};
//DocumentS VIEW MODEL
class DocumentsViewModel {
DocumentRows: KnockoutObservableArray<Document>;
IsAnySelected: KnockoutObservable<boolean>;//used for delete button
constructor(params) {
this.DocumentRows = ko.observableArray([]);
this.selectedIds = ko.observableArray([]);
}
InitComputed = () => {
this.AllChecked= ko.pureComputed({
read: function () {
return this.selectedIds().length === this.DocumentRows().length;
},
write: function (value) {
this.selectedIds(value ? this.DocumentRows.slice(0) : []);
},
owner: this
}
this.IsAnySelected = ko.pureComputed(() => {
var isChecked = false;
ko.utils.arrayForEach(this.DocumentRows(), function (item) {
if (item.IsSelected()) {
isChecked = true;
}
});
return isChecked;
});
}
selectAll = (): void => {
if (this.selectedIds().length > 0) {
this.selectedIds.removeAll();
ko.utils.arrayForEach(this.DocumentRows(), function (item) {
item.IsSelected(false);
});
} else {
ko.utils.arrayPushAll(this.selectedIds(), this.DocumentRows())
ko.utils.arrayForEach(this.DocumentRows(), function (item) {
item.IsSelected(true);
});
}
}
}
I am trying to push and splice the elements based on checkall, single checkbox clicked, my problem is I am getting a list from angularjs post request and displayed it using ng-repeat I have given provision to enter some text in a new column along with ng-repeat data. Now based on the user selection of checkall or single checkbox clicked I am pushing the data into array. Here I am able to push the data when the user clicked on single checkbox, but when the user clicked on chekall checkbox 0, 1 are pushing the array instead of textbox value. Any help will be greatly appreciated.
Html
<table class='reportstd' align='center' width='80%'>
<tr class='trdesign'>
<td>
<input type="checkbox" name="checkAll" id="all" data-ng-model="checkedAll" data-ng-change="toggleCheckAll()" />
</td>
<td> Sl No</td>
<td> RO No.</td>
<td> Truck No.</td>
</tr>
<tr data-ng-repeat="user in RosList">
<td> <input type="checkbox" value="{{user.do_ro_no}}" data-ng-model="user.checked" data-ng-change="modifyArrayToPost(user,truck_no[$index])" /> </td>
<td>{{$index + 1}}</td>
<td>{{user.do_ro_no}}</td>
<td><input type='text' data-ng-model="truck_no[$index]" id="truck_no_{{$index}}" name="truck_no_{{$index}}" value=""></td>
</tr>
</table>
<table>
<tr>
<td colspan='2'><input type="submit" id="btn_submit" name='sea' value='Search' data-ng-submit="postROs(arrayToPost)" /></td>
</tr>
</table>
Angularjs
$scope.arrayToPost = [];
$scope.toggleCheckAll = function() {
if ($scope.checkedAll) {
angular.forEach($scope.RosList, function(user, truckno) {
user.checked = true;
$scope.modifyArrayToPost(user, truckno);
});
} else {
angular.forEach($scope.RosList, function(user, truckno) {
user.checked = false;
$scope.modifyArrayToPost(user, truckno);
});
}
}
$scope.modifyArrayToPost = function(user, truckno) {
if (user.checked && truckno != null && $scope.arrayToPost.indexOf(user.do_ro_no) == -1) {
$scope.arrayToPost.push(user.do_ro_no, truckno);
} else if (!user.checked) {
$scope.arrayToPost.splice($scope.arrayToPost.indexOf(user.do_ro_no, truckno), 2);
}
}
$scope.$watch('RosList', function() {
var allSet = true;
var allClear = true;
angular.forEach($scope.RosList, function(user, truckno) {
if (user.checked) {
allClear = false;
} else {
allSet = false;
}
});
var checkAll = $element.find('#all');
checkAll.prop('indeterminate', false);
if (allSet) {
$scope.checkedAll = true;
} else if (allClear) {
$scope.checkedAll = false;
} else {
$scope.checkedAll = false;
checkAll.prop('indeterminate', true);
}
}, true);
$scope.RosList = [
{do_ro_no: "217PALV000201898", slno: 1, },
{do_ro_no: "317PALV000201898", slno: 2, }
]
truck_no model is not coming from RosList.
You should initialize truck_no in your controller as $scope.truck_no = [] in order to access the values, and in your $scope.toggleCheckAll function change $scope.modifyArrayToPost(user, truckno); to $scope.modifyArrayToPost(user, $scope.truck_no[truckno]);
EDIT:
I've slightly modified your code to handle all cases.
Demo: https://next.plnkr.co/edit/DnzsCFkPQU8ByFZ8
If I understand the issue correctly, I think that the solution is much simpler. The main confusation is that there is not just only one "source of truth" - you hold a state for each row and also all the do_ro_no's.
I suggest to keep track only for each row and calculate the arrayToPost whenever you need.
Like this:
angular.module('app', []).controller('ctrl', ($scope, $element) => {
$scope.truck_no = [];
$scope.RosList = [{
do_ro_no: "217PALV000201898",
slno: 1,
},
{
do_ro_no: "317PALV000201898",
slno: 2,
}
];
$scope.getTruckNo = () => {
return $scope.truck_no.filter((t, index) => {
return $scope.RosList[index].checked;
});
}
$scope.getArrayToPost = () => {
return $scope.RosList
.filter(ros => ros.checked)
.map(ros => ros.do_ro_no);
}
$scope.arrayToPost = [];
$scope.toggleCheckAll = function() {
if ($scope.checkedAll) {
//angular.forEach($scope.RosList, function(user, truckno) {
// user.checked = true;
// $scope.modifyArrayToPost(user, truckno);
//});
$scope.RosList.forEach(ros => ros.checked = true);
} else {
//angular.forEach($scope.RosList, function(user, truckno) {
// user.checked = false;
// $scope.modifyArrayToPost(user, truckno);
//});
$scope.RosList.forEach(ros => ros.checked = false);
}
}
//$scope.modifyArrayToPost = function(user, truckno) {
// if (user.checked && truckno != null && $scope.arrayToPost.indexOf(user.do_ro_no) == -1) {
// $scope.arrayToPost.push(user.do_ro_no, truckno);
// } else if (!user.checked) {
// $scope.arrayToPost.splice($scope.arrayToPost.indexOf(user.do_ro_no, truckno), 2);
// }
//}
//$scope.$watch('RosList', function() {
// var allSet = true;
// var allClear = true;
// angular.forEach($scope.RosList, function(user, truckno) {
// if (user.checked) {
// allClear = false;
// } else {
// allSet = false;
// }
// });
//
// var checkAll = $element.find('#all');
// checkAll.prop('indeterminate', false);
// if (allSet) {
// $scope.checkedAll = true;
// } else if (allClear) {
// $scope.checkedAll = false;
// } else {
// $scope.checkedAll = false;
// checkAll.prop('indeterminate', true);
// }
//}, true);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<table class='reportstd' align='center' width='80%'>
<tr class='trdesign'>
<td>
<input type="checkbox" name="checkAll" id="all" data-ng-model="checkedAll" data-ng-change="toggleCheckAll()" />
</td>
<td> Sl No</td>
<td> RO No.</td>
<td> Truck No.</td>
</tr>
<tr data-ng-repeat="user in RosList">
<td> <input type="checkbox" value="{{user.do_ro_no}}" data-ng-model="user.checked" data-ng-change="modifyArrayToPost(user,truck_no[$index])" /> </td>
<td>{{$index + 1}}</td>
<td>{{user.do_ro_no}}</td>
<td><input type='text' data-ng-model="truck_no[$index]" id="truck_no_{{$index}}" name="truck_no_{{$index}}" value=""></td>
</tr>
</table>
<table>
<tr>
<td colspan='2'><input type="submit" id="btn_submit" name='sea' value='Search' data-ng-submit="postROs(arrayToPost)" /></td>
</tr>
</table>
<pre>
{{getTruckNo() | json}}
</pre>
</div>
The array is the result of getTruckNo() as you can see in the snippet.
I want to delete a row from html table on button click using ReactJS. The problem is that on clicking delete button always last row is being deleted instead of the row that is clicked. Please tell what is the issue with my code?
code:
var RecordsComponent = React.createClass({
getInitialState: function() {
return {
rows: ['row1', 'row2', 'row3'],
newValue: "new value"
}
},
render : function() {
return (
<div>
<table>
<tbody>
{this.state.rows.map((r) => (
<tr>
<td>{r}</td>
<td>
<button onClick={this.deleteRow}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
<input trype="text" id={"newVal"} onChange={this.updateNewValue}></input>
<button id="addBtn" onClick={this.addRow}>ADD</button>
</div>
);
},
updateNewValue: function(component) {
this.setState({
newValue: component.target.value
});
},
addRow : function() {
var rows = this.state.rows
rows.push(this.state.newValue)
this.setState({rows: rows})
},
deleteRow : function(record) {
var index = -1;
var clength = this.state.rows.length
for( var i = 0; i < clength; i++ ) {
if( this.state.rows[i].value === record.target.value ) {
index = i;
break;
}
}
var rows = this.state.rows
rows.splice( index, 1 )
this.setState( {rows: rows} );
}
});
React.render(<RecordsComponent/>, document.getElementById('display'))
You need pass a param on func deleteRow
<button onClick={() => this.deleteRow(r)}>Delete</button>
I refactor a litte bit of your func
deleteRow : function(record) {
this.setState({
rows: this.state.rows.filter(r => r !== record)
});
}
In deleteRow you are looking for the index of the row that matches record.target.value (record is actually an event), but event.target.value is blank so the index remains -1, and rows.splice(-1, 1) deletes the last element.
You should pass the row data itself, like:
<button onClick={e => this.deleteRow(r)}>Delete</button>
I'm looking to implement a table in ReactJS with the following features:
initially empty
rows are dynamically added and removed
when there are no rows, an empty state (e.g. a box saying "Table empty") should be displayed
when a row is removed, there should be a fade out transition
when the first row is added, there should be no fade out transition on the empty state
I came up with two approaches using ReactCSSTransitionGroup.
1. Wrap only rows into ReactCSSTransitionGroup
Codepen: https://codepen.io/skyshell/pen/OpVwYK
Here, the table body is rendered in:
renderTBodyContent: function() {
var items = this.state.items;
if (items.length === 0) {
return (
<tbody><tr><td colSpan="2">TABLE EMPTY</td></tr></tbody>
);
}
const rows = this.state.items.map(function(name) {
return (
<tr key={name}>
<td>{name[0]}</td>
<td>{name[1]}</td>
</tr>
);
});
return (
<ReactCSSTransitionGroup
component="tbody"
transitionName="example"
transitionEnter={false}
transitionLeave={true}>
{rows}
</ReactCSSTransitionGroup>
);}
The issue is that the last row to be removed does not get the fade out transition before disappearing since the ReactCSSTransitionGroup is not rendered when item.length === 0.
2. Wrap table body into ReactCSSTransitionGroup
Codepen: https://codepen.io/skyshell/pen/RpbKVb
Here, the entire renderTBodyContent method is wrapped into ReactCSSTransitionGroup within the render method:
<ReactCSSTransitionGroup
component="tbody"
transitionName="example"
transitionEnter={false}
transitionLeave={true}>
{this.renderTBodyContent()}
</ReactCSSTransitionGroup>
And the RenderTBody method looks like:
renderTBodyContent: function() {
var items = this.state.items;
if (items.length === 0) {
return (
<tr><td colSpan="2">TABLE EMPTY</td></tr>
);
}
const rows = this.state.items.map(function(name) {
return (
<tr key={name}>
<td>{name[0]}</td>
<td>{name[1]}</td>
</tr>
);
});
return rows;}
The issue is that the empty state gets animated too.
Any suggestions on how to obtain the desired behaviour?
Thanks!
Thank you realseanp for your pointers. Using the low level API and TweenMax instead of CSS transitions, I came up with the following solution. First, introduce a Row component:
var Row = React.createClass({
componentWillLeave: function(callback) {
var el = React.findDOMNode(this);
TweenMax.fromTo(el, 1, {opacity: 1}, {opacity: 0, onComplete: callback})
},
componentDidLeave: function() {
this.props.checkTableContent();
},
render: function() {
const name = this.props.name;
return (
<tr>
<td>{name[0]}</td>
<td>{name[1]}</td>
</tr>
);
}
});
Then populate the table based on an isEmpty flag:
var Table = React.createClass({
getInitialState: function() {
return {
items: [],
isEmpty: true
};
},
addRow: function() {
var items = this.state.items;
var firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
var lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
items.push([firstName, lastName]);
this.setState({items: items, isEmpty: false});
},
removeLastRow: function() {
var items = this.state.items;
if (items.length != 0) {
items.splice(-1, 1);
this.setState({items: items});
}
},
checkTableContent: function() {
if (this.state.items.length > 0) {
this.setState({isEmpty: false});
}
else {
this.setState({isEmpty: true});
this.forceUpdate();
}
},
renderTBodyContent: function() {
if (this.state.isEmpty) {
return (
<tr><td colSpan="2">TABLE EMPTY</td></tr>
);
}
var that = this;
const rows = this.state.items.map(function(name) {
return <Row
key={name}
name={name}
checkTableContent={that.checkTableContent} />;
});
return rows;
},
render: function() {
return (
<div>
<button onClick={this.addRow}>Add row</button>
<button onClick={this.removeLastRow}>Remove row</button>
<table>
<thead>
<tr>
<th>First name</th>
<th>Last name</th>
</tr>
</thead>
<ReactTransitionGroup
component="tbody"
transitionName="example"
transitionEnter={false}
transitionLeave={true}>
{this.renderTBodyContent()}
</ReactTransitionGroup>
</table>
</div>
);
}
});
Codepen: https://codepen.io/skyshell/pen/yMYMmv
I am completely stumped on how to achieve something specific that the below website has achieved. Does anyone know how to update the quantity of a product from the product details page to the shopping cart, and have that quantity shared/bound between the cart and and the product details page for each an every product repeated from a collection. (I am not talking about simply having a global cart quantity total via a simple custom directive). Please see the link below. Add a product to the cart and then update the quantity from both the product page and from the cart itself. This is what I am trying to achieve. Thank you all in advance!
http://demo.shopnx.in/
Typically you'll get better responses if you post some code that you have tried and then ask to be guided on where you are going wrong. I've created a simple JSFiddle to demonstrate one method of doing this. It is extremely simple, contrived, not production worthy by any stretch of the imagination and doesn't really do much, but it should show you one construct that will allow you to accomplish the functionality you're after.
The key is to use some type of shared storage so that the same array of items is available to both your product listing and the cart. In the sample I have done this using a Value:
.value('cartStorage', {
items: []
})
This value is then injected in the main controller:
.controller('mainController', function(cartStorage) {
var _this = this;
_this.cartStorage = cartStorage;
_this.items = [{
name: 'Apple',
price: .5,
quantity: 0,
showAddToCart: false,
addedToCart: false
}, {
name: 'Orange',
price: .5,
quantity: 0,
showAddToCart: false,
addedToCart: false
}, {
name: 'Grapes',
price: 1,
quantity: 0,
showAddToCart: false,
addedToCart: false
}];
_this.addToCart = function(item) {
_this.cartStorage.items.push(item);
item.addedToCart = true;
}
_this.increaseItemAmount = function(item) {
item.quantity++;
item.showAddToCart = true;
}
_this.decreaseItemAmount = function(item) {
item.quantity--;
if (item.quantity <= 0) {
item.quantity = 0;
item.addedToCart = false;
item.showAddToCart = false;
var itemIndex = _this.cartStorage.items.indexOf(item);
if (itemIndex > -1) {
_this.cartStorage.items.splice(itemIndex, 1);
}
} else {
item.showAddToCart = true;
}
}
})
As well as the cart controller:
.controller('cartController', function(cartStorage) {
var _this = this;
_this.cartStorage = cartStorage;
_this.increaseItemAmount = function(item) {
item.quantity++;
}
_this.decreaseItemAmount = function(item) {
item.quantity--;
if (item.quantity <= 0) {
item.quantity = 0;
item.addedToCart = false;
item.showAddToCart = false;
var itemIndex = _this.cartStorage.items.indexOf(item);
if (itemIndex > -1) {
_this.cartStorage.items.splice(itemIndex, 1);
}
}
}
_this.removeFromCart = function(item) {
item.quantity = 0;
item.addedToCart = false;
item.showAddToCart = false;
var itemIndex = _this.cartStorage.items.indexOf(item);
if (itemIndex > -1) {
_this.cartStorage.items.splice(itemIndex, 1);
}
}
})
Now the cartStorage object is shared so any update made in one controller will automagically be reflected in the other controller. All that's left is the markup:
<div ng-app="app">
<div ng-controller="mainController as main">
<h2>Main Controller</h2>
<div>
<table>
<tr>
<td>Item</td>
<td>Price</td>
<td>Quantity</td>
<td></td>
</tr>
<tr ng-repeat="item in main.items">
<td>{{item.name}}</td>
<td>{{item.price | currency}}</td>
<td>{{item.quantity}}</td>
<td>
<button ng-click="main.increaseItemAmount(item)">+</button>
<button ng-click="main.decreaseItemAmount(item)">-</button>
<button ng-click="main.addToCart(item)" ng-show="item.showAddToCart && !item.addedToCart">Add to Cart</button>
</td>
</tr>
</table>
</div>
</div>
<div ng-controller="cartController as cart">
<h2>Cart Controller</h2>
<div>
<table>
<tr>
<td>Item</td>
<td>Price</td>
<td>Quantity</td>
<td></td>
</tr>
<tr ng-repeat="item in cart.cartStorage.items">
<td>{{item.name}}</td>
<td>{{item.price | currency}}</td>
<td>{{item.quantity}}</td>
<td>
<button ng-click="cart.increaseItemAmount(item)">+</button>
<button ng-click="cart.decreaseItemAmount(item)">-</button>
<button ng-click="cart.removeFromCart(item)">Remove from Cart</button>
</td>
</tr>
</table>
</div>
</div>
</div>
Update showing the usage of a Factory instead of Value
Instead of using a Value use this service:
.factory('cartStorage', function() {
var _cart = {
items: []
};
var service = {
get cart() {
return _cart;
}
}
return service;
})
Then modify the code in the controllers to use the .cart property of the service instead of the value. You only need to change one line of code in both controllers. Change:
_this.cartStorage = cartStorage;
to:
_this.cartStorage = cartStorage.cart;
Here is an updated JSFiddle.
I made this plunker as an example.
I've used events to achieve the desired behavior. (
This is just one way of doing this, should have a lot of possibilities)
ProductsController:
app.controller('ProductsCtrl', function($scope, $rootScope) {
$scope.products = [
{
'name': 'Product One',
'price': 10,
'qty': 0
},
{
'name': 'Product two',
'price': 20,
'qty': 0
}
];
// Fire event to add
$scope.add = function(product) {
product.qty++;
$rootScope.$broadcast('addProduct', product.price);
}
// Fire event to remove
$scope.remove = function(product) {
if(product.qty > 0) {
product.qty--;
$rootScope.$broadcast('removeProduct', product.price);
}
}
});
CartController:
app.controller('CartCtrl', function($scope) {
$scope.total = 0;
// Catch the event to add
$scope.$on('addProduct', function(event, data) {
$scope.total += data;
});
// Catch the event to remove
$scope.$on('removeProduct', function(event, data) {
$scope.total -= data;
});
});
View:
<div ng-controller="CartCtrl">Total: {{total}}</div>
<br>
<div ng-controller="ProductsCtrl">
<div ng-repeat="product in products">
<span>Name: {{product.name}}</span>
<br>
<span>Price:{{product.price}}</span>
<span>Quantity:{{product.qty}}</span>
<br>
<button type="button" ng-click="add(product);">Add</button>
<button type="button" ng-click="remove(product);">Remove</button>
<br><br><br>
</div>
</div>
You can have a shared service between your Product details and Cart detail controller which can have an array where you can push the the Product selected with its quantity and other details.