I'm doing a form helper to react-native, I also want to implement a feature called namespace to the forms to get a modular result.
An example:
return(
<Form
ref="configForm"
>
<Form.TextInput
name="firstName"
/>
<Form.TextInput
name="lastName"
/>
<Form.Namespace name="address">
<Form.TextInput
name="city"
/>
<Form.TextInput
name="address"
/>
<Form.TextInput
name="zip"
/>
</Form.Namespace>
</Form>
);
In this case the final result must be:
{
firstName: 'John',
lastName: 'Tompson',
address: {
city: 'New York',
address: '3th Av 231',
zip: '32132'
}
}
To get fields values I map recursivery the children of FormNamespace, and I clone the elements that own the property isFormValue (this property is owned by elements that provide values to Form, wich are FormNamespace and FormField):
wrapChildren(child) {
if (!child) {
return child;
}
if (Array.isArray(child)) {
return child.map((c) => this.wrapChildren(c));
}
if (child.type.isFormValue) {
let ref = child.ref || child.props.name;
child = React.cloneElement(child, {
ref: ref,
style: {borderWidth: 1, borderColor: 'red'}, // for debug
onUpdateValue: this.handleFieldChange.bind(this, ref),
});
}
if (child.props.children && !child.type.isFormStopableWrapper) {
child.props.children = this.wrapChildren(child.props.children);
}
return child;
}
render() {
let childs = this.wrapChildren(this.props.children);
debugger;
return(
<View style={this.props.style}>
{childs}
</View>
);
};
All logic is in FormNamespace component, in fact, the Form component only renders a FormNamespace:
render() {
return(
<Namespace
ref="formData"
name="mainNamespace"
style={[styles.form, this.props.style]}
onUpdateValue={(data) => this._handleUpdateData(data)}
>
{this.props.children}
</Namespace>
);
};
The Form component works correctly on the first depth, but when must to clone the FormNamespace element, by some issue it does not work as it should.
Debugging, I checked the React.cloneElement is applied to the FormNamespace but in the final render the FormNamespace never is referenced (nor the border style is applied), unlike the FormField that works well.
I don't know what is wrong here, the final FormNamespace rendered has not define the onUpdateValue prop. The objetive is the FormNamespace be taken like a simple value provider to Form (like fields components) and this manage theirs own childs.
The complete component code are here:
FormNamespace: https://paste.kde.org/pyz5gdsgb
Form: https://paste.kde.org/p4kmc2k9j
Related
Formik's documentation states you can use a lodash type dot path to name/access nested objects (e.g. name.firstName). This is also supposed to apply to it's built in <ErrorMessage/> component. I was working with a React-Typescript tutorial app that uses formik for form inputs and it seemed to work fine under the hood when paired with backend code, but I did notice that the fields that fed nested object values would not throw any errors in the UI. The error itself would be generated, but the <ErrorMessage/> component didn't seem to want to render.
A pared down version of the app is below. The "Field is Required" error should be thrown if you exit a form field without a valid input but again it doesn't work for the nested object fields (first/last name). I was wondering if anyone else has run across this issue. It's a little annoying.
I've seen that formik seems to be paired frequently with Yup for validation, which may make this issue moot, but I haven't gotten quite that far yet. Thanks!
import React from 'react';
import ReactDOM from 'react-dom';
import { ErrorMessage, Field, FieldProps, Formik } from "formik";
import { Form, Button } from "semantic-ui-react";
interface TextProps extends FieldProps {
label: string;
placeholder: string;
}
const TextField: React.FC<TextProps> = ({
field, label, placeholder
}) => (
<Form.Field>
<label>{label}</label>
<Field placeholder={placeholder} {...field} />
<div style={{ color: "red" }}>
<ErrorMessage name={field.name} />
</div>
</Form.Field>
);
const App: React.FC = () => {
return (
<Formik
initialValues={{
name: {
firstName: "",
lastName: ""
},
job: "",
}}
onSubmit={()=>console.log("submitted")}
validate={values => {
const requiredError = "Field is required";
const errors: { [field: string]: string } = {};
if (!values.name.firstName) {
errors["name.firstName"] = requiredError;
}
if (!values.name.lastName) {
errors["name.lastName"] = requiredError;
}
if (!values.job) {
errors["job"] = requiredError;
}
return errors;
}}
>
{({ isValid, dirty}) => {
return (
<Form>
<Field label="First Name" name="name.firstName" component={TextField} />
<Field label="Last Name" name="name.lastName" component={TextField} />
<Field label="Job" name="job" component={TextField} />
<Button type="submit" disabled={!dirty || !isValid}> Add</Button>
</Form>
);
}}
</Formik>
);
};
Have you tried using formik's inbuilt function getIn()? It is used to extract values from deeply nested objects. Check this Q&A
Try changing validate function to this
validate={values => {
const requiredError = "Field is required";
const errors: any = {name: {}};
if (!values.name.firstName) {
errors.name.firstName = requiredError;
}
if (!values.name.lastName) {
errors.name.lastName = requiredError;
}
if (!values.job) {
errors["job"] = requiredError;
}
return errors;
}}
How should I be getting values from a FormPanel using ext-react 6.6.0?
According to the API documentation I should be using getValues function, that works in 6.5.1 but I get error _this.form.getValues is not a function in 6.6.0
Code
Works in 6.5.1: https://fiddle.sencha.com/?extreact#view/editor&fiddle/2n05
Fails in 6.6.0 (see console for error): https://fiddle.sencha.com/?extreact#view/editor&fiddle/2n04
I get error _this.form.getValues is not a function in 6.6.0
The reason ref={form => this.form = form}. In extreact-6.6.0 the form variable is not exact formpanel. So for this you need to access like this
ref={form => this.form = (this.form || form.cmp)}}
Another way you use button.up('formpanel') to get the formpanel component. This button is first parameter of your handler.
button.up('formpanel').getValues()
You can check here with working fiddle.
Code Snippet
import React, { Component } from 'react';
import {launch} from '#sencha/ext-react';
import { ExtReact } from '#sencha/ext-react';
import { Container, Label, FormPanel, TextField, Button } from '#sencha/ext-modern';
class App extends Component {
state = {
values:JSON.stringify({
fname: 'null',
lname: 'null'
})
}
submit = (btn) => {
const values = btn.up('formpanel').getValues();
console.log('Values using up selector',values);
console.log('Values using up ref',this.form.getValues());
this.setState({values:JSON.stringify(this.form.getValues())});
}
render() {
return (
<Container defaults={{ margin: 10, shadow: true }}>
<FormPanel title="Form" ref={form => this.form = (this.form || form.cmp)}>
<TextField name="fname" label="First Name"/>
<TextField name="lname" label="Last Name"/>
<Button handler={this.submit} text="Submit"/>
</FormPanel>
<Label padding={'10'} html={this.state.values} />
</Container>
)
}
}
launch(<ExtReact><App /></ExtReact>);
I am using a 'reusable component library' provided by my employer to render a checkbox. The code for the checkbox component is:
const CheckboxInput = (props) => {
const { classNames, label, ...attrs } = props;
return (
<label className="foo">
<Input
{ ...attrs }
classNames={classnames(
classNames,
'bar')}
type="checkbox"
/>
<span className="foo"></span>
{label &&
<span className="foo">
{label}
</span>
}
</label>
);
};
CheckboxInput.propTypes = {
label: PropTypes.string,
classNames: PropTypes.string
};
I would like for part of the label to be grey and the rest to have the default styling. I was able to get this to work with either of the following implementations:
<CheckboxInput
label={<span>My unstyled string <span style={{ color: 'grey' }}>{'My String that I want to be grey'}</span></span>}
checked={isChecked}
onChange={ () => handleChange(e.target.value) }
/>
or
<CheckboxInput
label={['My unstyled string', <span style={{ color: 'grey' }}>{'My String that I want to be grey'}</span>]}
checked={isChecked}
onChange={ () => handleChange(e.target.value) }
/>
unfortunately the first implementation raises a console error of: Invalid prop `label` of type `object` supplied to `CheckboxInput`, expected `string`.
the second implementation gives me a console error of: Invalid prop `label` of type `array` supplied to `CheckboxInput`, expected `string`. . In addition to this error: Each child in an array or iterator should have a unique "key" prop.
Is there some way to get around this error without destructively editing the CheckboxInput component?
It would work if you could you declare a node which is anything that is valid to use in render:
// Anything that can be rendered: numbers, strings, elements or an
array // (or fragment) containing these types:
optionalNode: PropTypes.node,
CheckboxInput.propTypes = {
label: PropTypes.node,
classNames: PropTypes.string
};
Problem
I have a list of people. I want to:
Select a user to edit by clicking on their name.
Edit that user's information, so I can click the submit button and update the list.
If I click on a different name, I want to switch to that person's information without having to deliberately close the form first.
Everything works until #3. When I click on another person, the form, itself, does NOT update.
My Code
Update Component for the update form:
const UpdateForm = ({ updatePerson, personToUpdate, handleInputChange }) => {
let _name, _city, _age, _id;
const submit = (e) => {
e.preventDefault();
updatePerson({
name: _name.value,
city: _city.value,
age: _age.value,
_id: _id.value
});
};
return (
<div>
<form onSubmit={submit}>
<h3>Update Person</h3>
<label htmlFor="_id">Some Unique ID: </label>
<input type="text" name="_id" ref={input => _id = input} id="_id" defaultValue={personToUpdate._id} onChange={input => handleInputChange(personToUpdate)} required />
<br />
<label htmlFor="name">Name: </label>
<input type="text" name="name" ref={input => _name = input} id="name" defaultValue={personToUpdate.name} onChange={input => handleInputChange(personToUpdate)} />
<br />
<label htmlFor="city">City: </label>
<input type="text" name="city" ref={input => _city = input} id="city" defaultValue={personToUpdate.city} onChange={input => handleInputChange(personToUpdate)} />
<br />
<label htmlFor="age">Age: </label>
<input type="text" name="age" ref={input => _age = input} id="age" defaultValue={personToUpdate.age} onChange={input => handleInputChange(personToUpdate)} />
<br />
<input type="submit" value="Submit" />
</form>
</div>
);
};
export default UpdateForm;
Relevant parts of Person Component:
class Person extends Component {
nameClick() {
if (this.props.person._id !== this.props.personToUpdate._id) {
this.props.setForUpdate(this.props.person);
this.forceUpdate();
}
else {
this.props.toggleUpdatePersonPanel();
}
}
render() {
return (
<div>
<span onClick={this.nameClick}>
{this.props.person.name} ({this.props.person.age})
</span>
</div>
);
}
}
export default Person;
Relevant parts of PeopleList, which holds Persons:
class PeopleList extends Component {
render() {
return(
<div>
{this.props.people.map((person) => {
return <Person
key={person._id}
person={person}
updatePersonPanel={this.props.updatePersonPanel}
setForUpdate={this.props.setForUpdate}
personToUpdate={this.props.personToUpdate}
/>;
})}
</div>
);
}
} // end class
export default PeopleList;
Form Reducer, with just the relevant actions:
export default function formReducer(state = initialState.form, action) {
let filteredPeople;
switch (action.type) {
case TOGGLE_UPDATE_PANEL:
return Object.assign({}, state, { updatePersonPanel: false }, { personToUpdate: {
_id: "",
name: "",
city: "",
age: ""
}});
case SET_FOR_UPDATE:
return Object.assign({}, state, { personToUpdate: action.person }, { updatePersonPanel: true });
case UPDATE_RECORD:
filteredPeople = state.people.filter((person) => {
return person._id === action.person._id ? false : true;
}); // end filter
return Object.assign({}, state, { people: [ ...filteredPeople, action.person] }, { personToUpdate: {
_id: "",
name: "",
city: "",
age: ""
}}, { updatePersonPanel: false });
case HANDLE_INPUT_CHANGE:
return Object.assign({}, state, { personToUpdate: action.person });
default:
return state;
}
}
The relevant parts of my Initial State file:
form: {
people: [
{
_id: "adfpnu64",
name: "Johnny",
city: "Bobville",
age: 22
},
{
_id: "adf2pnu6",
name: "Renee",
city: "Juro",
age: 21
},
{
_id: "ad3fpnu",
name: "Lipstasch",
city: "Bobville",
age: 45
}
],
updatePersonPanel: false,
personToUpdate: {
_id: "",
name: "",
city: "",
age: ""
},
}
Attempts at a Solution( so far)
I have attempted to make the component a completely controlled component, by switching the form attribute to value instead of defaultValue. When I do this, the names switch just fine, but the form becomes unchangeable and useless.
My Questions
Almost all of the solutions to these kind of issues either recommend using redux-form or supply two-way binding solutions that work fine in React without reduce. I want to know how to do this with Redux without using redux-form or anything extra if possible. Is there a way to resolve this without touching lifecycle methods?
Conclusion (For now)
Well, for now, I settled for making my form uncontrolled and used some classic Js DOM methods and a lifecycle method to control the form. For whatever reason, once I employed some of the answer suggestions my browser ate up my CPU and crashed, presumably because there was some kind of infinite loop. If anyone has some further recommendations, I'd really appreciate it. For now I settle for this:
class UpdateForm extends Component {
constructor(props){
super(props);
this.submit = this.submit.bind(this);
}
componentWillReceiveProps(nextProps) {
if (nextProps.personToUpdate._id !== this.props.personToUpdate._id) {
document.getElementById("name").value = nextProps.personToUpdate.name;
document.getElementById("age").value = nextProps.personToUpdate.age;
document.getElementById("city").value = nextProps.personToUpdate.city;
}
}
submit(e) {
e.preventDefault();
this.props.updatePerson({
name: document.getElementById("name").value,
city: document.getElementById("city").value,
age: document.getElementById("age").value,
_id: this.props.personToUpdate._id
});
}
render() {
return (
<div>
<form onSubmit={this.submit}>
<h3>Update Person</h3>
Unique ID: {this.props.personToUpdate._id}
<br />
<label htmlFor="name">Name: </label>
<input type="text" name="name" id="name" ref="name" defaultValue={this.props.personToUpdate.name} required />
<br />
<label htmlFor="city">City: </label>
<input type="text" name="city" id="city" ref="city" defaultValue={this.props.personToUpdate.city} required />
<br />
<label htmlFor="age">Age: </label>
<input type="text" name="age" id="age" ref="age" defaultValue={this.props.personToUpdate.age} required />
<br />
<input type="submit" value="Update" />
</form>
</div>
);
}
} // end class
export default UpdateForm;
I'll be soon exploring redux-form because it is evident that forms as inputs and outputs are a wonky business. For now, my little app works.
Yes there is and you are on the right path. The way is to use value instead of defaultValue but you have to read the value from a state and then use the onChange handler to modify the state.
Something like
this.state = {inputText:''}
Then in the input field
<input value={this.state.inputText} onChange={this.handleChange}/>
And the handleChange function will be
handleChange(event){
this.setState({inputText:event.target.value})
}
Remember to bind the handleChange event in the constructor so you can pass it as this.handleChange in the input field's onChange prop.
Something like this
this.handleChange = this.handleChange.bind(this)
https://facebook.github.io/react/docs/forms.html - Here are the official docs regarding it
Also if you want to do it in redux the same sort of logic applies where this will be the input field
<input value={this.props.inputText} onChange={this.props.handleChange}/>
where inputText and handleChange are redux state and action respectively passed to the component via props
For your case I guess it has to be something like where you are 'reading' values from the people array and the action bound to the onChange modifies that value in the people array in the state.
<--EDIT-->
How it can be done for the case in point. Pass the people in the redux state as a people prop to the component. Pass an action changePeople(newPeople) to the component as a prop which takes an argument newPeople and changes the people in the redux state to have the value newPeople. Since people is nested in form you'll have to do some Object.assign etc to modify the state.
Now in the component using the people props populate the checkboxes using a map function. The map function takes a second parameter index so for each checkbox have a function which sets the local state variable currentPerson to the value of the index
this.props.people.map((person,index) =>
return <Checkbox onClick={()=>this.setState(currentPerson:index)}/>
)
So everytime you click on a checkbox the currentPerson points to the corresponding index.
Now the input fields can be
<input value={this.props.people[this.state.currentPerson].name} onChange={this.handleChange.bind(this,'name')}/>
This is for the 'name' input field. It reads from the currentPerson index of the people array which has been passed down as a prop.
This is how the handleChange will be like
handleChange(property,event){
const newPeople = [
...this.props.people.slice(0, this.state.currentPerson),
Object.assign({}, this.props.people[this.state.currentPerson], {
[property]: event.target.value
}),
...this.props.people.slice(this.state.currentPerson + 1)
]
this.props.changePeople(newPeople)
}
The handleChange takes a property (so you don't have to write separate handlers for each input field). The newPeople basically modifies the element at current index this.state.currentPerson in the people passed from props (ES6 syntax being used here. If you have doubts do ask). Then it is dispatched using the changePeople action which was also passed as props.
I'm building a component which displays a series of generic input fields. The backing store uses a simple array of key-value pairs to manage the data:
[
{fieldkey: 'Signs and Symptoms', value:'fever, rash'},
{fieldkey: 'NotFeelingWell', value:'false'},
{fieldkey: 'ReAdmission', value:'true'},
{fieldkey: 'DateOfEvent', value:'12/31/1999'}
]
In order to eliminate a lot of boilerplate code related to data binding, the component uses these same keys when generating the HTML markup (see 'data-fieldname' attribute).
var Fields = React.createClass({
handleOnChange:function(e){
Actions.updateField( {key:e.target.attributes['data-fieldname'].value, value:e.target.value})
},
setValue:function(){
var ref = //get a reference to the DOM element that triggered this call
ref.value = this.props.form.fields[ref.attributes['data-fieldname']]
},
render:function(){
return (<div className="row">
<Input data-fieldname="Signs and Symptoms" type="text" label='Notes' defaultValue="Enter text" onChange={this.handleOnChange} value={this.setValue()} />
<Input data-fieldname="NotFeelingWell" type="checkbox" label="Not Feeling Well" onChange={this.handleOnChange} value={this.setValue()} />
<Input data-fieldname="ReAdmission" type="checkbox" label="Not Feeling Great" onChange={this.handleOnChange} value={this.setValue()} />
<Input data-fieldname="DateOfEvent" type="text" label="Date Of Event" onChange={this.handleOnChange} value={this.setValue()} />
</div>)
}
})
My goal is to use the same two functions for writing/reading from the store for all inputs and without code duplication (i.e. I don't want to add a refs declaration to each input that duplicates the key already stored in 'data-fieldname') Things work swimmingly on the callback attached to the 'onChange' event. However, I'm unsure how to get a reference to the DOM node in question in the setValue function.
Thanks in advance
I'm not sure if I understand your question right, but to reduce boilerplate I would map your array to generate input fields:
render:function(){
var inputs = [];
this.props.form.fields.map(function(elem){
inputs.push(<Input data-fieldname={elem.fieldkey} type="text" label="Date Of Event" onChange={this.handleOnChange} value={elem.value} />);
});
return (<div className="row">
{inputs}
</div>)
}
This will always display your data in props. So when handleOnChange gets triggered the component will rerender with the new value. In my opinion this way is better than accessing a DOM node directly.
If you want to use dynamic information on the input, you need to pass it through the array, and make a loop.
Here is a little example based on Dustin code:
var fieldArray = [ //replace by this.props.form.fields
{
fieldkey: 'Signs and Symptoms',
value: 'fever, rash',
type: 'text',
label: 'Notes'
},
{
fieldkey: 'NotFeelingWell',
value: 'false',
type: 'checkbox',
label: 'Not Feeling Well'
},
];
var Fields = React.createClass({
handleOnChange:function(e){
var fieldKey = e.target.attributes['data-fieldname'].value;
Actions.updateField({
key: fieldKey,
value: e.target.value
})
},
render() {
var inputs = [];
fieldArray.map(function(field) { //replace by this.props.form.fields
inputs.push(
<Input
data-fieldname={field.fieldkey}
value={field.value}
type={field.type}
label={field.label}
onChange={this.handleOnChange} />
);
}.bind(this));
return (
<div className="row">
{inputs}
</div>
);
}
});