I'm trying to make a clone button in React, I have a state with an array that has 2 items in it. The button will send the index of the element selected, in this case let's say index 0. :) I can't get the following code to work
elements = [
{ item: 'something1', another: 'something2' },
{ item: 'something1', another: 'something2' }
];
setState( {
elements: [
...elements.slice( 0, index ),
{
...elements[ index ],
item: 'something'
},
...elements.slice( index + 1 )
]
} )
I know I'm doing something wrong, but...
Use index + 1 in the 1st slice call because you want to get all items up to and including the item you clone (slice stops before the end index), insert the clone, and add all other items after it:
const elements = [
{ item: 'something1', another: 'something1' },
{ item: 'something2', another: 'something2' },
{ item: 'something3', another: 'something3' }
];
const index = 1;
const newElements = [
...elements.slice(0, index + 1),
{
...elements[index],
item: 'new something !!!'
},
...elements.slice(index + 1)
];
console.log(newElements);
Ori Drori's quite correct about why the cloning isn't working. But there are two other issues with the code you'll want to address:
When setting state based on existing state, you must use the callback version of setState since state updates can be stacked and are asynchronous. So not:
this.setState({
elements: [/*...*/]
});
but instead:
this.setState(prevState => {
return {
elements: [/*...*/]
};
});
Partially because of #1 above, using the index is unreliable; other changes may have altered the array (your element may not even be in it anymore). Use the element itself. For instance, if your click handler is cloneClick, you'd use onClick={this.cloneClick.bind(this, el)} on the button to pass the element to the handler.
Here's a complete example:
class Example extends React.Component {
constructor(...args) {
super(...args);
this.state = {
elements: [
{ item: 'item1', another: 'something1' },
{ item: 'item2', another: 'something2' }
]
};
}
cloneClick(el) {
this.setState(prevState => {
const index = prevState.elements.indexOf(el);
if (index === -1) {
return null;
}
return {
elements: [
...prevState.elements.slice(0, index + 1),
{
...el,
item: el.item + " (copy)"
},
...prevState.elements.slice(index + 1)
]
};
// Alternately, this is more efficient:
/*
let clone = null;
const elements = [];
prevState.elements.forEach(prevEl => {
elements[elements.length] = prevEl;
if (prevEl === el) {
elements[elements.length] = clone = {
...el,
item: el.item + " (copy)"
};
}
});
return clone ? {elements} : null; // No update if the el wasn't found
*/
});
}
render() {
return <div>
{this.state.elements.map((el, i) =>
<div key={i}>
{el.item}
-
{el.another}
<input type="button" value="Clone" onClick={this.cloneClick.bind(this, el)} />
</div>
)}
</div>;
}
}
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
Related
When the page first loads, the delete buttons generated by the code below work as expected. However, if you alter the text in one of the <textarea> elements, the delete button no longer works correctly. How can I fix this?
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
static get properties() {
return {
list: { type: Array },
};
}
constructor() {
super();
this.list = [
{ id: "1", text: "hello" },
{ id: "2", text: "hi" },
{ id: "3", text: "cool" },
];
}
render() {
return html`${this.list.map(item =>
html`<textarea>${item.text}</textarea><button id="${item.id}" #click="${this.delete}">X</button>`
)}`;
}
delete(event) {
const id = event.target.id;
this.list = this.list.filter(item => item.id !== id);
}
}
customElements.define("my-element", MyElement);
I'm not sure of the exact cause, but I think it has to do with the way lit-html decides which DOM elements to remove when rendering a list with fewer items than the previous render. The solution is to use the repeat directive. It takes as its second argument a function that helps lit-html identify which DOM elements correspond to which items in the array:
import { repeat } from 'lit-html/directives/repeat.js'
// ...
render(){
return html`
${repeat(this.list, item => item.id,
item => html`<textarea>${item.text}</textarea><button id="${item.id}" #click="${this.delete}">X</button><br>`
)}
`;
}
In React, upon deleting a component, I want to make a dynamic sentence shows the correct sentence like this in app.js:
let awesomePhrase = '';
if (!this.state.showPersons) {
awesomePhrase = 'Nobody is here, it seems :/';
}
if (this.state.showPersons && this.state.persons.length === 2) {
awesomePhrase = "All aboard :D";
}
if (!this.state.persons.filter(p => p.id === 1)) {
awesomePhrase = "Where's Matin?!";
}
if (!this.state.persons.filter(p => p.id === 2)) {
awesomePhrase = "Where's Mobin?!";
}
It doesn't show any of the sentence when I delete id 1 or id 2.That is, neither "where's Matin?!" nor "Where's Mobin?!".
But the two first sentences work fine.
(EDIT: every piece of code below is within app.js, the main file)
For deleting:
deleteHandler = index => {
const persons = [...this.state.persons].filter(
person => person.id !== index
);
this.setState({ persons });
};
The State:
state = {
persons: [
{ id: 1, name: 'Matin', age: 27 },
{ id: 2, name: 'Mobin', age: 26 }
],
showPersons: false,
...
};
The component within the render of the class:
{this.state.persons.map(person => {
return (
<Person
key={person.id}
name={person.name}
age={person.age}
click={() => this.deleteHandler(person.id)}
/>
);
})}
the part of render where dynamic text is used:
return (
<div>
...
<h2>{awesomePhrase}</h2>
...
</div>
)
The problem with your code is the filter function. Filter will return an empty array if no elements passed the test, and in Javascript, an empty array is not a falsy value.
The condition !this.state.persons.filter(p => p.id === 2) will always be false.
The proper function to use in this situation is Array.some, which return a boolean value depends on the result of the test function.
Be aware of the return type and the falsiness / truthiness in Javascript.
I think I found a workaround
instead of filter I used find, and tried to check them within deleteHandler method. Also added an independent awesomePhrase to State.
So:
deleteHandler = index => {
const persons = [...this.state.persons].filter(
person => person.id !== index
);
if (persons.length === 0) {
this.setState({ awesomePhrase: 'where did they all gone?' });
}
if (persons.find(p => p.name === 'Matin')) {
this.setState({ awesomePhrase: 'Where is Mobin?' });
}
if (persons.find(p => p.name === 'Mobin')) {
this.setState({ awesomePhrase: 'Where is Matin?' });
}
this.setState({ persons });
};
state = {
persons: [
{ id: 1, name: 'Matin', age: 27 },
{ id: 2, name: 'Mobin', age: 26 }
],
...,
showPersons: false,
mobin: true,
awesomePhrase: ''
};
return (
<div className="App">
...
<h2>{this.state.awesomePhrase || awesomePhrase}</h2>
...
</div>
);
I'd welcome any suggestion to help improve my code further or correct it properly. I just made it work now.
I have a react component that shows a list of teams.
const teams = [
{
name: 'Liverpool'
},
{
name: 'Chelsea'
},
{
name: 'Fulham'
}
]
I then show these teams in a react component.
render() {
return(
<div>
{teams.map(team => {
return <p key={team.name}>{team.name}</p>
});
</div>
)
}
This works fine, but I want there to be a message shown if there are less than 5 teams in the array. There should be a message saying insert team here. So it should be:
Liverpool
Chelsea
Fulham
Insert team here
Insert team here
Does this make sense. Basically there should always be 5 boxes, with the names of teams, and if not full then it should show a different message.
I need some sort of if statement but not sure the best way to do this. Let me know if more information is required.
The data comes from an API so I don't really want to change that if I can avoid it. Is there anything I can do in the render method?
You might consider looping at least for 5 times. So that you have something like .fill():
if (teams.length < 5) {
const origLength = teams.length;
teams.length = 5;
teams = teams.fill({ name: "Insert team here" }, origLength, 5);
}
Complete Working Solution
class App extends React.Component {
render() {
let teams = [{ name: "Chelsea" }, { name: "Liverpool" }];
if (teams.length < 5) {
const origLength = teams.length;
teams.length = 5;
teams = teams.fill({ name: "Insert team here" }, origLength, 5);
}
return (
<div>
{teams.map((team, key) => {
return <p key={key}>{team.name}</p>;
})}
</div>
);
}
}
Working Snippet
class App extends React.Component {
render() {
let teams = [{ name: "Chelsea" }, { name: "Liverpool" }];
if (teams.length < 5) {
const origLength = teams.length;
teams.length = 5;
teams = teams.fill({ name: "Insert team here" }, origLength, 5);
}
return (
<div>
{teams.map((team, key) => {
return <p key={key}>{team.name}</p>;
})}
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<div id="root"></div>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"> </script>
I have also fixed your key.
Demo: https://codesandbox.io/s/ymjym0z7lx
Direct Link: https://ymjym0z7lx.codesandbox.io/
You can use a function to ensure that you always have a minimum size on your array, filling spots that are empty:
const teams = [
{
name: 'Liverpool'
},
{
name: 'Chelsea'
},
{
name: 'Fulham'
}
]
const padArray = (arrayIn, size, filler) => Array.from(
new Array(Math.max(size, arrayIn.length)).keys()
).map((i) => arrayIn[i] || { name: filler })
const result = padArray(teams, 5, 'Insert team here');
console.dir(result)
Then you can run padArray(teams, 5, 'Insert team here').map instead of teams.map
Alternatively, you can use Array.fill, as #praveen-kumar-purushothaman suggests:
teams.fill({ name: "Insert team here" }, 5, teams.length).map() // etc.
I am working on an click and drop feature --- where on the page the module is used in a recursive way so it has a parent and children.
I have hit an issue where if the user started to select the children - and then selects the parent - I want to deselect the children. Although I am unsure how to store or monitor a change in the parent/child items selected to make global deselection.
So the user has selected the child of bacon3.. if they select the parent - it would need to deselect the children -- but I feel I am currently locked in the scope of the module
I think this example will help you https://canary.ember-twiddle.com/468a737efbbf447966dd83ac734f62ad?openFiles=utils.tree-helpers.js%2C
So, this was an interesting problem. It turned out to be more of a recursion problem than anything having to do with ember, javascript, or checkbox behavior.
Here is what I have (using the updated syntax and such (if you have the option to upgrade to 3.4, you most definitely should -- it's a dream))
// wrapping-component.js
import Component from '#ember/component';
import { action, computed } from '#ember-decorators/object';
import { check } from 'twiddle/utils/tree-helpers';
export default class extends Component {
options = [{
id: 1,
label: 'burger',
checked: false,
children: [{
id: 3,
label: 'tomato',
checked: false
}, {
id: 4,
label: 'lettus',
checked: false
}, {
id: 5,
label: 'pickle',
checked: false
}]
}, {
id: 2,
label: 'kebab',
checked: false,
children: [{
id: 6,
label: 'ketchup',
checked: false
}, {
id: 7,
label: 'chilli',
checked: false
}]
}];
#action
toggleChecked(id) {
const newTree = check(this.options, id);
this.set('options', newTree);
}
}
template:
{{yield this.options (action this.toggleChecked)}}
and the usage:
// application.hbs
<WrappingComponent as |options toggle|>
{{#each options as |item|}}
<CheckboxGroup #item={{item}} #onClick={{toggle}} />
{{/each}}
</WrappingComponent>
CheckboxGroup is a template-only component:
// checkbox-group.hbs
<div class="checkboxhandler">
<input
type="checkbox"
checked={{#item.checked}}
onclick={{action #onClick #item.id}}
>
<label>{{#item.label}}</label>
{{#if #item.children}}
{{#each #item.children as |child|}}
<CheckboxGroup #item={{child}} #onClick={{#onClick}} />
{{/each}}
{{/if}}
</div>
and the recursive helpers (this is a mess, but I've just been prototyping):
// utils/tree-helpers.js
const toggle = value => !value;
const disable = () => false;
// the roots / siblings are contained by arrays
export function check(tree, id, transform = toggle) {
if (tree === undefined) return undefined;
if (Array.isArray(tree)) {
return selectOnlySubtree(tree, id, transform);
}
if (tree.id === id || id === 'all') {
return checkNode(tree, id, transform);
}
if (tree.children) {
return checkChildren(tree, id, transform);
}
return tree;
}
function selectOnlySubtree(tree, id, transform) {
return tree.map(subTree => {
const newTree = check(subTree, id, transform);
if (!newTree.children || (transform !== disable && didChange(newTree, subTree))) {
return newTree;
}
return disableTree(subTree);
});
}
function isTargetAtThisLevel(tree, id) {
return tree.map(t => t.id).includes(id);
}
function checkNode(tree, id, transform) {
return {
...tree,
checked: transform(tree.checked),
children: disableTree(tree.children)
};
}
function disableTree(tree) {
return check(tree, 'all', disable);
}
function checkChildren(tree, id, transform) {
return {
...tree,
checked: id === 'all' ? transform(tree.checked) : tree.checked,
children: check(tree.children, id, transform)
};
}
export function didChange(treeA, treeB) {
const rootsChanged = treeA.checked !== treeB.checked;
if (rootsChanged) return true;
if (treeA.children && treeB.children) {
const compares = treeA.children.map((childA, index) => {
return didChange(childA, treeB.children[index]);
});
const nothingChanged = compares.every(v => v === false);
return !nothingChanged;
}
return false;
}
hope this helps.
I would like to iterate through an array and display each element in the array every time I click a button.
so far I have this:
{this.state.users.map(function(user, index){
return <p key={ index }>{user.name} </p>;
}, this)}
This displays each users name on screen, one after each other.
How can I do it so it just shows users[0] and then moves to users[1] or even better, click removes the user at position users[0] and then a new person is at position users[0] and then if the array is empty, it displays the text 'no more users'
I know how to remove elements from arrays etc, it's just the displaying one at a time in React land which I cant do
Based on my understanding to your question may be you are trying to achieve this -
let users = [{
name: "abcd"
}, {
name: "xyz"
}, {
name: "temp"
}];
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
activeIndex: 0
};
}
tick =() => {
let activeIndex = this.state.activeIndex;
if (activeIndex == this.props.users.length -1){
activeIndex = 0;
} else {
activeIndex++;
}
this.setState({
activeIndex
});
}
render() {
return (
< ul className = "list-group" >
< li className = "list-group-item" >
{this.props.users[this.state.activeIndex].name}
< button className = "btn btn-default" onClick={this.tick} >
show Next
< /button>
</li >
< /ul>
);
}
}
ReactDOM.render(
< Example users = {users}/ > ,
document.getElementById('test')
);
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="test">
</div>
Keep the index of activeUser in state, then render just this one user: { this.state.users[ this.state.activeUser ].name}. Increment/decrement the activeUser on click.
demo on jsfiddle
var Users = React.createClass({
getInitialState(){
return {
users : [{ name : 'John'}, { name : 'Jane'}, { name : 'Robert' }],
activeUser : 0
};
},
next(){
/* here you should add checking if there are more users before setting the state */
this.setState({ activeUser : this.state.activeUser + 1 });
},
render: function() {
return (<div>
<p>{ this.state.users[ this.state.activeUser ].name} </p>
<span onClick={ this.next }>next</span>
</div>);
}
});
Seems like you have the pseudo code there in your question which like Rajesh asked seems to describe some kind of paginated list of users/carousel.
You'll need to define the maximum number of users that are visible in the list and then store the current topmost element index. I'd suggest storing these in state.
Then store the list of names as an array of object each containing the name details as well as a parameter that will represent whether it is visible or not.
Your initial state could look like this:
{
currentTopElement: 0,
maxNamesToShow: 1,
names: [
{name: 'John Smith', isVisible: false},
{name: 'Jane Smith', isVisible: false},
...etc.
]
}
Add a click handler on your button would be required that would increment the currentTopElement by one.
iterate through the names array setting isVisible to false unless the index matches that of currentTopElement, if it does set isVisible to true
To finish off, in the component that needs to render your names list can do so if you conditionally render a name only if its corresponding isVisible value is true:
const elementsToShow = this.state.filter( t => t.isVisible);
You can the iterate over elementsToShow like so:
elementsToShow.map((user, index) => <p key={ index }>{user.name} </p>);