I am working on a react coding template where after clicking on submit, it shows me a successful message and stays on the same page. I would like to add a redirect to the submit button if successful without changing the design. My ./AskPage component uses interfaces from ./Form. How do I add a redirect to the ./AskPage once I click on submit? Below is my code
./Form code is:
import { FC, useState, createContext, FormEvent } from 'react';
import { PrimaryButton, gray5, gray6 } from './Styles';
/** #jsx jsx */
import { css, jsx } from '#emotion/core';
export interface Values {
[key: string]: any;
}
export interface Errors {
[key: string]: string[];
}
export interface Touched {
[key: string]: boolean;
}
/* export interface onSubmit {
[key: string]: any
} */
interface FormContextProps {
values: Values;
setValue?: (fieldName: string, value: any) => void;
errors: Errors;
validate?: (fieldName: string) => void;
touched: Touched;
setTouched?: (fieldName: string) => void;
}
export const FormContext = createContext<FormContextProps>({
values: {},
errors: {},
touched: {},
});
type Validator = (value: any, args?: any) => string;
export const required: Validator = (value: any): string =>
value === undefined || value === null || value === ''
? 'This must be populated'
: '';
export const minLength: Validator = (value: any, length: number): string =>
value && value.length < length
? `This must be at least ${length} characters`
: '';
interface Validation {
validator: Validator;
arg?: any;
}
interface ValidationProp {
[key: string]: Validation | Validation[];
}
export interface SubmitResult {
success: boolean;
errors?: Errors;
}
interface Props {
submitCaption?: string;
validationRules?: ValidationProp;
onSubmit: (values: Values) => Promise<SubmitResult>;
successMessage?: string;
failureMessage?: string;
}
export const Form: FC<Props> = ({
submitCaption,
children,
validationRules,
onSubmit,
successMessage = 'Success!',
failureMessage = 'Something went wrong',
}) => {
const [values, setValues] = useState<Values>({});
const [errors, setErrors] = useState<Errors>({});
const [touched, setTouched] = useState<Touched>({});
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [submitError, setSubmitError] = useState(false);
const validate = (fieldName: string): string[] => {
if (!validationRules) {
return [];
}
if (!validationRules[fieldName]) {
return [];
}
const rules = Array.isArray(validationRules[fieldName])
? (validationRules[fieldName] as Validation[])
: ([validationRules[fieldName]] as Validation[]);
const fieldErrors: string[] = [];
rules.forEach(rule => {
const error = rule.validator(values[fieldName], rule.arg);
if (error) {
fieldErrors.push(error);
}
});
const newErrors = { ...errors, [fieldName]: fieldErrors };
setErrors(newErrors);
return fieldErrors;
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (validateForm()) {
setSubmitting(true);
setSubmitError(false);
const result = await onSubmit(values);
setErrors(result.errors || {});
setSubmitError(!result.success);
setSubmitting(false);
setSubmitted(true);
}
};
const validateForm = () => {
const newErrors: Errors = {};
let haveError: boolean = false;
if (validationRules) {
Object.keys(validationRules).forEach(fieldName => {
newErrors[fieldName] = validate(fieldName);
if (newErrors[fieldName].length > 0) {
haveError = true;
}
});
}
setErrors(newErrors);
return !haveError;
};
return (
<FormContext.Provider
value={{
values,
setValue: (fieldName: string, value: any) => {
setValues({ ...values, [fieldName]: value });
},
errors,
validate,
touched,
setTouched: (fieldName: string) => {
setTouched({ ...touched, [fieldName]: true });
},
}}
>
<form noValidate={true} onSubmit={handleSubmit}>
<fieldset
disabled={submitting || (submitted && !submitError)}
id="fieldset" >
{children}
<div id="children" >
<PrimaryButton type="submit">{submitCaption}</PrimaryButton>
</div>
{submitted && submitError && (
<p id="failure" >
{failureMessage}
</p>)}
{submitted && !submitError && (
<p id="success" >
{successMessage}
</p>)}
</fieldset>
</form>
</FormContext.Provider>
);
};
My ./AskpPage is:
import React, { useState, FC } from 'react';
import { Page } from './Page';
import { Form, required, minLength, Values } from './Form';
import { Field } from './Field';
import { postQuestion } from './QuestionsData';
import { BrowserRouter, Route, Redirect, Switch, Link } from 'react-router-dom';
import { isPropertySignature } from 'typescript';
export const AskPage = () => {
const handleSubmit = async (values: Values) => {
const question = await postQuestion({
title: values.title,
content: values.content,
userName: 'Fred',
created: new Date(),
});
return { success: question ? true : false };
};
return (
<Page title="Ask a Question">
{/* I want to add a redirect to this form once I click Submit Your Question */}
<Form
submitCaption="Submit Your Question"
validationRules={{
title: [{ validator: required }, { validator: minLength, arg: 10 }],
content: [{ validator: required }, { validator: minLength, arg: 20 }],
}}
onSubmit={handleSubmit}
failureMessage="There was a problem with your question"
successMessage="Your question was successfully submitted"
>
<Field name="title" label="Title" />
<Field name="content" label="Content" type="TextArea" />
</Form>
</Page>
);
};
export default AskPage;
I use this code example to redirect after my form submissions in react. I basically do all that is needed and save the answer then call the appropriate function to take me to an appropriate page. That page may then go get the answer. Another way would be to use props and import another page...
function viewPage() {
let url = window.location.origin;
console.log('viewPage', url)
url=url.concat('/SelfAssessment')
window.open(url, "_blank");
}
Related
I am learning React. In a tutorial I saw online regarding handling Form submissions, there is a component Form which is being extended by another component LoginForm.
Form.jsx:
import { React, Component } from "react";
import Joi from "joi-browser";
class Form extends Component {
state = {
data: {},
errors: {},
};
validate = () => {
const options = { abortEarly: false };
const { error } = Joi.validate(this.state.data, this.schema, options);
if (!error) return null;
const errors = {};
for (let item of error.details) errors[item.path[0]] = item.message;
return errors;
};
validateProperty = ({ name, value }) => {
const obj = { [name]: value };
const schema = { [name]: this.schema[name] };
const { error } = Joi.validate(obj, schema);
return error ? error.details[0].message : null;
};
handleSubmit = (e) => {
e.preventDefault();
const errors = this.validate();
this.setState({ errors: errors || {} });
if (errors) return;
this.doSubmit();
};
handleChange = ({ currentTarget: input }) => {
const errors = { ...this.state.errors };
const errorMessage = this.validateProperty(input);
if (errorMessage) errors[input.name] = errorMessage;
else delete errors[input.name];
const data = { ...this.state.data };
data[input.name] = input.value;
this.setState({ data, errors });
};
}
export default Form;
LoginForm.jsx:
import React, { Component } from "react";
import Form from "./common/form";
import Joi from "joi-browser";
import Input from "./common/input";
class LoginForm extends Form {
state = {
data: { username: "", password: "" },
errors: {},
};
schema = {
username: Joi.string().required().label("Username"),
password: Joi.string().required().label("Password"),
};
doSubmit = () => {
// call the server
console.log("Submitted");
};
render() {
const { data, errors } = this.state;
return (
<div>
<h1>Login</h1>
<form onSubmit={this.handleSubmit}>
<Input
name="username"
value={data.username}
label="Username"
onChange={this.handleChange}
error={errors.username}
/>
<Input
name="password"
value={data.password}
label="Password"
onChange={this.handleChange}
error={errors.password}
/>
<button disabled={this.validate()} className="btn btn-primary">
Login
</button>
</form>
</div>
);
}
}
export default LoginForm;
Since LoginForm is extending Form, in the validate() function, how can Form be using properties like this.schema, if schema is defined in LoginForm.jsx?
I have an React Component Which Consists Dropdown , the dropdown has 4 values 'requsted,all,taken,available' and initially all the List of item are loaded. Each Item has isRequested,isTaken and isAvailable. Now, I need to filter those arrays accordingly to dropdown but i am getting something else as an output
My React Component is:
import React, { useContext, useEffect, SyntheticEvent, useState } from 'react'
import { observer } from 'mobx-react-lite';
import { Dropdown, Segment, Item, Icon, Label, Button, Select } from 'semantic-ui-react';
import { BooksStatus } from '../../app/common/options/BookStatus';
import { RootStoreContext } from '../../app/stores/rootStore';
import { format } from 'date-fns';
import { NavLink } from 'react-router-dom';
import { id } from 'date-fns/esm/locale';
const LibrarianManager: React.FC = () => {
const rootStore = useContext(RootStoreContext);
const { loadBooks, getAvailableBooks, deleteBook } = rootStore.bookStore;
const [status, setStatus] = useState(BooksStatus[0].value);
useEffect(() => {
loadBooks();
}, [loadBooks]);
const onChange = (value: any) => {
setStatus(value)
console.log(value)
if (value === 'requested') {
if (value === 'requested') {
getAvailableBooks.filter(data => data.isRequested == true)
console.log(getAvailableBooks)
}
}
}
return (
<div>
<Select
value={status}
onChange={(e, data) => onChange(data.value)}
options={BooksStatus}
/>
{getAvailableBooks.map(books => (
<Segment.Group key={books.bookName}>
<Segment>
<Item.Group>
<Item>
<Item.Image size='tiny' circular src='/assets/books.jpg' />
<Item.Content>
<Item.Header as={NavLink} to={`/booksDetail/${books.id}`} >{books.bookName}</Item.Header>
</Item.Content>
</Item>
</Item.Group>
</Segment>
<Segment clearing>
<span></span>
</Segment>
</Segment.Group>
))}
</div>
)
}
export default observer(LibrarianManager);
My Map Functions is :-
#computed get getAvailableBooks() {
return Array.from(this.bookRegistry.values());
}
#action loadBooks = async () => {
this.loadingInitial = true;
try {
const books = await agent.Books.list();
runInAction("loading books", () => {
books.forEach((books) => {
books.issuedOn = new Date(books.issuedOn);
this.bookRegistry.set(books.id, books);
});
this.loadingInitial = false;
});
} catch (error) {
runInAction("load books error", () => {
this.loadingInitial = false;
});
}
};
and my data Model or the item is
export interface IBooks {
id: number;
bookname: string;
issuedOn: Date;
returnDate: Date;
isReturned: boolean;
isRequested: boolean;
isAvailable: boolean;
isTaken: boolean;
name: string;
requestedBy: string;
}
while trying by this method
if (value === 'requested') {
if (value === 'requested') {
getAvailableBooks.filter(data => data.isRequested == true)
console.log(getAvailableBooks)
}
in console i am getting all the list without filtering
Array.filter doesn't mutate the original array but returns a new one which is nice (MDN).
What you want is:
const filteredBooks = getAvailableBooks.filter(data => data.isRequested == true)
console.log(filteredBooks)
I have 2 components:
Form
EmailForm (which extends Form)
I don't understand why my state "errors" is not updated whenI submit the form button?
When I check in the component react tool in chrome dev tools nothing happens.
Do you know what I didn't do correctly in the handleSubmit and validate function?
import React from 'react';
import Form from './form';
class EmailForm extends Form {
state = {
data: { email: '' },
errors: {}
};
render() {
return (
<div>
<p>
<i>Renseignez votre email pour être averti dès que l'application sera disponible :</i>
</p>
<form onSubmit={this.handleSubmit}>
{this.renderInput('email', 'Email')}
{this.renderButton('Envoyer')}
</form>
</div>
);
}
}
export default EmailForm;
import React, { Component } from 'react';
import { ButtonPrimary } from './buttonPrimary';
import { ButtonTransparent } from './buttonTransparent';
import Input from './input2';
interface IFormState {
data: any;
errors: any;
}
class Form extends React.PureComponent<{}, IFormState> {
state = {
data: {},
errors: {}
};
validate = (): any => {
return { email: 'Email is required' };
};
validateProperty = (input: any) => {
console.log('validated Property');
};
handleSubmit = (e: any) => {
e.preventDefault();
const errors: any = this.validate();
this.setState({ errors });
if (errors) return;
this.doSubmit();
};
doSubmit = () => {
console.log('Submitted');
};
handleChange = ({ currentTarget: input }: any) => {
const errors: any = { ...this.state.errors };
const errorMessage: any = this.validateProperty(input);
if (errorMessage) errors[input.name] = errorMessage;
else delete errors[input.name];
const data: any = { ...this.state.data };
data[input.name] = input.value;
this.setState({ data, errors });
};
renderButton(label: string) {
return <ButtonPrimary disabled={!this.validate()}>{label}</ButtonPrimary>;
}
renderInput(name: string, label: string, type = 'email') {
const { data, errors }: any = this.state;
return (
<Input
autoFocus
type={type}
name={name}
value={data[name]}
label={label}
onChange={this.handleChange}
error={errors[name]}
/>
);
}
}
export default Form;
I created a component to dispaly a question and its different options and when a user click the next button, a redirection to the same page will be executed in order to rerender the component and display a new question.
I use a checkBox component to display the question options but whenever I chose an option and go to the next question; the checkboxes are not reset for the new one (for example if I checked the second option for the first question, I get the second option checked in the second question before I checked it).
import Grid from "#material-ui/core/Grid";
import Typography from "#material-ui/core/Typography";
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import SyntaxHighlighter from "react-syntax-highlighter";
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Dispatch } from "redux";
import { IAnswer, incrementQuestion, IQuestion, questionRequest } from "../../actions/index";
import CheckBoxWrapper from "../../components/common/CheckBoxWrapper";
import ContentQuiz from "../../components/ContentQuiz";
import history from "../../history/history";
interface IProps {
currentQuestionNumber: number;
loadingData: boolean;
questions: IQuestion[];
questionRequest: () => void;
incrementQuestion: (arg: IAnswer) => void;
numberOfQuestions: number;
}
interface IAnswerOption {
option1: boolean;
option2: boolean;
option3: boolean;
option4: boolean;
[key: string]: boolean;
}
const Quiz = (props: IProps) => {
const { currentQuestionNumber,
loadingData,
questions,
questionRequest,
incrementQuestion,
numberOfQuestions } = props;
const [answerOption, setAnswerOption] = useState<IAnswerOption>({
option1: false,
option2: false,
option3: false,
option4: false,
});
const handleChange = (option: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
setAnswerOption({ ...answerOption, [option]: event.target.checked });
};
useEffect(() => {
questionRequest();
});
const handleNextQuiz = () => {
if (currentQuestionNumber === numberOfQuestions - 1) {
history.push("/homepage");
} else {
incrementQuestion(answerOption);
history.push("/contentQuiz");
}
};
const currentQuestion = questions[currentQuestionNumber];
return (
<div>
{loadingData ? ("Loading ...") : (
< ContentQuiz
questionNumber={currentQuestionNumber + 1}
handleClick={handleNextQuiz} >
<div>
<Typography variant="h3" gutterBottom> What's the output of </Typography>
<>
<SyntaxHighlighter language="javascript" style={dark} >
{currentQuestion.description.replace(";", "\n")}
</SyntaxHighlighter >
<form>
<Grid container direction="column" alignItems="baseline">
{currentQuestion.options.map((option: string, index: number) => {
const fieldName = `option${index + 1}`;
return (
<Grid key={index}>
<CheckBoxWrapper
checked={answerOption[fieldName]}
value={fieldName}
onChange={handleChange(fieldName)}
label={option}
/>
</Grid>);
}
)}
</Grid>
</form>
</>
</div >
</ContentQuiz >
)}
</div>
);
};
const mapStateToProps = (state: any) => {
const { currentQuestionNumber, loadingData, questions, numberOfQuestions } = state.quiz;
return {
currentQuestionNumber,
loadingData,
questions,
numberOfQuestions
};
};
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
incrementQuestion: (answer: IAnswer) => dispatch<any>(incrementQuestion(answer)),
questionRequest: () => dispatch<any>(questionRequest())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Quiz);
How can I reset the question options whenever I rerender the component, in order to check the options?
const fieldName = `option${index + 1}`;
Reset fieldName when you are going to the next question
I think you just need to fix your useEffect.
useEffect(() => {
setAnswerOption({
option1: false,
option2: false,
option3: false,
option4: false,
});
questionRequest();
}, []);
Also, do not forget to pass the second argument, otherwise you might have an infinite loop in your component. https://reactjs.org/docs/hooks-effect.html
I have made an HOC which is providing props for network calls to the passed component - so network call loading indicators, error messages etc can be handled by the HOC.
The code works as expected by typescript is not happy.
In my TestContainer.tsx I am making a getQuotes prop to be injected by the HOC.
But when I use the wrapped exported component in my App.tsx it's complaining that I didn't specify the prop that is injected by by the HOC.
Type error: Property 'getQuotes' is missing in type '{ message: string; }' but required in type 'Readonly<Pick<OwnProps & NetworkProps, "message" | "getQuotes">>'. TS2741
25 |
> 26 | <TestContainer message="lala" />
| ^
27 | </div>
28 | );
29 | }
App.tsx:
<div className="App">
<TestContainer message="lala" />
</div>
This is my HOC
import * as React from "react";
interface Props {}
interface State {
errors: Error[];
isLoading: boolean;
}
export type FunctionMap = { [key: string]: (...args: any[]) => any };
export const withNetwork = <P extends Props>(
PassedComponent: React.ComponentType<P>,
calls: FunctionMap
) => {
return class NetworkWrapper extends React.Component<
Pick<P, Exclude<keyof P, keyof Props>>,
State
> {
functionMap: FunctionMap;
constructor(props: P) {
super(props);
this.functionMap = Object.keys(calls).reduce(
(prev: FunctionMap, current: string) => ({
...prev,
[current]: this.makeHandler(calls[current])
}),
{}
);
}
state = { errors: [], isLoading: false };
makeHandler = (func: (...orignalArgs: any) => any) => async (
...args: any[]
) => {
try {
this.setState({ isLoading: true });
const result = await func(...args);
this.setState({ isLoading: false });
return result;
} catch (error) {
this.setState({ isLoading: false });
this.setState(prev => ({ errors: [...prev.errors, error] }));
}
};
handleDismissError = () => {
this.setState((prev: State) => {
const [first, ...errors] = prev.errors;
return { errors };
});
};
render() {
const props = this.props as P;
return (
<div>
{this.state.isLoading && <h3>LOADING</h3>}
{this.state.errors.length > 0 && (
<>
<ul>
{this.state.errors.map(error => (
// #ts-ignore
<li>{error.message}</li>
))}
</ul>
<button onClick={this.handleDismissError}>Dismiss</button>
</>
)}
<div>
<PassedComponent {...this.functionMap} {...props} />
</div>
</div>
);
}
};
};
This is the TestContainer that use the HOC
import * as React from "react";
import axios from "axios";
import { withNetwork, FunctionMap } from "./NetworkWrapper";
interface OwnProps {
message: string;
}
interface NetworkProps {
getQuotes: (n: number) => Promise<any>;
}
interface State {
body?: any;
}
class TestContainer extends React.Component<OwnProps & NetworkProps, State> {
componentDidMount = async () => {
const { getQuotes } = this.props;
const data = await getQuotes(1234);
data && this.setState({ body: data });
};
state = { body: undefined };
render() {
const { body } = this.state;
const { message } = this.props;
return (
<div>
<h1>{message}</h1>
<section>
<code>{JSON.stringify(body, null, 2)}</code>
</section>
</div>
);
}
}
const api = {
getQuotes: async (n: number) => {
console.warn({ n });
const result = await axios.get(
"http://example.com/quotes"
);
if (result.status === 200) {
return result.data;
} else {
throw new Error("invalid response");
}
}
};
export default withNetwork(TestContainer, api);
Your getQuotes prop defined as require. When you sign getQuotes as optional, it wont give type error.
interface NetworkProps {
getQuotes?: (n: number) => Promise<any>;
}
or you need to use your TestContainer with getQuotes and message
<TestContainer message="....." getQuotes={.....}/>