Unable to access mutator functions in Wizard form page while using react-final-form - react-final-form

I am trying to create a Wizard form using react-final-form by referring to this code https://codesandbox.io/s/km2n35kq3v. For my use case I need some mutator functions to be used inside my form fields. This example illustrates how to do that - https://codesandbox.io/s/kx8qv67nk5?from-embed.
I am not sure how to access mutator functions in my form steps when I am using a wizard form instead of a single page form.
I tried to combine both the examples by modifying the <Form> component rendered by Wizard.js to pass in the mutators. However I cannot access these mutators in the Wizard form pages.
In Wizard.js
return (
<Form
mutators={{
// potentially other mutators could be merged here
...arrayMutators,
}}
render={({
handleSubmit,
submitting,
values,
pristine,
invalid,
form: {
mutators: {push, pop, remove},
},
}) => {
return (
<form onSubmit={handleSubmit}>
Another file index.js
<Wizard
initialValues={{ employed: true, stooge: "larry" }}
onSubmit={onSubmit}
>
<Wizard.Page>
<FieldArray name="customers">
{({ fields }) =>
fields.map((name, index) => (
<div key={name}>
<label>Cust. #{index + 1}</label>
<Field
name={`${name}.firstName`}
component="input"
placeholder="First Name"
/>
<span
onClick={() => fields.remove(index)}
style={{ cursor: "pointer" }}
>
❌
</span>
</div>
))
}
</FieldArray>
</Wizard.Page>
</Wizard>
It errors out - remove is undefined in index.js

Look at this working example: https://codesandbox.io/s/znzlqvzvnx
changes I have made:
Wizard.js
static Page = ({ children, mutators }) => {
if(typeof children === 'function'){
return children(mutators);
}
return children;
};
...
<form onSubmit={handleSubmit}>
{
// activePage
<activePage.type {...activePage.props} mutators={mutators} />
}
...
index.js (only first <Wizard.page>)
<Wizard.Page>
{
({ upper }) => (
<React.Fragment>
<div>
<label>First Name</label>
<Field
name="firstName"
component="input"
...
</div>
</React.Fragment>
)
}
</Wizard.Page>

Related

Rendering new list item after adding it from a nested form. React hooks, redux, React Router V6

I am creating a list tracking app with React hooks, Redux, and Ruby on Rails. There is a List model, with a title as a string and completed as a boolean, and a ListItem model with descriptions as a string (the list item), completed boolean, and list_id as an integer.
I am using react route V6 for this and getting a little lost in re-rendering/ updating the page. Here is the breakdown of the application:
On the home screen, you can click to view all Lists and add a new list. when viewing all list each list title is displayed as a link to that list show page. The show page shows the list title, list items and a form to add another list item. Now where I am having trouble is being able to add a new list item, and it display on the page right after submission. Right now when I add a new item, and refresh the page it is not there. But if I click back to view all lists, then click that list again it shows up under the list items.
I tried using useNavigate to navigate to that list show page even though it is already on it but I am getting this error
Uncaught TypeError: Cannot destructure property 'list' of 'location.state' as it is null.
Here is all my components:
App.js
class App extends React.Component {
render(){
return (
<div className="App">
<Navbar/>
<br></br>
<Routes>
<Route path="/" element={<Home/>} />
<Route path="/lists" element={<Lists />} />
<Route path="/lists/new" element={<ListForm />} />
<Route path="/lists/:id" element={<ListContainer />} />
</Routes>
</div>
);
}
}
Lists.js
export default function Lists() {
const lists = useSelector(state => state.lists)
// replaces mapStateToProps
const dispatch = useDispatch()
// replaces mapDispatchToProps
useEffect(() => {
dispatch(fetchLists())
}, [])
return (
<div>
{Array.isArray(lists) && lists.map((list) => {
return (
<Link
key={list.id}
to={`/lists/${list.id}`}
state={{ list: list }}
>
<h2>{list.title}</h2>
</Link>
)
})}
</div>
)
}
ListContainer.js
export default function ListContainer() {
const location = useLocation();
const { list } = location.state;
console.log(list)
return (
<div>
<List list={list}/>
<ListItemForm list={list}/>
</div>
);
}
List.js
export default function List({list}) {
return (
<div>
<h4>{list.title}</h4>
{list.list_items.map((item) => {
return (
<div key={item.id}>
<li key={item.id}>{item.description}</li>
</div>
);
})}
<br></br>
</div>
);
}
and ListItemForm.js
export default function ListItemForm({list}) {
const [item, setItem] = useState("")
const dispatch = useDispatch()
const navigate = useNavigate()
function handleSubmit(e) {
e.preventDefault()
let newItem = {description: item, completed: false, list_id: list.id}
dispatch(createListItem(newItem, list.id))
setItem("")
navigate(`/lists/${list.id}`)
}
return (
<div>
<br></br>
<form onSubmit={handleSubmit}>
<label>Add to your list: </label>
<input value={item} onChange={(e) => setItem(e.target.value)} />
</form>
</div>
)
}
I have been stuck on this for quite some time now and not sure where to go from here or where I am going wrong. Any help is appreciated!!
Sometimes when you navigate to "/lists/:id" you send route state, sometimes you don't. It's undefined when you navigate to "/lists/:id" when adding new list items. This navigation to the route you are already on for editing a list is unnecessary.
Since you are using Redux I don't think there's any need to send a list item in route state at all. Use the id route parameter and your lists redux state to derive the specific list you want to view/edit.
Example
Given: <Route path="/lists/:id" element={<ListContainer />} />
Lists
function Lists() {
const dispatch = useDispatch();
const lists = useSelector((state) => state.lists);
useEffect(() => {
if (!lists.length) {
dispatch(fetchLists());
}
}, [dispatch, lists]);
return (
<div>
{lists.map((list) => (
<Link key={list.id} to={`/lists/${list.id}`}>
<h2>{list.title}</h2>
</Link>
))}
</div>
);
}
ListContainer
import { useParams } from 'react-router-dom';
function ListContainer() {
const { id } = useParams();
const lists = useSelector((state) => state.lists);
const list = lists.find((list) => list.id === id);
return (
<div>
<List list={list} />
<ListItemForm list={list} />
</div>
);
}
ListItemForm
function ListItemForm({ list }) {
const [item, setItem] = useState("");
const dispatch = useDispatch();
function handleSubmit(e) {
e.preventDefault();
dispatch(actions.createListItem(item, list.id));
setItem("");
}
return (
<div>
<br></br>
<form onSubmit={handleSubmit}>
<label>Add to your list: </label>
<input value={item} onChange={(e) => setItem(e.target.value)} />
</form>
</div>
);
}

How to register 'react-bootstrap-typeahead' component using React 'useForm' hook?

I'm building a simple html form using "react-hook-form" library: https://react-hook-form.com/
I've incorporated "react-bootstrap-typeahead" into the html form but haven't been able to register this component with 'useForm' hook. Hence, "react-bootstrap-typeahead" input data is ignored during onSubmit.
"react-bootstrap-typeahead" doesn't provide a "name" prop which makes it difficult to register the component.
I've read the 'useForm' documentation on the different options for registering this type of components but still don't understand how to achieve this: https://react-hook-form.com/get-started#Registerfields
Does anybody have faced such challenge before?
It would be great to see a working example to get a better idea on how to implement "react-bootstrap-typeahead" + "react-hook-form" in my application. Thanks!
Here's my sample code:
import useForm from 'react-hook-form';
import { Typeahead } from 'react-bootstrap-typeahead';
import 'react-bootstrap-typeahead/css/Typeahead.css';
const myForm = (props) => {
const { register, handleSubmit, errors } = useForm();
const onSubmit = data => {
// api post request with form data
})
};
const mydata = [ "one", "two", "three" ];
return (
<>
<form onSubmit={handleSubmit(onSubmit)} >
<div className="form-group">
{/* Here I'm registering text input using ref: */}
<input type="text" className="form-control" name="name" ref={register({ required: true })} />
</div>
<div className="form-group mb-0">
{/* How can I register the below component with useForm? */}
<Typeahead
id="multiple-typeahead"
clearButton
multiple
options={mydata}
/>
</div>
<button type="submit">Save</button>
</form>
</>
);
}
This is how i was able to register the component:
import useForm from 'react-hook-form';
import { useForm, Controller } from "react-hook-form";
import 'react-bootstrap-typeahead/css/Typeahead.css';
const myForm = (props) => {
const { register, handleSubmit, errors, control } = useForm();
const onSubmit = data => {
// api post request with form data
})
};
const mydata = [ "one", "two", "three" ];
return (
<>
<form onSubmit={handleSubmit(onSubmit)} >
<div className="form-group">
<input type="text" className="form-control" name="name" ref={register({ required: true })} />
</div>
<div className="form-group mb-0">
<Controller
as={Typeahead}
control={control}
name="typeahead_component"
rules={{ required: true }}
id="multiple-typeahead"
clearButton
multiple
options={mydata}
defaultValue=""
/>
</div>
<button type="submit">Save</button>
</form>
</>
);
}

