duplicate grouped state and bind to onChange event - javascript

How can I make dynamic row (add and delete row), and input binding (bind onChange event)
to work?, If it's just single input then it's easy, but a certain structure to defined
the intial form element.
this.state = {
rowCount: 1,
rows: [{
id: 1,
structure: [
{
type: 'input',
value
},
{
type: 'textarea',
value:
}
]
}]
}
render() {
return(
<div>{this.state.rows.map(o => <div>
{o.structure.map(s => {
let block = ''
if(s.type === 'input') {
block = <div><input /></div>
}else if(s.type === 'textarea') {
block = <div><textarea /></div>
}
return block
})}
<button onClick={this.addBlock}>Add block</button>
</div>)}</div>
)
}
https://codesandbox.io/s/nr88plwqj4

I modified your code so we can add blocks (delete have a similar approach), with the asumption that all the blocks will have the same structure. Also, I split your "render" in several "renders" that display different parts of the program, to make it easier to read.
https://codesandbox.io/s/xjzlzqj04z
The idea is the following: Everytime you click to Add block, you check all the rows, making a copy into a "newRows" variable. Finnaly, I do a push to newRows with the new data we want to add, in this case and adding the suitable id.
In the case of this structure in particular, I highly recommend that each row is a separate react component, more if the structure is the same for each row (text + input). If you do it with an extra react component, you can manage the add or delete block with the state being a simple array of ids instead of adding all the structure, and all the and all the logic (onchange value, delete) can be manage inside the other component. If you want to access to the value of all the components in the base component, you can add as a prop an "updateValue" that will get the id, and will update the state. I will try to make an example and update it later
edit:
here I have an example with the rows added as a different component. This simplify a lot the state logic, but that also depends on your needs.
https://codesandbox.io/s/4xq4w37v1w
I must say, codesandbox is a bit buggy with react, so If it doesnt work I recomend to "select all -> cut -> paste"

Here's an example of adding new row and deleting the last row without changing the initial state rows object structure:
https://codesandbox.io/s/vm4lqnjyz7
A few points worth to be mentioned:
1) don't forget to add keys to elements when rendering a list, here's an explanation why: https://reactjs.org/docs/lists-and-keys.html#keys
2) "rowCount" state property seems to be redundant as it equals the rows array length
3) "add row" button probably should be placed outside of the map method if you're going just to add a new row to existing array (instead of building an independent tree of input blocks inside each box).

Related

Is My Approach to the Todo App Delete Function Wrong?

I am learning React and just created a simple todo app using only React. My todo app has the standard structure of having a text input and an "ADD" button next to it. The user would type their todo in the input and every time they click on the "ADD" button next to it, a new ordered list of their inputs would appear underneath the input and "ADD" button.
The user can also delete a todo entry by clicking on the entries individually, like this:
To accomplish this behaviour of deleting entries, I used this delete function:
delete(elem) {
for (var i = 0; i < this.state.listArray.length; i++) {
if (this.state.listArray[i] === elem) {
this.state.listArray.splice(i, 1);
this.setState({
listArray: this.state.listArray
});
break;
}
}
}
My todo app works exactly the way that I want it to work, but as I look at other people's more conventional approach to this delete function, they either just simply use the splice method or the filter method.
For the splice method approach, they apparently just simply "remove" the unwanted entry from the listArray when the user clicks the particular entry. This does not work for me as using this method results in all my entries getting deleted except for the entry that I clicked on, which is the one that I want to delete.
On the other hand, the filter method approach apparently works by comparing the elem, which is the data passed from a child component, with each element in the listArray, and if the element in the for loop does not equal to the elem, then it would be passed onto a new array. This new array would be the one to not be deleted. This approach works better than the simple splice approach, however, one problem that I had encountered with this approach is that if I have more than one entry of the same value, for example, "Feed the dog". I only want one of the "Feed the dog" entries to be deleted, but it deletes both of them.
I thought of an approach to tackle this problem, eventually coming up with the current version of my code, which uses the splice method, but the splice method is used before I set it in the state. As evident here:
this.state.listArray.splice(i, 1);
this.setState({
listArray: this.state.listArray
});
My question can be broken down into three subquestions:
Considering that React states should be immutable, is the first line of the code above mutating my state? Is this approach not okay?
I thought that all React states were only possible to be changed inside a "setState" function, but my first line of code from above is not inside a setState function, yet it changed the state of listArray. How is this possible?
If my approach is mutating the state and is not ideal, how would you go about making the delete function so that it only deletes one entry and not more than one if there are multiple similar entries?
Yes, splice affects the array it acts on so don't use in this way. Instead you need to create a new array of the correct elements:
this.setState({
listArray: this.state.listArray.filter((el, idx) => idx !== i);
});
If you want to remove only the first instance, maybe couple with a findIndex (although indexOf would work in your example as well) first:
delete(elem) {
const idxToFilter = this.state.listArray.findIndex(el => el === elem);
if (idxToFilter < 0) {
return;
}
this.setState({
listArray: this.state.listArray.filter((el, idx) => idx !== idxToFilter);
});
}
This creates a new array without modifying the old which will cause anything that reacts to listArray changing to be notified since the reference has changed.

