Trying to get Calendar to work with a Redux Form
Using a Redux Form Field:
<Field name={name} component={this.renderCal}/>
which is leveraging a stateless function:
renderCal({input, ...rest}) {
input.value = new Date();
return <Calendar {...input}
onChange={() => input.onChange(input.value)}
value={input.value}
{...rest}/>
}
When I submit the form, the value is still null. I appears like the value is not bound to the component. This is my request payload from the Chrome Developer Tools > Network ...
inputs : [{name: "fromDate", title: "From Date", dataType: "date", format: "mm/dd/yyyy", value: null}] 0 : {name: "fromDate", title: "From Date", dataType: "date", format: "mm/dd/yyyy", value: null} dataType : "date" format : "mm/dd/yyyy" name : "fromDate" title : "From Date" value : null
Does anyone have Calendar working with Redux Form as a stateless function?
Thanks,
Steve
Steve and I found a solution. Posting back our findings for everyone else...
To clarify, the initial state/shape of the form is actually an array of dates:
{
myDates: [
{myDate: null},
{myDate: null},
{myDate: null}
]
}
If you just need to tie DateTimePicker up to a Redux Form field then you can drop renderDynamicFields and use renderDynamicField directly:
<Field name={name} component={renderDynamicField}/>
Points of Note:
Changed directions slightly and are now using DateTimePicker as opposed to Calendar because we also needed the time portion. That being said the solution should work with Calendar too.
Used Redux Form FieldArray
DateTimePicker needed a "defaultValue"
Solution
import React from "react";
import {Button, Col, Form, FormGroup, ControlLabel} from "react-bootstrap";
import {reduxForm, Field, FieldArray} from "redux-form";
import {connect} from "react-redux";
import {DateTimePicker} from "react-widgets";
class ReactWidgets extends React.Component {
constructor(props) {
super(props);
this.renderDynamicFields = this.renderDynamicFields.bind(this);
}
submit(form) {
console.log('form', form);
}
renderDynamicFields({fields = []}) {
let collection = [];
fields.map((name, index) => {
collection.push(<Field key={index} name={`${name}.myDate`} component={renderDynamicField}/>);
});
return <div>{collection}</div>;
function renderDynamicField(props) {
const {input} = props;
let component = null;
if (true) { // going to support different types...
component = <DateTimePicker
defaultValue={input.value || new Date()}
onChange={(value) => {
input.onChange(value);
}}/>;
}
return component;
}
}
render() {
const {error, handleSubmit, pristine, reset, submitting} = this.props;
return (
<Form horizontal onSubmit={handleSubmit(this.submit)}>
<FormGroup><Col componentClass={ControlLabel} xs={2}>
My Date2</Col>
<Col xs={4}>
<FieldArray name="myDates" component={this.renderDynamicFields}/>
</Col>
</FormGroup>
<FormGroup>
<Col smOffset={1} xs={11}>
<Button type="submit" bsStyle="primary"
disabled={pristine || submitting}>Save</Button>
<Button type="button" disabled={pristine || submitting}
onClick={reset}>Reset</Button>
</Col>
</FormGroup>
</Form>
);
}
}
ReactWidgets = reduxForm({
form: 'reactWidgets'
})(ReactWidgets);
ReactWidgets = connect(
state => {
return {
initialValues: {
myDates: [
{myDate: null},
{myDate: null},
{myDate: null}
]
},
}
})(ReactWidgets)
export default ReactWidgets
Related
I'm using React 16.13.0. I want to create a simple search component -- a single text box that as you type displays results. I have created the following component ...
export default class Search extends Component {
constructor(props) {
super(props);
this.state = {
searchTerm: "",
setSearchTerm: "",
searchResults: [],
setSearchResults: []
}
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
const query = event.target.value;
if ( ! query ) {
this.setState({ searchTerm: query, searchResults: [] } );
} else {
this.setState({ searchTerm: query, loading: true }, () => {
this.doSearch(query);
});
}
}
doSearch = ( query ) => {
console.log("dosearch:" + query);
const searchUrl = '/coops/?contains=' + encodeURIComponent(query);
fetch(searchUrl,{
method: "GET"
}).then(response => response.json())
.then(data => {
console.log("query:" + query);
console.log(data);
this.setState({
searchResults: data,
loading: false,
});
});
};
renderSearchResults = () => {
const {searchResults} = this.state;
if (searchResults && searchResults.length) {
return (
<div className="results-container">
<div>Results</div>
<ul>
{searchResults.map(item => (
<li className="result-items" key={item.id} value={item.name}>{item.name}</li>
))}
</ul>
</div>
);
}
};
render() {
return (
<div className="searchForm">
<input
type="text"
placeholder="Search"
value={this.state.searchTerm}
onChange={this.handleChange}
/>
{ this.renderSearchResults() }
</div>
);
}
The issue is that if I type too fast, the fetch requests do not necessarily complete in the order they are sent out. For exmple, if my term is "south," the fetch corresponding to having typed "sou" may complete after I've fully typed "south," causing the results to be displayed incorrectly. How do I account for this and force my results to be displayed corresponding to the inputs typed?
You need to use onKeyUp={} this means that when user finished typing their search query only there you will start making the request.
<input
type="text"
placeholder="Search"
value={this.state.searchTerm}
onKeyUp={this.handleChange}
/>
I am struggling to get the value out of the Material-ui Autocomplete when using redux-form. Has anyone solved this? I am using the exact example from the material-ui Autocomplete https://material-ui.com/components/autocomplete/ I am able to see the list options and it populates after clicking it twice, but I am unable to extract the real value, instead I am returning ({ title : 0 }) instead of the value.
import React from "react";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import { Field, reduxForm } from "redux-form";
import { connect } from "react-redux";
class Form extends React.Component {
onSubmit = formValues => {
console.log(formValues);
};
renderTextField = ({
label,
input,
meta: { touched, invalid, error },
...custom
}) => (
<Autocomplete
label={label}
options={this.props.movies}
placeholder={label}
getOptionLabel={option => option.title}
onChange={input.onChange}
{...input}
{...custom}
renderInput={params => (
<TextField {...params} label={label} variant="outlined" fullWidth />
)}
/>
);
render() {
const { handleSubmit } = this.props;
return (
<div>
<form onSubmit={handleSubmit(this.onSubmit)}>
<Field
name="propertySelector"
component={this.renderTextField}
label="Select Property"
type="text"
/>
</form>
</div>
);
}
}
const mapStateToProps = state => {
console.log(state);
return {
movies: [
{ title: "The Shawshank Redemption", year: 1994 },
{ title: "The Godfather", year: 1972 },
{ title: "Schindler's List", year: 1993 }
]
};
};
Form = reduxForm({
form: "auto_complete"
});
export default connect(mapStateToProps, null)(Form);
Solved by passing in the (event, value) to the onChange props.
onChange={(event, value) => console.log(value)}
From the docs;
Callback fired when the value changes.
Signature:
function(event: object, value: T) => void
event: The event source of the callback.
value: null
Im using the useState hook to update an array. This array renders a list of inputs.
This code does update the useState hook correctly but it removes focus from the input after every key press. Why is this happening and how can I fix it?
import React, { useState } from "react";
const Todos = () => {
const [todos, setTodos] = useState(["Read book", "Tidy room"]);
function update(index: number, event: React.ChangeEvent<HTMLInputElement>) {
const newTodos = [...todos];
newTodos[index] = event.target.value;
setTodos(newTodos);
}
return (
<ul>
{todos.map((item, index) => {
return (
<li key={item}>
<input
type="text"
value={item}
onChange={event => update(index, event)}
/>
</li>
);
})}
</ul>
);
};
export default Exercises;
So the problem is that you're using the item's value as the key for each <li>. When you change the value in the input, the key will change and react renders an entire new <li> instead of just changing the one that is already loaded on the screen.
The easiest solution would be to make each Todo an object, and give it a id that doesn't change:
import React, { useState } from "react";
interface Todo {
value: string;
id: string;
}
const Todos = () => {
const [todos, setTodos] = useState<Todo[]>([
{
value: "Read book",
id: '1'
},
{
value: "Tidy room",
id: '2'
}
]);
function update(index: number, event: React.ChangeEvent<HTMLInputElement>) {
const newTodos = [...todos];
newTodos[index].value = event.target.value;
setTodos(newTodos);
}
return (
<ul>
{todos.map((item, index) => {
return (
<li key={item.id}>
<input
type="text"
value={item.value}
onChange={event => update(index, event)}
/>
</li>
);
})}
</ul>
);
};
export default Exercises;
I am validating a input field ( required , min length 3 ) with VeeValidate plugin.. it's working fine
but how I can avoid the onInput action to be called ( to avoid commit in store when the input becomes invalid ( as soon as input aria-invalid switch to false )
shortly said : Is there anyway to switch calling/not Calling onInput: 'changeTitle' when the input field aria-invalid is false/true ?
thanks for feedback
ChangeTitleComponent.vue
<template>
<div>
<em>Change the title of your shopping list here</em>
<input name="title" data-vv-delay="1000" v-validate="'required|min:3'" :class="{'input': true, 'is-danger': errors.has('required') }" :value="title" #input="onInput({ title: $event.target.value, id: id })"/>
<p v-show="errors.has('title')">{{ errors.first('title') }}</p>
</div>
</template>
<style scoped>
</style>
<script>
import { mapActions } from 'vuex'
import Vue from 'vue'
import VeeValidate from 'vee-validate'
Vue.use(VeeValidate)
export default {
props: ['title', 'id'],
methods: mapActions({ // dispatching actions in components
onInput: 'changeTitle'
})
}
</script>
vuex/actions.js
import * as types from './mutation_types'
import api from '../api'
import getters from './getters'
export default {
...
changeTitle: (store, data) => {
store.commit(types.CHANGE_TITLE, data)
store.dispatch('updateList', data.id)
},
updateList: (store, id) => {
let shoppingList = getters.getListById(store.state, id)
return api.updateShoppingList(shoppingList)
.then(response => {
return response
})
.catch(error => {
throw error
})
},
...
}
UPDATE
I tried to capture the input value with #input="testValidation) and check for a valid input value (required)
if valid ( aria-invalid: false) then I emit the input value, but the props are not updated in the parent component and the vuex action 'changeTitle' is not triggered
<template>
<div>
<em>Change the title of your shopping list here</em>
<input name="title" ref="inputTitle" data-vv-delay="1000" v-validate="'required'" :class="{'input': true, 'is-danger': errors.has('required') }" :value="title" #input="testValidation({ title: $event.target.value, id: id })"/>
<p v-show="errors.has('title')">{{ errors.first('title') }}</p>
</div>
</template>
<script>
import { mapActions } from 'vuex'
import Vue from 'vue'
import VeeValidate from 'vee-validate'
Vue.use(VeeValidate)
export default {
props: ['title', 'id'],
methods: {
testValidation: function (value) {
const ariaInvalid = this.$refs.inputTitle.getAttribute('aria-invalid')
if (ariaInvalid === 'false') {
this.$nextTick(() => {
this.$emit('input', value) // should change props in parent components
})
} else {
console.log('INVALID !') // do not change props
}
},
...mapActions({
onInput: 'changeTitle' // update store
})
}
}
</script>
like you access the errors collection in the VUE template, you can also access the same errors collection in your testValidation method
so replace
const ariaInvalid = this.$refs.inputTitle.getAttribute('aria-invalid')
with
const ariaInvalid = this.$validator.errors.has('title')
grtz
I have a form that display a Many-to-Many relationship between two entities Brand and Distribution.
I display the distributions through a CheckboxGroupInput component in a brand page. During the form edition (Edit component), I managed to pre-check distributions that have been checked previously during the creation (Create) (this is not specified in the documentation) but I can't check or uncheck any distributions.
Here's the edition form :
export class BrandEdit extends Component {
constructor(props) {
super(props)
this.state = {
isLoading: true
}
}
componentDidMount() {
//Find all distributions
restClient(GET_MANY, 'distribution', {
sort: {field: 'name', order: 'ASC'}
})
.then(response => {
return restClient(GET_ONE, 'brand', {
id: this.props.match.params.id
}).then(brandResult => {
let distributionsIds = []
brandResult.data.distributions.forEach(distribution => {
distributionsIds.push(distribution.id)
})
this.setState({
distributions: response.data,
distribution_checked_ids: distributionsIds,
isLoading: false,
})
});
})
}
render() {
const { isLoading, distributions, distribution_checked } = this.state
return (
<div>
{
<Edit {...this.props}>
<SimpleForm>
<DisabledInput source="id"/>
<TextInput label="Nom" source="name"/>
<CheckboxGroupInput
source="distributions"
input={{
value: this.state.distribution_checked_ids,
onChange: (checkedDistributionIds) => {
this.setState({ distribution_checked_ids: checkedDistributionIds });
}
}}
choices={distributions}
optionValue="id"
optionText="name"
/>
</SimpleForm>
</Edit>
}
</div>
);
}
}
Any ideas ?
Thanks
We need to pass an array of Distribution id to the component and not and array of Distribution objects.
Here's the component :
<CheckboxGroupInput
source="distributions"
choices={distributions}
optionValue="id"
optionText="name"
/>
Data should look like :
{
"id": 2115,
"name": "Test",
"distributions": [12, 13, 14]
}
Please remove the input prop. Why would you handle form state by yourself? AOR uses redux-form to handle that.