Adding a ref to cloneElement to then set focus of cloneElement - reactjs

I've not used cloned Elements before, but am now trying to create a button that when clicked opens up an input box of an appropriate type and, crucially, focuses on the input box. What I have seems to basically work except that it doesn't focus (I get a console error that _this.refs.content.focus is not a function). I have tried reading documentation and other questions but am no further forward. Any help appreciated
getInitialState() {
return {
isEditing: false,
.....
};
},
_onEdit() {
if (this.state.isEditing) return;
this.setState({
isEditing: true,
editedValue: this.props.value,
}, () => {
console.dir(this.refs);
this.refs.content.focus();
});
},
.....
render() {
.....
// button clicked
if (this.state.isEditing) {
return React.cloneElement(
this.props.children[0], {
onBlur: this.onBlur,
value: this.state.editedValue,
onChange: this.onChange,
ref: "content",
}
);
} else {
const child = this.props.children[1];
// Editable but in read mode - display clickable button
return pug`
button.btn-block.btn-editable(
onClick=this._onEdit,
type='button',
)= React.cloneElement(child, {}, this.state.editedValue || (child.props.children || ""))
`;
}
},

Related

Want to test for size of an array constant in React

I'm using enzyme and right now I have a const in my main file that I want to test the size of but I'm not sure on how to do this with enzyme or jest.
const menuItems = [
{
id: "loginID",
name: "Login",
link: "/",
},
{
id: "eventListID",
name: "Event List",
link: "/eventlist",
},
{
id: "listID",
name: "List",
link: "/list",
},
];
I looked on google but wondering if there is a way to reference menuItems in my code? I have a menu that when open calls the menuItems const and renders the values in there
In my test file I tried:
it("renders three options when menu is active", () => {
const wrapper = mount(<EventPage />);
wrapper.setState({ active: true });
expect(wrapper.find(menuItem).length).toBe(3);
});
But obviously that didn't work. Any help would be great
edit:
Here the relevant EventPage code:
state = {
menuOpen: false,
};
toggleMenu = (e) => {
e.preventDefault();
this.setState({
menuOpen: !this.state.menuOpen,
});
};
<Menu
id="menu1"
open={this.state.menuOpen}
menuText="Menu"
items={menuItems}
//toggleMenu opens menu side panel if Menu is clicked
handleMenuToggle={this.toggleMenu}
data-testid="toggleMenu"
>
You could locate the rendered Menu, and then just check the length of its items prop
Assuming there is a single Menu:
expect(wrapper.find(Menu).at(0).props().items.length).toBe(3)

React checkboxes. State is late when toggling the checkboxes

