I have an if statement that errors out if a user selects a file format that is not valid, however, it is currently done within js as an alert, I would like to change this to Material UI's snackbar error message. However I am having trouble doing so, the code below.
let file = document.getElementById("file-upload");
const onSelectFile = e => {
setFieldValue(e.target.name, e.target.files[0]);
setFieldTouched(e.target.name, true, false);
let fileName = file.value;
if (
fileRestrict.findIndex(item => fileName.toLowerCase().endsWith(item)) >= 0
) {
//TODO
} else {
{
LongTextSnackbar;
}
file.value = "";
}
};
function LongTextSnackbar() {
return (
<div className={classes.root}>
<SnackbarContent
className={classes.snackbar}
message={"error error error"}
/>
</div>
);
}
I have tried to imitate Mat UI's `snackbar, but to no avail, inside the else, I need to call another component of material UI's somehow, any suggestions on how I could do this?
Maybe check this library https://www.polonel.com/snackbar/
Usage is simple like this:
Snackbar.show({text: 'Example notification text.'});
Patch this into your code-
Set initial state of snackbar to closed:
state = {
snackOpen: false
}
Logic to open the snackbar by setting state:
let file = document.getElementById('file-upload');
const onSelectFile = e => {
setFieldValue(e.target.name, e.target.files[0]);
setFieldTouched(e.target.name, true, false);
let fileName = file.value;
if (
fileRestrict.findIndex(item => fileName.toLowerCase().endsWith(item)) >= 0
) {
} else {
this.setState({ snackOpen: "You have tried to upload an unsupported file type. Allowed file types are ${fileRestrict}"})
file.value = '';
}
};
Need to render the snackbar as shown:
render() {
return (
<Snackbar message={this.state.snackOpen} close={() => this.setState({ snackOpen: false })} />
)
}
Note:
I have not tested this, just plugged a snackbar in to demonstrate
how it needs to be done.
The material-ui Dialog component is more appropriate for this use case.
Related
What I'm looking for is to have my custom input validation to show errors input by input, as I type into them (not on submit). What it does now is showing every errors as soon as I type inside any input. I know the reason : because setErrors() is in handleFormChange(), therefore every errors are set as soon as I type in any input, but I can't find a solution that meets my needs.
What I try to do is put most of the form's logic in the Form component, as it is where FormContext is defined. I believe doing so will give me more reusability, as the only parts I would need to change to reuse the components are only the validate function (to define the input's rules) and the onSubmit function, that are located in the parent component (here App). No need to touch anything on Form or FormInput when everything will be set.
I know react-hook-form and how to use it but I would like to create my own form / input validation logic. Everything works as needed expect this error display problem.
Here is the Form component (including FormContext) :
export const FormContext = createContext({
form: {},
errors: {},
});
export default function Form({
formInitialValues,
onSubmit = () => {},
validate = () => {},
children
}) {
const [form, setForm] = useState(formInitialValues);
const [errors, setErrors] = useState({});
const handleFormChange = (e) => {
const { name, value } = e.target || e;
const updatedForm = {
...form,
[name]: value,
};
const errors = validate(updatedForm);
setForm(updatedForm);
setErrors(errors);
};
return (
<FormComp>
<FormContext.Provider value={{
form,
errors,
handleFormChange,
}}>
{ children }
</FormContext.Provider>
<button type="button" onClick={() => onSubmit(form)}>
Envoyer
</button>
</FormComp>
)
}
Here is my FormInput component :
export default function FormInput({
label,
type = 'text',
name,
forwardRef,
}) {
const formContext = useContext(FormContext);
const { form, errors, handleFormChange } = formContext;
const inputRef = useRef(null);
return (
<InputGroup>
<label>{ label }</label>
<input
type={type}
name={name}
value={form[name]}
onChange={handleFormChange}
ref={forwardRef ? forwardRef : inputRef} />
{ errors && errors[name] && errors[name].length > 0 && <span>{ errors[name] }</span> }
</InputGroup>
)
}
And here is my App component, where I define the validate function (that is used in Form -> handleFormChange()) :
function App() {
const initialValues = {
lastName: '',
firstName: '',
};
const validate = (form) => {
let errors = {};
if (form.lastName === '') {
errors.lastName = 'Last name is required'
}
if (form.firstName === '') {
errors.firstName = 'This field is required';
}
return errors;
};
const onSubmit = (form) => {
console.log({ form });
};
return (
<div className="App">
<h1>S'inscrire</h1>
<Form
formInitialValues={initialValues}
onSubmit={onSubmit}
validate={validate}
>
<FormInput
label="Nom"
name="lastName" />
<FormInput
label="Prénom"
name="firstName" />
</Form>
</div>
);
}
export default App;
Could anyone point me in the right direction ? I tried lots of stuff but nothing works (at least nothing that meets my needs). This should be simple but I don't understand why I can't get it right (even though I know where the problem lies). Thank you very much for your help.
---------- EDIT ----------
Ok so I found a way to make it happen, but it seems a little bit "too much" (and still not working properly), I'm guessing there should be a more simple way to achieve what I want.
So what I did is setting a new state errors in the parent component (App) that I use to set the errors in the validate function (they are not set in Form -> handleFormChange() anymore), that I then pass down as a prop to Form. Here are the modified parts :
App component :
function App() {
//.... Cut down unnecessary parts ....
const [errors, setErrors] = useState({});
const validate = (name, value) => {
const omit = (key, { [key]: _, ...obj }) => obj
switch (name) {
case 'lastName':
if (value === '') {
setErrors({
...errors,
[name]: 'Last name required',
});
}
else {
let newObj = omit('lastName', errors);
setErrors(newObj);
}
break;
case 'firstName':
if (value === '') {
setErrors({
...errors,
[name]: 'First name required',
});
}
else {
let newObj = omit('firstName', errors);
setErrors(newObj);
}
break;
}
//.... Same as before ....
}
Then here is the new Form component (I'm just showing the handleFormChange function, everything is the same expect for the fact that I pass down the errors prop from App to Form (used in handleFormChange() -> validate()) :
const handleFormChange = (e) => {
const { name, value } = e.target || e;
const updatedForm = {
...form,
[name]: value,
};
setForm(updatedForm);
validate(name, value);
};
So as you can see, validate() still runs in Form but the errors are not set in Form anymore (they are set inside validate(), in the parent component App, in the switch statement).
This almost works as I would like : the error messages show input by input, but only one input at the time. What I mean is, if I type in and blank the first input, it displays the error for this input only, but then if I type in the second input while the first error is set, the first error message disappears but the second input error message displays (and even if I don't trigger the second input's error, it still hides the first error message), and vice versa.
What am I doing wrong ? I would greatly appreciate some advice. Thank you very much
Ok so I found the problem and now everything works as expected : as you type in the first input, if an error occurs for that input it displays it. Then if you type in the second input and an error occurs there, it also displays. And when I have no more errors, it hides them input by input.
What I missed was only better defining values in the validate() switch statement. What I mean is, before I only did if (values === ''), where I needed to do if (values.fieldName === '') to set the errors.
Here is the new validate function in the parent component (App) (note that I also use the name variable everywhere I can to avoid repeating lastName and firstName) :
switch (name) {
case 'lastName':
if (values.lastName === '') {
setErrors({
...errors,
[name]: 'Lastname is required'
})
}
else {
let newObj = omit(name, errors);
setErrors(newObj);
}
break;
case 'firstName':
if (values.firstName === '') {
setErrors({
...errors,
[name]: 'Firstname is required'
})
}
else {
let newObj = omit(name, errors);
setErrors(newObj);
}
break;
}
}
And here is the handleFormChange function in Form, where I needed to do validate(name, udpatedForm.values) instead of validate(name, updatedForm) (or as I showed before validate(name, values)) :
const handleFormChange = (e) => {
const { name, value } = e.target || e;
const updatedForm = {
...form,
values: {
[name]: value
},
};
validate(name, updatedForm.values);
setForm(updatedForm);
};
So that was just a silly error on my part... But now everything works great ! I now have my full reusable form / input validation using React context, and if I want to reuse the components, I just have to modify the validate function in the parent component (as well as formInitialValues), and everything is handled within the Form component, holding the context. So no need to touch this anytime soon ! Reusability for life (at least until I decide to add some stuff up maybe).
Note that I still set the errors in the parent component (in validate()), that I then pass down as props to Form. Is anyone as a better suggestion for going about this, please go ahead (I would have liked all the errors to be set in Form but eh, that's what I came up with, it works and meets my needs). Thank you very much
I have a component that is rather complex, so I build it from nested components. Simplified:
export function BlockProvideFeedback(props) {
const handleSelect = (event) => console.log(event);
const Form = (props) => (
<>
<RadioList selected={props.selected} />
<Button onClick={handleSelect} />
</>
);
const Message = () => (
<p>Thanks for the feedback</p>
);
if (props.feedbackStatus == 'agreed') {
return(<Form selected='done'/>);
if (props.feedbackStatus == 'pending') {
return(<Form selected='needswork'/>);
} else {
return(<Message/>);
}
};
BlockProvideFeedback.propTypes = {
feedbackStatus: PropTypes.string.isRequired,
};
The linter is, rightfully IMO, pointing out that props.selected in Form(props) needs propTypes. But I don't know where and how to provide them. 'selected' is missing in props validation.
I tried the obvious:
BlockProvideFeedback().Form.propTypes { //... }
But that throws errors because I'm instantiating the component (by calling the function) without the proper context, props, providers etc.
How do I put selected for my nested component, in a props validation?
Or am I maybe doing something else horribly wrong: for example, my entire setup of nesting the maybe is so non-react-ish that tooling, like linters, will fail on it without being able to solve it?
If Form and Message are not intended to be shared and reused, I would convert them in "render functions"; something like:
export function BlockProvideFeedback(props) {
const handleSelect = (event) => console.log(event);
const renderForm = (selectedVal) => (
<>
<RadioList selected={selectedVal} />
<Button onClick={handleSelect} />
</>
);
const renderMessage = () => (
<p>Thanks for the feedback</p>
);
if (props.feedbackStatus == 'agreed') {
return renderForm('done');
if (props.feedbackStatus == 'pending') {
return renderForm('needswork');
} else {
return renderMessage();
}
};
BlockProvideFeedback.propTypes = {
feedbackStatus: PropTypes.string.isRequired,
};
Not really an answer to your question, I know, but it seems to me this case can be a sort of "XY problem".
I am fairly new to React and trying to edit a former coworker's code (hence the non-traditional React style).
I need to return a series of buttons (here "Hello" and "Goodbye") when a single button ("Dropdown") is clicked. In order to make this more dynamic down the line, I'd like to return those buttons from a function (dropdownDetails) rather than [show,setShow] variables, etc.
When I run this code, the console shows that dropdownDetails is triggered, but "Hello" and "Goodbye" don't appear in the UI. What am I doing wrong?
Thanks in advance!
///// In base.js:
const e = React.createElement;
///// in page_body.js:
function pageBody(props) {
const dropdownDetailsWrap = (props) => (e) => {
console.log("Wrap triggered")
dropdownDetails()
}
function dropdownDetails() {
console.log("dropdownDetails useEffect triggered")
return(
e("button",{type:"button"},"Hello"),
e("button",{type:"button"},"Goodbye")
);
};
const pageBody = () => (e) => {
return(
e("button", {type:"button",onClick:dropdownDetailsWrap(),className:'btn'},"Dropdown")
)}
}
ReactDOM.render(e(pageBody), document.querySelector('#page_body'));
Note: I know there are more elegant ways to make a dropdown, particularly using packages. Just trying to understand the basics with pure React for now.
Probably you can do like this provided you use the useState hook of react.
Wihtout using state, I doubt we can do this.
I am pasting the code below, check it out on codesandbox to explore more.
Here is the link Link
Code :
import React, { useState } from "react";
const e = React.createElement;
export default function App() {
return (
<div className="App">
<PageBody />
</div>
);
}
function PageBody(props) {
var buttonsToShow;
const [showButtons, setshowButtons] = useState(false);
const [buttons, setButtons] = useState([]);
const dropdownDetailsWrap = (props) => {
console.log("Wrap triggered");
buttonsToShow = dropdownDetails();
setButtons(buttonsToShow);
setshowButtons(true);
};
function dropdownDetails() {
console.log("dropdownDetails useEffect triggered");
return [
e("button", { type: "button", key: 1 }, "Hello"),
e("button", { type: "button", key: 2 }, "Goodbye")
];
}
var dropbutton = () => {
return e(
"button",
{ type: "button", onClick: dropdownDetailsWrap, className: "btn" },
"Dropdown"
);
};
return (
<div>
{dropbutton()} <br /> {showButtons && buttons}
</div>
);
}
Now,I know this is not the best version but still you can clean this code and use the logic. Also by using state your code does not become static, moreover you get to customize more things,
Long question please be ready
I've been working with react-query recently and discovered that a lot of the code has to be duplicated for every component that is using useQuery. For example:
if(query.isLoading) {
return 'Loading..'
}
if(query.isError) {
return 'Error'
}
if(query.isSuccess) {
return 'YOUR ACTUAL COMPONENT'
}
I tried creating a wrapper component to which you pass in the query information and it will handle all the states for you.
A basic implementation goes as follows:
const Wrapper = ({ query, LoadingComponent, ErrorComponent, children }) => {
if (query.isLoading) {
const toRender = LoadingComponent || <DefaultLoader />;
return <div className="grid place-items-center">{toRender}</div>;
}
if (query.isError) {
const toRender = ErrorComponent ? (
<ErrorComponent />
) : (
<div className="dark:text-white">Failed to Load</div>
);
return <div className="grid place-items-center">{toRender}</div>;
}
if (query.isSuccess) {
return React.Children.only(children);
}
return null;
};
And, using it like:
const Main = () => {
const query = useQuery(...);
return (
<Wrapper query={query}>
<div>{query.message}</div>
</Wrapper>
)
}
The issue with this is that query.message can be undefined and therefore throws an error.
Can't read property message of undefined
But, it should be fixable by optional chaining query?.message.
This is where my confusion arises. The UI elements inside wrapper should have been rendered but they donot. And, if they don't then why does it throw an error.
This means that, the chilren of wrapper are executed on each render. But not visible. WHY??
Render Props to rescue
Wrapper
const WrapperWithRP = ({ query, children }) => {
const { isLoading, data, isError, error } = query;
if (isLoading) return 'Loading...'
if (isError) return 'Error: ' + error
return children(data)
}
And using it like,
const Main = () => {
const query = useQuery(...);
return (
<Wrapper query={query}>
{state => {
return (
<div>{state.message}</div>
)
}
}
</Wrapper>
)
}
This works as expected. And the children are only rendered when the state is either not loading or error.
I am still confused as to why the first approach doesn't show the elements on the UI event when they are clearly executed?
Codesandbox link replicating the behaviour: https://codesandbox.io/s/react-composition-and-render-prop-tyyzh?file=/src/App.js
Okay, so i figured it out and it is simpler than it sounds.
In approach one: the JSX will still be executed because it's a function call React.createElement.
But, Wrapper is only returning the children when the query succeeds. Therefore, the children won't be visible on the UI.
Both approaches work, but with approach 1, we might have to deal with undefined data.
I ended up using render-prop as my solution as it seemed cleaner.
I am developing a light weight project using React Native, and I encountered some setbacks, I couldn't figure it out. :(
I have a page that contains a Yes and a No button and a Yes/No render area, users will be able to click on either of the buttons. According to the users' choice, an avatar will appear in the correct render area (click yes, the avatar will be in the Yes area...). But one user can only be able to click once. I am trying to solve this using state and setState, but couldn't get it to work.
I have:
this.state = {invitedState : false}
and a function (part)
onPress={() => {
if (this.state.invitedState) {
onPress();
}
this.setState(prevState => ({
invitedState: !prevState.invitedState,
}));
}}
Should I not use setState to solve this problem?
thanks!
I think I understand your problem. Something like this?
state = {
toggleUI: true,
userToggled: false
};
handleToggleUI = e => {
this.setState(currentState => {
if ( this.state.userToggled === false ) {
return {
toggleUI: !currentState.toggleUI,
userToggled: true
};
}
});
};
You could try:
onPress{() => {
let tempVar = this.state.invitedState ? false : true;
this.setState({invitedState: tempVar});
}