Dynamic name for react-final-form Field

I have 2 forms. When I choose an option on 1st form, the 2nd form is added to the page with the parameters retrieved from backend. Now how can I set the parameter names as react-final-form Field names?
I could not find a way to do this. Where to pass the parameter names?
<Form
onSubmit={onSubmit}
validate={validate}
React Final Form calls your onSubmit function with the values from all the fields in your form. It's totally up to you to transmit the values to your server.
If you're asking how to build the second form, you just add the fields you need to add. So, say you got back from the server that you needed three fields: [ 'name', 'startTime', 'endTime' ]. You'd just loop through that array and add the fields.
<Form onSubmit={onSubmit}>({handleSubmit}) => (
<form onSubmit={handleSubmit}>
{fieldsFromServer.map(fieldName => (
<div key={fieldName}>
<label>{fieldName}</label>
<Field name={fieldName} component="input"/>
</div>
))}
</form>
)}<Form>
Does that help? You don't have to "pass parameters to the form", you just add the Field components that you need.
Call the FinalForm like
<FinalFieldArrayForm onSubmit={this.handleSubmitTemplate} fieldsFromServer={parameters} />
and FinalForm is
import React from "react";
import ReactDOM from "react-dom";
import { Form, Field } from 'react-final-form'
import arrayMutators from 'final-form-arrays'
import { FieldArray } from 'react-final-form-arrays'
import "./styles.css";
const FinalForm = ({onSubmit, fieldsFromServer}) => (
<Form
onSubmit={onSubmit}
mutators={{
// potentially other mutators could be merged here
...arrayMutators
}}
render={({
handleSubmit,
form: {
mutators: { push, pop }
},
pristine,
form,
submitting,
values
}) => (
<form onSubmit={handleSubmit}>
<div className="buttons">
<button type="button" onClick={() => push('records', undefined)}>+</button>
<button type="button" onClick={() => pop('records')}>-</button>
<button type="button" onClick={form.reset} disabled={submitting || pristine}>Reset</button>
</div>
<FieldArray name="records">
{ ({fields}) => (
<div>
{fields.map( (name, index) => (
<div key={`${name}.${index}`}>
<label>{index + 1}</label>
{fieldsFromServer.map( param => <Field key={`${name}.${param}`} name={`${name}.${param}`} component="input" placeholder={`${name}.${param}`} /> )}
<button type="button" onClick={() => fields.remove(index)}>-</button>
<button type="button" onClick={() => fields.insert(index+1)}>+</button>
</div>
))}
</div>
)}
</FieldArray>
<div className="buttons">
<button type="submit" disabled={submitting || pristine}>Submit</button>
</div>
<pre>{JSON.stringify(values, 0, 2)}</pre>
</form>
)}
/>
)
const rootElement = document.getElementById("root");
ReactDOM.render(<FinalForm onSubmit={() => (<div/>)} fieldsFromServer={["firstName", "lastName"]} />, rootElement);

