I am trying to create a toggable button group using React, Redux Form and Bootstrap (reactstrap).
What i have done is already correctly updating the redux form data.
The problem is with the button color attribute wich should be toggled between "success" and "secondary". Right now it does set the color on the first toggle, but does not update when i click another button afterwards.
This is my render component:
import React from 'react';
import classNames from 'classnames';
import { Label, FormGroup, ButtonGroup, Button } from 'reactstrap';
import FontAwesome from 'react-fontawesome';
export default class buttonOptions extends React.PureComponent {
static propTypes = {
input: React.PropTypes.object,
buttons: React.PropTypes.any,
label: React.PropTypes.string,
meta: React.PropTypes.shape({
touched: React.PropTypes.bool,
error: React.PropTypes.any,
})
};
constructor(props) {
super(props);
this.toggleOption = this.toggleOption.bind(this);
}
toggleOption(val) {
if (!this.props.input.value.length) this.props.input.value = [];
// option per buttongroud is always limited to 1
// remove previously selected options
for (let b of this.props.buttons) {
if (b.value !== val && this.props.input.value.indexOf(b.value) > -1) {
this.props.input.value.splice(this.props.input.value.indexOf(b.value), 1)
}
}
// push the new option and update state
this.props.input.value.push(val);
this.props.input.onChange(this.props.input.value)
}
render() {
const { input, buttons, label, meta: { touched, error }} = this.props;
const labelStyles = {width: '100%', marginBottom: '0'};
return (
<FormGroup>
<Label style={labelStyles}>{label}</Label>
<ButtonGroup>
{
buttons.map((b) => {
return (
<Button
key={b.title}
color={classNames({
success: input.value.indexOf(b.value) > -1,
secondary: input.value.indexOf(b.value) === -1,
})}
role="button"
onClick={() => { this.toggleOption(b.value) }}
>
{b.title}
</Button>
)
})
}
</ButtonGroup>
</FormGroup>
);
}
}
And this is how its implemented:
import React from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './AdWizard.css';
import cx from 'classnames';
import FontAwesome from 'react-fontawesome';
import { Field, reduxForm } from 'redux-form'
import { Row, Col, FormGroup, Label, Button } from 'reactstrap';
import buttonOptions from '../FormComponents/buttonOptions';
class Step2 extends React.Component {
constructor(props) {
super(props);
this.workingtimes = [
{
title: "Vollzeit",
value: "Vollzeit",
selected: true
},
{
title: "Teilzeit",
value: "Teilzeit",
selected: false
}
]
}
render() {
const { handleSubmit, previousPage } = this.props;
return (
<form onSubmit={handleSubmit}>
<Row className="justify-content-center">
<Col xs="12" sm="6">
<Field
label="Arbeitszeit"
name="arbeitszeit"
buttons={this.workingtimes}
component={buttonOptions}
/>
</Col>
</Row>
</form>
)
}
}
Step2 = reduxForm({
form: 'posting',
destroyOnUnmount: false,
forceUnregisterOnUnmount: true
})(Step2);
export default withStyles(s)(Step2);
Would be great if someone could help out!
Cheers
Stefan
The problem you have is that your toggleOption function is impure*.
This means it is mutating this.props.input.value, instead of creating a new array whose value is based on it - putting it simple, always create a new reference!
Since most React code out there is very sensitive to pure function calls,
you must convert that function to a pure one, so that redux-form actually sees you changed the array:
toggleOption (b) {
let newValue;
const currValue = this.props.input.value || [];
if (currValue.includes(b.value)) {
// value already exists in array, let's remove it from there
newValue = currValue.filter(val => val !== b.value);
} else {
// value doesn't exist in array, let's add it there
newValue = currValue.concat([b.value]);
}
this.props.input.onChange(newValue);
}
Array methods such as .filter() and .concat() are your frinds here: they return new array instances, instead of mutating the existing array.
Your code was using .push() and .splice(), methods that are bad, because they mutate the existing array.
You can see a small demo here.
* You can read more about this subject here.
Related
I've created a form in react and after some research i think that if you don't want to use an external library to manage the form, the context could be the best choice, expecially in my case where i've many nested component that compose it.
But, i'm not sure that putting a function inside my state is a good thing.
But let me give you some code:
configuration-context.js
import React from 'react'
export const ConfigurationContext = React.createContext();
ConfigurationPanel.jsx
import React, { Component } from 'react'
import { Header, Menu, Grid } from 'semantic-ui-react'
import ConfigurationSection from './ConfigurationSection.jsx'
import {ConfigurationContext} from './configuration-context.js'
class ConfigurationPanel extends Component {
constructor(props) {
super(props)
this.state = {
activeItem: '',
configuration: {
/* the configuration values */
banana: (data) => /* set the configuration values with the passed data */
}
}
}
handleItemClick = (e, { name }) => this.setState({ activeItem: name })
render() {
return (
<ConfigurationContext.Provider value={this.state.configuration}>
<Grid.Row centered style={{marginTop:'10vh'}}>
<Grid.Column width={15} >
<div className='configuration-panel'>
/* SOME BUGGED CODE */
<div className='configuration-section-group'>
{this.props.data.map((section, i) => <ConfigurationSection key={i} {...section} />)}
</div>
</div>
</Grid.Column>
</Grid.Row>
</ConfigurationContext.Provider>
)
}
}
ConfigurationItem.jsx
import React, { Component } from 'react'
import { Input, Dropdown, Radio } from 'semantic-ui-react'
import {ConfigurationContext} from './configuration-context.js'
class ConfigurationItem extends Component {
static contextType = ConfigurationContext
constructor(props) {
super(props)
}
handleChange = (e, data) => this.context.banana(data)
itemFromType = (item) =>{
switch (item.type) {
case "toggle":
return <div className='device-configuration-toggle-container'>
<label>{item.label}</label>
<Radio name={item.name} toggle className='device-configuration-toggle'onChange={this.handleChange} />
</div>
/* MORE BUGGED CODE BUT NOT INTERESTING*/
}
}
render() {
return this.itemFromType(this.props.item)
}
}
So, at the end i've a ConfigurationContext that is just a declaration, everything is inside the parent state.
The thing that i don't like is putting the banana function inside the state (it will have more logic that just logging it)
What do you think about it?
Any suggestion is appreciated.
Thanks
banana is just a regular function and you do not have to put it in the state, just do:
class ConfigurationPanel extends Component {
banana = data => console.log(data)
...
render() {
return (
<ConfigurationContext.Provider value={{banana}}>
...
}
After that you can use this.context.banana(data) as normal.
I would like to create a custom component in React using TypeScript that's essentially a combobox which has auto-complete/search functionality, connecting to its own remote store. What I would like to do is send an "onSelect" event so that I can receive the selected item where ever I'm using that component in my app.
Doing the auto-complete/search stuff with the remote store is easy, but the React component stuff has me stumped. I'm still learning both, so perhaps I'm trying to walk before I can crawl, but I don't want to start out creating a mess when I know that it should be possible to achieve this outcome which would be more elegant. I just need to find some sort of guide, but so far I haven't found one.
Here's what I want to achieve:
<MyCombobox onSelect={handleSelect} />
The handleSelect function would be used throughout my app where ever I need to use the MyCombobox component. The function needs to accept an argument, of course (which is what has me stumped at the moment, in TS).
One possible solution is as following
import * as React from "react";
import { render } from "react-dom";
interface MyComboProps {
// Here props from parent should be defined
}
interface MyComboState {
ValuesToShow: string[];
SearchValue: string;
}
class StringSearchMenu extends React.Component<MyComboProps, MyComboState> {
constructor(p: MyComboProps) {
super(p);
this.state = {
ValuesToShow: [],
SearchValue: ""
};
}
protected selectString(event: React.ChangeEvent<HTMLInputElement>): void {
let value = event.target.value;
if (value === "") this.setState({ ValuesToShow: [] });
else {
/* here you can put fetch logic. I use static array as example */
let possibleValues = ["Green", "Red", "Blue", "Yellow", "Black"];
this.setState({
ValuesToShow: possibleValues.filter(f => f.indexOf(value) > -1)
});
}
}
render() {
return (
<div>
Enter value to search {" "}
<input onChange={this.selectString.bind(this)} />
<div>
{this.state.ValuesToShow.map(v => (
<div>{v}</div>
))}
</div>
</div>
);
}
}
And working example is here
From all the googling I've done this morning, I've managed to cobble together something that works (from a plethora of sources):
App.tsx:
import React from 'react';
import './App.css';
import MyCombobox from './MyCombobox';
class App extends React.Component {
receiveSomething(something: string) {
alert('Something: ' + something);
}
render() {
return (
<div>
<MyCombobox receiveSomething={this.receiveSomething} defaultValue="qwerty" />
</div>
);
}
}
export default App;
MyCombobox.tsx:
import React from 'react';
export interface IMyCombobox {
defaultValue: string,
receiveSomething:(name:string) => void
}
class MyCombobox extends React.PureComponent<IMyCombobox, any> {
state = {
something: this.props.defaultValue
}
sendSomething() {
this.props.receiveSomething(this.state.something);
}
handleChange = (event: any) : void => {
this.setState({
something: event.target.value
});
}
render() {
return (
<div>
<input
type='text'
maxLength={20}
value={this.state.something}
onChange={this.handleChange} />
<input
type='button'
value='Send Something'
onClick={this.sendSomething.bind(this)} />
</div>
)
}
}
export default MyCombobox;
My custom component should pass data to parent which is react-admin <Create>
I have came across some questions already and found that I can't simply set state from child to parent.
The problem is that this component should work like the default react-admin components (ex. ). It means when I submit the form It gets data from that component.
I have already tried addField()
This is my custom component (child):
import React from "react";
import MenuItem from "#material-ui/core/MenuItem";
import Select from "#material-ui/core/Select";
import { fetchUtils } from 'react-admin';
import FormControl from '#material-ui/core/FormControl';
import InputLabel from '#material-ui/core/InputLabel';
import { DataService } from '../routes/api';
import PropTypes from 'prop-types';
import { resources as rsrc } from '../resources';
const divStyle = {
marginTop: '16px',
marginBottom: '8px',
};
const inputStyle = {
width: '256px',
}
export default class MultipleSelect extends React.Component {
constructor(props) {
super(props);
this.state = {
selectOptions: [],
selectedValues: [],
selectedValue: null,
};
}
getRoles() {
// get data from api
}
getAllOptions() {
// get some additional data from API
}
createRelationRecord(id) {
// create relation record (for ex. User's Role)
}
deleteRelationRecord(id) {
// delete relation record
}
componentDidMount() {
this.getRoles();
this.getAllOptions();
}
renderSelectOptions = () => {
return this.state.selectOptions.map((dt, i) => (
<MenuItem key={dt.id} value={dt.id}>
{dt.value}
</MenuItem>
));
};
handleChange = event => {
this.setState({ selectedValue: event.target.value });
// If record doesn't exist
if (this.state.selectedValue != event.nativeEvent.target.dataset.value) {
this.createRelationRecord(event.nativeEvent.target.dataset.value);
}
if (this.state.selectedValues.includes(Number(event.nativeEvent.target.dataset.value))) {
this.deleteRelationRecord(event.nativeEvent.target.dataset.value);
} else {
this.createRelationRecord(event.nativeEvent.target.dataset.value);
}
};
selectboxType() {
if (this.props.multiple) {
return true;
}
return false;
}
getSelected() {
if (this.selectboxType()) {
return this.state.selectedValues;
}
return this.state.selectedValue;
}
render() {
return (
<div style={divStyle}>
<FormControl>
<InputLabel htmlFor={this.props.label}>{this.props.label}</InputLabel>
<Select
multiple={this.selectboxType()}
style={inputStyle}
value={this.getSelected()}
onChange={this.handleChange}
>
{this.renderSelectOptions()}
</Select>
</FormControl>
</div>
);
}
}
Parent (create form):
export const ServerCreate = props => (
<Create {...props}>
<SimpleForm>
<TextInput source="Name" validate={required()} />
<ReferrenceSelectBox label="ServerType" multiple={false} source="ServerTypeId" reference="ServerType"></ReferrenceSelectBox>
<TextInput source="Barcode" validate={required()} />
</SimpleForm>
</Create>
);
It works with handleChange to achieve the data update. Now I need to save the selected data in Create form, but handleChange will not help me, because the object is not created yet and I cannot set attribute of non-existent record.
So my question is how can I pass value/values from my component to Create? How to set parent's state?
Your ServerCreate doesn't have any state, but if it did, you need to pass a function which can update its state to the child component as a prop.
Generally speaking, you should probably lift the state higher up the component tree
Objective
Setup a dynamic form controlled by the user using react-redux and revalidate to run validation checks on my form.
Problem:
Since my form is dynamic I need to make dynamic validations. In order to do this my form data needs to be passed in as props to my component, which can be used as a second argument in the validate function from revalidate
My approach
To do this I am waiting until the component is mounted, building the form, passing it to redux, and then will map state to props. As the user adds more rows I will update state and then the component will render. I will use shouldComponentUpdate() to avoid any render loops.
The Error
My error is regarding the dispatch. When I try to run the dispatch (which will pass the form into redux) I get Dispatch is not a function error.
I am not supper comfortable with the connect() as I have to wrap redux with it as well as firebase. This syntax really confuses me.
Question
I believe the issue with with how I am exporting the component where I am using HOC like withFirebase, Redux, and Connect. Somewhere along the way I am losing scope to the connect. Can someone shed light into what it is I am doing wrong?
Component
import React, { Component } from "react";
import { reduxForm, Field } from "redux-form";
import { Container, Form, Col, Button } from "react-bootstrap";
import MaterialIcon from '../../material-icon/materialIcon';
import { withFirestore } from "react-redux-firebase";
import { connect } from "react-redux";
import TextInput from "../../forms/textInput";
import { combineValidators, isRequired } from "revalidate";
import { setupStudentForm } from '../../../store/actions/students';
const validate = (values, ownprops) => {
// Will be passing in validation rules, once form is apssed in with props via mapStateToProps.
}
export class CreateStudentsForm extends Component {
// Using constrcutor so componentDidMount() can render() cann access variables
constructor(props) {
super(props);
this.state = {
rows: 2,
}
this.formArray = [];
this.form = null;
}
componentDidMount() {
// Once component is rendered, setup form and send to redux
for (let i = 1; i !== this.state.rows + 1; i++) {
let firstNameField = {
fieldName: `firstName${i}`,
label: 'First Name',
required: true,
type: "text",
}
let lastNameField = {
fieldName: `lastName${i}`,
label: 'Last Name',
required: true,
type: "text",
}
this.formArray.push([firstNameField, lastNameField]);
}
this.props.setupStudentFormHandler(this.formArray);
}
// Ensure we do not get stuck in render loop
shouldComponentUpdate(nextProps, nextState){
if(nextProps !== this.props){
return true
} else {
return false
}
}
render() {
// Allows user to add another row
const addRow = () => {
this.setState({
rows: this.state.rows + 1
})
}
// Map through form array and create template
if (this.formArray) {
let form = this.formArray.map((field, index) => {
return (
<Form.Row key={index} className="animated fadeIn">
<Col xs={5}>
<Form.Group className="mb-0 noValidate">
<Field
label={field[0].label}
attempt={this.props.attempt}
name={field[0].fieldName}
type={field[0].type}
component={TextInput}
/>
</Form.Group>
</Col>
<Col xs={5}>
<Form.Group className="mb-0 noValidate">
<Field
label={field[1].label}
attempt={this.props.attempt}
name={field[1].fieldName}
type={field[1].type}
component={TextInput}
/>
</Form.Group>
</Col>
<Col xs={2}>
<MaterialIcon icon="delete" className="mt-4" />
</Col>
</Form.Row>
)
})
}
return (
<Container>
{this.form}
<Button variant="outline-success" onClick={addRow}>Add Another Student</Button>
</Container>
)
}
}
const mapStateToProps = state => {
return {
// Get access to student state which will have form
studentForm: state.students
};
};
const mapDispatchToProps = dispatch => {
return {
//Send formArray to redux
setupStudentFormHandler: (form) => dispatch(setupStudentForm(form))
};
};
export default withFirestore(
reduxForm({
form: "createStudents", validate
})(connect(
mapDispatchToProps,
mapStateToProps
)(CreateStudentsForm)
)
);
mapStateToProps is the first argument of connect, mapDispatchToProps is the second. Try swapping the order:
connect(
mapStateToProps,
mapDispatchToProps
)(CreateStudentsForm)
I am trying to load my previous values in Redux From Fields on screen and hoping to get new values from each fields as a user updates the values. I have tried couple different ways, like onChange or initialValues but so far I still have no luck to get it working:
Here is my code so far:
`import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { reduxForm, Field } from 'redux-form';
import { Link } from 'react-router-dom';
import SurveyField from './SurveyField';
import { fetchOneSurvey } from '../../actions';
import validateEmails from '../../utils/validateEmails';
import formFields from './formFields';
class SurveyEditForm extends Component {
componentDidMount() {
const id = window.location.pathname.split('/')[2];
this.props.fetchOneSurvey(id);
}
renderFields() {
return _.map(formFields, ({ label, name }) => {
return (
<Field
key={name}
component={SurveyField}
type="text"
label={label}
name={name}
/>
);
});
}
render() {
return (
<div>
{/*.handleSubmit is from reduxForm method*/}
<form
onSubmit={this.props.handleSubmit(
this.props.onSurveySubmit
)}
>
{this.renderFields()}
<Link to="/surveys" className="red btn-flat white-text">
Cancel
</Link>
<button
type="submit"
className="teal btn-flat right white-text"
>
Next
<i className="material-icons right">done</i>
</button>
</form>
</div>
);
}
}
function validate(values) {
const errors = {};
errors.recipients = validateEmails(values.recipients || '');
_.each(formFields, ({ name }) => {
if (!values[name]) {
errors[name] = 'You must provide a value';
}
});
return errors;
}
function mapStateToProps(state) {
return {
surveyEdit: state.surveyEdit
};
}
SurveyEditForm = connect(mapStateToProps, { fetchOneSurvey })(SurveyEditForm);
SurveyEditForm = connect(state => ({
initialValues: {
title: this.props.surveyEdit.title
}
}))(SurveyEditForm);
export default reduxForm({
validate,
form: 'surveyEditForm',
destroyOnUnmount: false,
enableReinitialize: true
})(SurveyEditForm);`
This currently gives me error:
TypeError: Cannot read property 'surveyEdit' of undefined
this.props.surveyEdit
this is the object I fetched from the database and I was hoping to use it to provide initial values to the field. I also couldn't see previous value on screen while I was trying.
Anyone have better idea? I am pretty sure there must be a way to do it, I just can't seem to comprehend how to implement it.
Thank you in advance as always!