Bootstrap Vue Table - Getting the selections from multiple tables?

I've been playing around with Bootstrap Vue and tables.
My problem is as follows: I have several tables that are dynamically loaded on a page, and users should be able to select items on each of those tables. All selections should then be concatenated into one array that I can then display at the top of the page.
So far I've added the following to each of the tables:
#row-selected="onRowSelected"
As well as the following method:
methods: {
onRowSelected(items) {
items.forEach((item) => {
if (!this.selectedHashtags.includes(item.hashtag)) {
this.selectedHashtags.push(item.hashtag);
}
})
},
}
The issue is that as soon as I deselect an item from the table it doesn't remove it from the array, and I'm struggling to find a way to make this work.
Unfortunately the #row-selected event doesn't send the ID / ref of the table, and I can't find find a method of getting all the selected rows from each individual table. That way I could just loop through all the this.$refs and get all the selected rows and bundle them together on every row-click.
Essentially the easiest way would be if there was a way to programmatically obtain all the selected items from a table?
Any thoughts on what the best way to achieve this might be?
Probably the easiest way would be to store the selected value together with a unique key of each table. You would then call the method like with the keyword $event (see the documentation):
#row-selected="onRowSelected('table1', $event)"
You could also wrap an inline function in the template to achieve the same result:
#row-selected="(items) => onRowSelected('table1', items)"
Then, you would store the items in an object depending on the table key:
onRowSelected(tableKey, items) {
// clears the list of this table key and overwrites it with the current entries
this.selectedHashtags[tableKey] = items;
}
You can then define a computed variable to retrieve all selected hashtags (over all tables):
allSelectedHashtags() {
const allSelectedHashtags = [];
Object.keys(this.selectedHashtags).forEach(tableArray => {
allSelectedHashtags.concat(tableArray);
});
return allSelectedHashtags;
}

"You may have an infinite update loop in a component render function" when I add this line of code - console.log(perksTree.slots.unshift())