Automatic "for" and "id"

Because we cannot assume our components are singletons: I am trying to automatically handle giving htmlFor to a label and field. We see below I use useMemo and lodash uniqueId to memoize a unique id for the form on initial render. I had to give useMemo and empty array as second argument so it never re-calculates the id. Is there some automated way to handle this in final-form?
import React, { useMemo } from 'react';
import { Form, Field } from 'react-final-form';
import { uniqueId } from 'lodash';
function TaskForm() {
const id = useMemo(() => uniqueId('_form'), []);
const getFor = name => name + id;
return (
<>
<h3>Create a task</h3>
<Form onSubmit={onSubmit}>
{({ handleSubmit, pristine, invalid, ...rest }) => {(
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor={getFor('firstName')}>First Name</label>
<Field name="firstName" id={getFor('firstName')} component="input" placeholder="First Name" />
</div>
<button type="submit" disabled={pristine || invalid}>Submit</button>
</form>
)}}
</Form>
</>
);
}

form.valid() not working from .js file in MVC

I have a .js file which is called on mvc form submit click. In that .js file function I am trying to validate the form before I do ajax post to my controller
I have also referred following script files at top of .js files as below: -
/// <reference path="~/Scripts/jquery-1.9.1.js" />
/// <reference path="~/Scripts/jquery-ui-1.10.0.js" />
/// <reference path="~/Scripts/jquery.unobtrusive-ajax.js" />
/// <reference path="~/Scripts/jquery.validate.js" />
/// <reference path="~/Scripts/jquery.validate.unobtrusive.js" />
save = function() {
var form = $("#formID");
var result1 = $("#formID").validate();
var result = $("#formID").valid();
if (result === true) {
$.ajax({
url: whatever the url,
data: form.serialize(),
type: 'POST',
...............
..........
});
}
}
My View is strongly typed and model class have all DataAnnotations.
In my scenario I have a form which loads with all data initially and hten I am trying to clear all required field data and trying to submit so that I can see the validation. When form loads I can see the html with all data- atributes such as below.
<input class="custom" data-val="true" data-val-required="First Name is required." id="txtFirstName" name="Form1[0].FirstName" placeholder="First Name" title="First Name" type="text" value="robert">
I always get 'result === true' and thats why it goes for ajax post to controller and it breaks.( i will have server side validation in future to avoid this )
Surprisingly even after I have cleared the data from "First Name" field I still see value="robert" in there....is that an issue ?
I am not sure why this is not working.
1 Firsty use "Chrome Developer Tool(CDT)" for debugging client side
2 Put a break point on the line mentioned below
3 Then in CDT put the below code, it will show you what is the field, and the validation that is failing
**$.data($('form')[0], 'validator').errorList**
[
Object
element: input#FirstName.text-box single-line input-validation-error
message: "The FirstName field is required."
__proto__: Object
Working code below
$(function () {
// Handler for .ready() called.
$('#mycustomsubmitbutton').click(function () {
var $form = $('form').first();
var result = $form.valid();
// Put you break point in the below if condition
if (result === true) {
alert("form valid");
} else {
alert("invalid form");
}
});
});
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
Employee
<div class="editor-label">
#Html.LabelFor(model => model.FirstName)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.FirstName)
#Html.ValidationMessageFor(model => model.FirstName)
</div>
<p>
<input id="mycustomsubmitbutton" type="button" value="Valid the form and make an Ajax request" />
</p>
</fieldset>
}
Quote OP:
"Surprisingly even after I have cleared the data from "First Name"
field I still see value="robert" in there....is that an issue ?"
<input class="custom" data-val="true"
data-val-required="First Name is required."
id="txtFirstName" name="Form1[0].FirstName"
placeholder="First Name"
title="First Name"
type="text"
value="robert">
value="robert" is your problem. Because of this attribute the field is not empty.
See: http://jsfiddle.net/M7skq/ and http://jsfiddle.net/JszxA/

Resources