I have a group of 3 checkboxes and the main checkbox for checking those 3 checkboxes.
When I select all 3 checkboxes I want for main checkbox to become checked.
When I check those 3 checkboxes nothing happens but when I then uncheck one of those trees the main checkbox becomes checked.
Can someone explain to me what actually is happening behind the scenes and help me somehow to solve this mystery of React state? Thanks!
Here is a code snnipet:
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange = id => {
const data = [...this.state.data];
data.forEach(item => {
if (item.id === id) {
item.checked = !item.checked;
}
})
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
this.setState({ data: data, main: everyCheckBoxIsTrue });
}
onMainCheckBoxChange = () => {
let data = [...this.state.data];
data.forEach(item => {
!this.state.main ? item.checked = true : item.checked = false
})
this.setState({
this.state.main: !this.state.main,
this.state.data: data,
});
}
render () {
const checkbox = this.state.data.map(item => (
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
))
}
return (
<input type="checkbox" name="main" checked={this.state.main} onChange={this.onMainCheckBoxChange} />
{checkbox}
)
I can't make a working code snippet based on the code you provided, one of the issues was:
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
where checkbox is not defined.
However, I think you confused about using the old state vs the new state, it'd be simpler to differentiate if you name it clearly, e.g.:
eventHandler() {
const { data } = this.state; // old state
const newData = data.map(each => ...); // new object, soon-to-be new state
this.setState({ data }); // update state
}
Here's a working example for your reference:
class App extends React.Component {
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange(id) {
const { data } = this.state;
const newData = data.map(each => {
if (each.id === id) {
// Toggle the previous checked value
return Object.assign({}, each, { checked: !each.checked });
}
return each;
});
this.setState({
data: newData,
// Check if every checked box is checked
main: newData.every(item => item.checked === true),
});
}
onMainCheckBoxChange() {
const { main, data } = this.state;
// Toggle the previous main value
const newValue = !main;
this.setState({
data: data.map(each => Object.assign({}, each, { checked: newValue })),
main: newValue,
});
}
render () {
const { data, main } = this.state;
return (
<div>
<label>Main</label>
<input
type="checkbox"
name="main"
// TODO this should be automatically checked instead of assigning to the state
checked={main}
onChange={() => this.onMainCheckBoxChange()}
/>
{
data.map(item => (
<div>
<label>{item.id}</label>
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
</div>
))
}
</div>
);
}
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Side note: You might want to consider not to use the main state
You shouldn't be storing state.main to determine whether every checkbox is checked.
You are already storing state that determines if all checkboxes are checked, because all checkboxes must be checked if every object in state.data has checked: true.
You can simply render the main checkbox like this:
<input
type="checkbox"
name="main"
checked={this.state.data.every(v => v.checked)}
onChange={this.onMainCheckBoxChange}
/>;
The line this.state.data.every(v => v.checked) will return true if all of the checkboxes are checked.
And when the main checkbox is toggled, the function can look like this:
onMainCheckBoxChange = () => {
this.setState(prev => {
// If all are checked, then we want to uncheck all checkboxes
if (this.state.data.every(v => v.checked)) {
return {
data: prev.data.map(v => ({ ...v, checked: false })),
};
}
// Else some checkboxes must be unchecked, so we check them all
return {
data: prev.data.map(v => ({ ...v, checked: true })),
};
});
};
It is good practice to only store state that you NEED to store. Any state that can be calculated from other state (for example, "are all checkboxes checked?") should be calculated inside the render function. See here where it says:
What Shouldn’t Go in State? ... Computed data: Don't worry about precomputing values based on state — it's easier to ensure that your UI is consistent if you do all computation within render(). For example, if you have an array of list items in state and you want to render the count as a string, simply render this.state.listItems.length + ' list items' in your render() method rather than storing it on state.

Interupt code and wait for user interaction in a loop - React

I am trying to implement an "add all" button in my react app. to do that, i pass this function to the onClick method of the button :
for (element in elements) {
await uploadfunction(element)
}
const uploadfunction = async (element) => {
if (valid) {
// await performUpload(element)
}
else if (duplicate) {
//show dialog to confirm upload - if confirmed await performUpload(element)
}
else {
// element not valid set state and show failed notification
}
}
const performUpload = async (element) => {
// actual upload
if(successful){
// set state
}else{
// element not successful set state and show failed notification
}
}
the uploadfunction can have three different behaviors :
Add the element to the database and update the state
Fail to add the element and update the state
Prompt the user with the React Dialog component to ask for confirmation to add duplicat element and update the state accordingly
My problem now is since i'm using a for loop and despite using Async/await , i can't seem to wait for user interaction in case of the confirmation.
The behavior i currently have :
The for loop move to the next element no matter what the result
The Dialog will show only for a second and disappear and doesn't wait for user interaction
Wanted behavior:
Wait for user interaction (discard/confirm) the Dialog to perform the next action in the loop.
How can i achieve that with React without Redux ?
Here is an example of a component that might work as an inspiration for you.
You might split it in different components.
class MyComponent extends Component {
state = {
items: [{
// set default values for all booleans. They will be updated when the upload button is clicked
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_2',
}],
performUpload: false,
};
onUploadButtonClick = () => {
this.setState(prevState => ({
...prevState,
items: prevState.items.map((item, index) => ({
isValid: validationFunction(),
isDuplicate: prevState.items.slice(0, index).some(i => i.data === item.data),
shouldUploadDuplicate: false,
data: item.data
})),
performUpload: true,
}), (nextState) => {
this.uploadToApi(nextState.items);
});
};
getPromptElement = () => {
const firstDuplicateItemToPrompt = this.getFirstDuplicateItemToPrompt();
const firstDuplicateItemIndexToPrompt = this.getFirstDuplicateItemIndexToPrompt();
return firstDuplicateItemToPrompt ? (
<MyPrompt
item={item}
index={firstDuplicateItemIndexToPrompt}
onAnswerSelect={this.onPromptAnswered}
/>
) : null;
};
getFirstDuplicateItemToPrompt = this.state.performUpload
&& !!this.state.items
.find(i => i.isDuplicate && !i.shouldUploadDuplicate);
getFirstDuplicateItemIndexToPrompt = this.state.performUpload
&& !!this.state.items
.findIndex(i => i.isDuplicate && !i.shouldUploadDuplicate);
onPromptAnswered = (accepted, item, index) => {
this.setState(prevState => ({
...prevState,
items: prevState.items
.map((i, key) => (index === key ? ({
...item,
shouldUploadDuplicate: accepted,
}) : item)),
performUpload: accepted, // if at last an item was rejected, then the upload won't be executed
}));
};
uploadToApi = (items) => {
if (!this.getFirstDuplicateItemToPrompt()) {
const itemsToUpload = items.filter(i => i.isValid);
uploadDataToApi(itemsToUpload);
}
};
render() {
const { items } = this.stat;
const itemElements = items.map((item, key) => (
<MyItem key={key} {...item} />
));
const promptElement = this.getPromptElement();
return (
<div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{itemElements}
</div>
<Button onClick={this.onUploadButtonClick}>Upload</Button>
{promptElement}
</div>
)
}
}

ReactJS: How to set state of an object selected via radio button?

In my UsedComponents component I have a list of mapped radio buttons that each return componentType as value.
this.props.usedComponents.map((component, index) => (
<RadioButton
key={index}
id={component.componentType}
icon={ButtonIco}
label={component.componentName}
name="select"
value={component.componentType}
inputClass={classes.RadioInput}
labelClass={classes.RadioLabel}
selected={component.selected}
handleChange={this.props.selectComponent}
/>
))
My state looks like this:
constructor(props) {
super(props);
this.state = {
components: components,
usedComponents: [components[0], components[2], components[3]],
};
this.selectComponentHandler = this.selectComponentHandler.bind(this);
}
components is an imported array of objects that each look something like this:
{
componentType: "headerLogoNavigation",
componentName: "Header 02",
padding: "small",
fontSize: "small",
fontColor: "#1f1f1f",
fontFamily: "Sans-serif",
backgroundColor: "#ffffff",
image: placeholderLogo,
selected: false,
isEditing: false,
margins: false,
roundCorners: "none",
mobile: "false"
}
In my Page component I'm trying to pass a selectComponentHandler prop to my UsedComponents component that should select a component based on a value of a selected radio button and set its state to selected: true. For an added bonus it should set the state of any previously selected component to selected: false.So far I managed to figure out how to select the component but I'm not able to update its state. My final attempt to create this handler before I gave up looks like this:
selectComponentHandler = event => {
this.setState(prevState => {
let selected = prevState.usedComponents.filter(item => item.componentType === event.target.value);
selected.selected = 'true';
return { selected };
});
};
and it's an attempt to filter the prevState inside the setState for the componentType that matches event.target.value of the radio button and set it's state, but I messed up the logic or the syntax and my head is about to explode so I can't figure out what I did wrong.
Can someone help me figure this out?
I figured it out. It's a bit hacky but it works.
selectComponentHandler = event => {
const value = event.target.value;
this.setState(prevState => {
let selected = prevState.usedComponents.filter(item => item.componentType === value).shift();
let unSelected = prevState.usedComponents.filter(item => item.selected === true).shift();
if(unSelected) {
unSelected.selected = false;
}
selected.selected = true;
return { unSelected, selected };
});
};

React set button to disabled with state

I am in a React class-based component and a property on my state like so:
state = {
controls: {
email: {
validation: {
required: true,
isEmail: true
},
invalid: false,
},
password: {
validation: {
required: true,
minLength: 6
},
invalid: true,
},
disabledSubmit: true,
}
};
I have an inputChangedHandler triggered from an input component:
inputChangedHandler = (event, controlName) => {
console.log("[STATE]", this.state.controls);
const updatedControls = {
...this.state.controls,
[controlName]: {
...this.state.controls[controlName],
value: event.target.value,
invalid: !this.checkValidity(
event.target.value,
this.state.controls[controlName].validation
)
},
touched: true
};
console.log("[UPDATEDCONTROLS]", updatedControls);
this.setState({ controls: updatedControls }, () => {
this.disabledSubmitHandler();
});
};
And a disabledSubmitHandler that should be being called from within my inputChangedHandler :
disabledSubmitHandler() {
if (
!this.state.controls["email"].invalid &&
!this.state.controls["password"].invalid
) {
this.setState(
{ disabledSubmit: true },
console.log("[DISABLEDHANDLER] TRUE")
);
}
}
The prop is set on my button component in my JSX like so:
<Button
value="submit"
clicked={this.submitHandler}
disabled={this.state.disabledSubmit}
/>
This does not work, but I'm not sure what's happening?
I think maybe this bit seems to need fixing:
disabledSubmitHandler() {
if (
!this.state.controls["email"].invalid && //if email is not invalid
!this.state.controls["password"].invalid //if password is not invalid
) {
this.setState(
{ disabledSubmit: true },
console.log("[DISABLEDHANDLER] TRUE")
);
}
}
That code says if the email and password are valid, disable the input. I think it should be:
disabledSubmitHandler() {
if (
!this.state.controls["email"].invalid &&
!this.state.controls["password"].invalid
) {
this.setState(
{ disabledSubmit: false },
console.log("[DISABLEDHANDLER] FALSE")
);
}
}
Plas as #an0nym0us mentioned, disabledSubmit is nested inside controls.
Also, on a side note, it seems a little odd that you would call a function which sets state, only to call another function which sets state inside that function, as a callback to set state (inputChangedHandler calling disabledSubmitHandler'). It seems you could call that disabled check from withininputChangedHandlerpassing it yourupdatedControls, and return true/false fordisabledSubmit, resulting in a single call tosetState`

Resources