I have a function that find an object from a JSON that has an id === this.match.mainParticipant.stats.perkSubStyle. This object contains a property called slots that is an array and has 4 elements. Each slot has 3 elements which represent runes from a game. If you iterate over the slots and their elements you get this:
I get the object using this function:
secondaryPerks(){
let perksTree = this.$store.state.summonerRunes.find(value => value.id === this.match.mainParticipant.stats.perkSubStyle);
console.log(perksTree.slots.unshift())
return perksTree
}
and I iterate and display the icons using this:
<div v-for='runes in this.secondaryPerks().slots'>
<div v-for='rune in runes.runes'>
<img :src="'https://ddragon.leagueoflegends.com/cdn/img/' + rune.icon" alt="">
</div>
</div>
Now the problem is that because that perks tree is secondary one, the perks in slot[0] can never be picked because if they were picked, they'd have to be part of the primaryPerks tree. This means there's no point displaying that none of them were selected. For that reason I am trying to remove the first slot[0] element from the array, however, when I try to unshift() it, I get an error:
"You may have an infinite update loop in a component render function"
And I have no clue why. Any advices?
Firstly, I think you mean shift rather than unshift. unshift will try to add items to the array rather than removing them. It doesn't actually matter from the perspective of the infinite loop, either method will have the same effect.
You're creating a dependency on the array and then modifying it. Modifying it will trigger a re-render.
Each time the component re-renders it will shift another item onto/out of the array. Even if the call to shift/unshift doesn't actually change anything it will still count as modifying the array.
Try:
computed: {
secondaryPerkSlots () {
const perksTree = this.$store.state.summonerRunes.find(
value => value.id === this.match.mainParticipant.stats.perkSubStyle
);
return perksTree.slots.slice(1)
}
}
with:
<div v-for='runes in secondaryPerkSlots'>
That will create a new array containing the same elements as the original array, omitting the first element.
Alternatively you could put the slice(1) directly in the template:
<div v-for='runes in secondaryPerks().slots.slice(1)'>
Either way I suggest changing the method to a computed property instead. You should also drop the this in your template.
I had the same problem a few months ago.
I think the main issue is that you perform logic such as arr.unshift()(which will cause the template to re-render in this case) in your computed property.
So, imagine this:
const arr1 = [/* ... */];
// This is different
const computedArr = () => {
return arr.filter(() => { /* ... */ });
};
// Than this
const computedArr = () => {
const newArr = arr.filter(() => { /* ... */ });
// Vue cannot allow this without a re-render!
newArr.unshift();
return newArr;
};
The latter will cause the template to re-render;
EDIT
Check the first comment!

Recursive function for detailCellRendererParams, Ag-Grid?

I have data which consists of multiple rows of data. Each row contains a 'children' array property, which may have data in the form of more rows, or may be empty. On top of that, each of the rows within the 'children' array property may also contain more 'children' data or rows and so on, so it looks like this (think of each line as a row and each indented line as a child row of that row):
r|-------
r1|------*
r1a|------
r1b|------*
r1b1|------
r1c|------*
r1c1|------
r1c2|------
r2|------
r3|------*
r3a|------
r3b|------
Each parent containing child rows (I marked them with '*') must have detailCellRendererParams defined, which is fine if I was just going to define each one manually (as shown in Ag-Grid documentation under Nesting Master / Detail, however, it is uncertain how many parent/children rows there will be. I am looking to create a recursive function that defines the detailCellRendererParams for each parent row with children. How might I write something like this?
No recursion required, just use the tree data functionality of ag-grid:
https://www.ag-grid.com/javascript-grid-tree-data/
You need to enable tree functionality with:
var gridOptions = {
treeData: true,
...
}
and provide the grid with the field that creates your tree-hierarchy
gridOptions.getDataPath: function(data) {
return data.myHierarchyField;
},

IN CQ, how to set value of all the items in Panel to blank

In ExtJS panel I need to set value of all items (e.g. textfield, pathfield) to blank. I don't want to set value of each individual item to blank but of whole panel in one go.
I am able to get list of items
function getAllChildren (panel) {
/*Get children of passed panel or an empty array if it doesn't have thems.*/
var children = panel.items ? panel.items.items : [];
/*For each child get their children and concatenate to result.*/
CQ.Ext.each(children, function (child) {
children = children.concat(getAllChildren(child));
});
return children;
}
but how to set to blank for whole panel? Please suggest what need to be done in this case.
Actually, it's not possible to do it with one liner - all at the same time. What your method returns is purely an array of objects. In fact if such syntax existed, it would iterate over all fields anyway.
Though clearing all fields, having the method you've proposed is very trivial to do. Just iterate over them all and call reset method. Mind some (especially custom) widgets might not handle it.
var fields = getAllChildren(panel);
CQ.Ext.each(fields, function(field) {
if (child.reset) {
child.reset();
}
});
You've got similar loop in your getAllChildren code - you might reset field at the same place.
The method is defined in Field type which is usually a supertype of each dialog widget. You can read more here.

Categories