Why parent component does not update input component when setting state? - javascript

I have a input component like this:
let InputBasic = React.createClass({
displayName: 'InputBasic',
path: '',
componentWillMount: function () {
this.path = this.props.storePath + '.' + this.props.storeIndex;
},
getInitialState: function () {
return { };
},
getError: function () {
if (!Array.isArray(this.props.formErrorFields)) {
return false;
};
__this = this;
let hasError = false;
this.props.formErrorFields.forEach(function (index) {
if (__this.props.name == index.name) {
hasError = true;
return;
}
});
return hasError;
},
sendToValidation: function() {
if (this.props.rules) {
bom.form.validateInput(
/* Vars here */
);
}
},
getClassName: function () {
if(this.getError()) {
return 'input-basic has-error';
}
else {
return 'input-basic';
}
},
onBlur: function () {
this.sendToValidation();
},
handleChange: function (event) {
this.setState({ value: event.target.value });
},
render: function () {
return React.createElement('input',
{
type: this.props.type,
placeholder: this.props.placeholder,
style: this.props.style,
onChange: this.handleChange,
onBlur: this.onBlur,
className: this.getClassName()
}
);
}
});
The idea is to validate input field on blur. If the input doesn't pass the validation, the containing form state is altered. The form setState() function is passed to the input as a prop, then to the validation and so on.
The logic works in setting the form state. The form state is set accordingly. I also have a another component, which is dependant on the form state and that works perfectly. However, the input component is not updated as I was expecting.
In following scenario I have problems:
Empty, initial input -> Click to trigger onBlur -> validation fails -> input component updates
Type in to input -> Click to trigger onBlur -> validation success -> input component updates
Remove text again from input -> Click to trigger onBlur -> validation fail -> input component does not update
Add text again to input -> Click to trigger onBlur -> validation succes -> input component does update
This is peculiar, since other elements inside form are updated. If I add forceUpdate() to the onBlur function, the component updates as expected. However, this solution feels hacky, especially because I don't understand why I don't get the desired effect otherwise.
Things which came in to my mind while trying to solve this:
Can this be caused by the fact, that the component is triggering the update process which eventually points to itself? What I try to ask here is that is it a problem that I am passing the setState function down as a prop?
Are input components treated somehow differently, when it comes to updating?
EDIT:
Could this be caused because of onChange function? It is not in the example, since SO nagged about too long code sample.
MORE EDIT:
Apparently how I assign the value to the state affects if the update on children is launched.
This is the function which I use to update the state of the input parent:
bom.form = {
getErrorField: function (fields, name, text) {
let newFields = fields;
if (fields[name]) {
newFields[name].hasError = true;
newFields[name].errorText = text;
}
return newFields;
},
validateInput: function (inputValue, inputName, inputRules, setFormState, formFields) {
let fields = formFields;
if (!inputValue && inputRules.required) {
let text = 'This field is required.';
fields = this.getErrorField(formFields, inputName, text);
}
state = {};
state.fields = fields;
return setFormState(state);
}
};
So, calling validateInput() my I set parent component state of the input.
This does not launch the update in the input component. However, if I change state.fields assignment in example to this:
state.fields = { password: {}, username: {}}
It launches the update as desired. And of course, in this example the state is not what I want, this is just for presentational purposes.
The peculiar thing is that in both of these cases the other, different child component (which I call fieldMessage) updates. It is rendered this way:
return React.createElement('p', null, this.getText());
The getText() function returns parent component state, which is pointed to right field.
So I have became in to conclusion, that my problem has something to do how I assign the state object to setState() function. Am I correct? Can this happen because I pass the current state to the child function and am I possibly using object reference to original in a wrong place, or something like that?

You are not binding your onBlur handler to your react class. In particular:
render: function () {
return React.createElement('input',
{
type: this.props.type,
placeholder: this.props.placeholder,
style: this.props.style,
onChange: this.handleChange,
onBlur: this.onBlur.bind(this),
className: this.getClassName()
}
);
}
This is because, this.sendToValidation(); ends up being called bound to the event target.

I solved the problem by assigning the state.fields like this:
state.fields = Object.create(fields);
I assume that originally the fields were pointing to original state object reference and therefore did not launch the update. However, this is really just my guess and if somebody can clarify the behaviour to me I would be very grateful.

Related

Algolia vue-places: Clear input after selection

I'm using the vue-places component to render an Algolia places search input in Vue - it works brilliantly.
After a user selects / accepts a suggestion from the search dropdown, I want to clear the input and allow them to search again. Based on the standard example provided, I have tried to set form.country.label v-model value back to null in the change handler:
<template>
<places
v-model="form.country.label"
placeholder="Where are we going ?"
#change="selectLocation"
:options="options">
</places>
</template>
<script>
import Places from 'vue-places';
export default {
data() {
return {
options: {
appId: <YOUR_PLACES_APP_ID>,
apiKey: <YOUR_PLACES_API_KEY>,
countries: ['US'],
},
form: {
country: {
label: null,
data: {},
},
},
};
},
components: {
Places,
},
methods: {
selectLocation: function(event: any) {
if (event.name !== undefined) {
/**
* implementation not important
*/
this.form.country.label = null;
}
}
}
}
</script>
The selectLocation method fires as expected - but I cannot find any way to rest the input value to be empty.
How can I update a data value from a component method and have this reflected in a referencing component - in this case the Algolia places input?
The issue occurs because of how vue-places is proxying change events from the underlying implementation. When a change event is received, it broadcasts the same event and then updates the input value:
Places.vue:
this.placesAutocomplete.on('change', (e) => {
this.$emit('change', e.suggestion);
this.updateValue(e.suggestion.value);
});
This means that any attempt to set a value in our change handler will immediately be overridden.
My solution is to create a ref to the vue-places instance and then use the built-in Vue.nextTick to use the internal places.js setVal method after the call to updateValue:
methods: {
selectLocation: function(event: any) {
if (event.name !== undefined) {
// do something with the value
Vue.nextTick(() => {
// clear the input value on the next update
this.$refs.placesSearch.placesAutocomplete.setVal(null);
});
}
}
}

Vuex commit after await won't update state

I've read multiple similar questions about this here and elsewhere, but I can't figure it out.
I have a form with mapGetters and input values that should update based on Vuex state:
...mapGetters({
show: "getShow"
}),
sample form input (I'm using Bootstrap Vue):
<b-form-input
id="runtime"
name="runtime"
type="text"
size="sm"
v-model="show.runtime"
placeholder="Runtime"
></b-form-input>
Then I have this method on the form component:
async searchOnDB() {
var showId = this.show.showId;
if (!showId) {
alert("Please enter a showId");
return;
}
try {
await this.$store.dispatch("searchShowOnDB", showId);
} catch (ex) {
console.log(ex);
alert("error searching on DB");
}
},
and this action on the store:
async searchShowOnDB({ commit, rootState }, showId) {
var response = await SearchAPI.searchShowOnDB(showId);
var show = {
show_start: response.data.data.first_aired,
runtime: response.data.data.runtime,
description: response.data.data.overview
};
//I'm updating the object since it could already contain something
var new_show = Object.assign(rootState.shows.show, show);
commit("setShow", new_show);
}
mutation:
setShow(state, show) {
Vue.set(state, "show", show);
}
searchAPI:
export default {
searchShowOnDB: function (showId) {
return axios.get('/search/?id=' + showId);
},
}
Everything works, the API call is executed, I can even see the Vuex updated state in Vue Devtools, but the form is not updated.
As soon as I write something in an input field or hit commit in Vue Devtools, the form fields show_start, runtime, description all get updated.
Also, this works correctly and updates everything:
async searchShowOnDB({ commit, rootState }, showId) {
var show = {
show_start: "2010-03-12",
runtime: 60,
description: "something"
};
//I'm updating the object since it could already contain something
var new_show = Object.assign(rootState.shows.show, show);
commit("setShow", new_show);
}
I don't know what else to do, I tried by resolving Promises explicitly, remove async/await and use axios.get(...).then(...), moving stuff around... nothing seems to work.
On line 15 of your /modules/search.js you're using Object.assign() on rootState.search.show. This mutates the search prop of the state (which is wrong, btw, you should only mutate inside mutations!). Read below why.
And then you're attempting to trigger the mutation. But, guess what? Vue sees it's the same value, so no component is notified, because there was no change. This is why you should never mutate outside of mutations!
So, instead of assigning the value to the state in your action, just commit the new show (replace lines 15-16 with:
commit('setShow', show);
See it here: https://codesandbox.io/s/sharp-hooks-kplp7?file=/src/modules/search.js
This will completely replace state.show with show. If you only want to merge the response into current state.show (to keep some custom stuff you added to current show), you could spread the contents of state.show and overwrite with contents of show:
commit("setShow", { ...rootState.search.show, ...show });
Also note you don't need Vue.set() in your mutation. You have the state in the first parameter of any mutation and that's the state of the current module. So just assign state.show = show.
And one last note: when your vuex gets bigger, you might want to namespace your modules, to avoid any name clashes.
All props of objects in a state that is used in templates must exist or you should call Vue.set for such properties.
state: {
show: {
runtime: null // <- add this line
}
},
You call Vue.set for the whole object but it already exist in the state and you do not replace it by a new one you just replace props. In your case you have an empty object and add the 'runtime' prop it it using Object.assign.
Also all manipulations with state should be done in mutations:
var new_show = {
runtime: response.data.url
};
commit("setShow", new_show);
...
mutations: {
setShow(state, new_show) {
Object.assign(state.show, new_show)
}
},

In ReactJS todo app I am trying to set input field text state into another state which is array of objects

I am doing simple todo app. I have 1 input box and Add button. On typing into input box and clicking Add button the text is displayed in a list below. I am using below constructor:
class Entry extends Component {
constructor() {
super()
this.state = {
st_search_field: '',
st_lists: [
{
list_id: new Date(`enter code here`),
list_name: ''
}
]
}
}
Now I create list_handler = () => { ...... } function to set search_field text into list_name value. I use list_handler method during onClick for Add button. I have started by usingconst join = Object.assign({}, this.state) in list_handler function and tried using this.setState({st_lists.list_name: this.state.st_searh_field}) butst_lists.list_name is marked in red in VS editor. Tried this.state.st_lists.map(li => {}) above setState method but even that gives error.
you can't set state on one of states sub objects do it like this:
const st_lists = this.state.st_lists;
st_lists.list_name = this.state.st_search_field;
this.setState({st_lists});
st_lists.list_name is not a key, you need to add merge the object
this.setState({
st_lists: [
{
...this.state.st_lists,
list_name: this.state.st_searh_field,
},
]
})

Use React state to manage list of already instanced objects?

I want to check each collection I instance to see if another instance of the same type already exists so I can use the stored version instead of creating a new one, how can I use the react state to keep a list to check against?
Sample code:
export default React.createBackboneClass({
getInitialState() {
return {
data: [],
groups: {}
}
},
componentWillMount() {
this.setState({
data: this.props.collection
});
},
render() {
const gridGroups = this.state.data.map((model) => {
let gridGroupsCollection = null;
if(this.state.groups[model.get('id')]) {
gridGroupsCollection = this.state.groups[model.get('id')];
} else {
gridGroupsCollection = this.state.groups[model.get('id')] = new GridGroupCollection([], {
groupId: model.get('id')
});
this.setState((previous) => {
groups: _.extend({}, previous, gridGroupsCollection)
});
}
return <GridGroupComponent
key={model.get('id')}
name={model.get('n')}
collection={gridGroupsCollection} />
});
return (
<div>
{gridGroups}
</div>
);
}
});
Few points. First of all, I would use the componentWillReviewProps to do the heavy work. Doing so on the render method can be more costly. I have yet to save instances on the state object. Is it needed for caching performance-wise or to solve a sorting issue?
Moreover, as noted already, calling setState from the render method will generate an infinite loop. as render is called when the state changes.
On a side note I would consider Redux for this, which offers several holistic ways to separate components from data manipulation.
update
This TodoMVC example might point you to the direction of how to combine react and backbone:
componentDidMount: function () {
// Whenever there may be a change in the Backbone data, trigger a
// reconcile.
this.getBackboneCollections().forEach(function (collection) {
// explicitly bind `null` to `forceUpdate`, as it demands a callback and
// React validates that it's a function. `collection` events passes
// additional arguments that are not functions
collection.on('add remove change', this.forceUpdate.bind(this, null));
}, this);
},
componentWillUnmount: function () {
// Ensure that we clean up any dangling references when the component is
// destroyed.
this.getBackboneCollections().forEach(function (collection) {
collection.off(null, null, this);
}, this);
}

Is there a proper way of resetting a component's initial data in vuejs?

I have a component with a specific set of starting data:
data: function (){
return {
modalBodyDisplay: 'getUserInput', // possible values: 'getUserInput', 'confirmGeocodedValue'
submitButtonText: 'Lookup', // possible values 'Lookup', 'Yes'
addressToConfirm: null,
bestViewedByTheseBounds: null,
location:{
name: null,
address: null,
position: null
}
}
This is data for a modal window, so when it shows I want it to start with this data. If the user cancels from the window I want to reset all of the data to this.
I know I can create a method to reset the data and just manually set all of the data properties back to their original:
reset: function (){
this.modalBodyDisplay = 'getUserInput';
this.submitButtonText = 'Lookup';
this.addressToConfirm = null;
this.bestViewedByTheseBounds = null;
this.location = {
name: null,
address: null,
position: null
};
}
But this seems really sloppy. It means that if I ever make a change to the component's data properties I'll need to make sure I remember to update the reset method's structure. That's not absolutely horrible since it's a small modular component, but it makes the optimization portion of my brain scream.
The solution that I thought would work would be to grab the initial data properties in a ready method and then use that saved data to reset the components:
data: function (){
return {
modalBodyDisplay: 'getUserInput',
submitButtonText: 'Lookup',
addressToConfirm: null,
bestViewedByTheseBounds: null,
location:{
name: null,
address: null,
position: null
},
// new property for holding the initial component configuration
initialDataConfiguration: null
}
},
ready: function (){
// grabbing this here so that we can reset the data when we close the window.
this.initialDataConfiguration = this.$data;
},
methods:{
resetWindow: function (){
// set the data for the component back to the original configuration
this.$data = this.initialDataConfiguration;
}
}
But the initialDataConfiguration object is changing along with the data (which makes sense because in the read method our initialDataConfiguration is getting the scope of the data function.
Is there a way of grabbing the initial configuration data without inheriting the scope?
Am I overthinking this and there's a better/easier way of doing this?
Is hardcoding the initial data the only option?
extract the initial data into a function outside of the component
use that function to set the initial data in the component
re-use that function to reset the state when needed.
// outside of the component:
function initialState (){
return {
modalBodyDisplay: 'getUserInput',
submitButtonText: 'Lookup',
addressToConfirm: null,
bestViewedByTheseBounds: null,
location:{
name: null,
address: null,
position: null
}
}
}
//inside of the component:
data: function (){
return initialState();
}
methods:{
resetWindow: function (){
Object.assign(this.$data, initialState());
}
}
Caution, Object.assign(this.$data, this.$options.data()) does not
bind the context into data().
So use this:
Object.assign(this.$data, this.$options.data.apply(this))
cc this answer was originally here
To reset component data in a current component instance you can try this:
Object.assign(this.$data, this.$options.data())
Privately I have abstract modal component which utilizes slots to fill various parts of the dialog. When customized modal wraps that abstract modal the data referred in slots belongs to parent
component scope. Here is option of the abstract modal which resets data every time the customized modal is shown (ES2015 code):
watch: {
show (value) { // this is prop's watch
if(value) {
Object.assign(this.$parent.$data, this.$parent.$options.data())
}
}
}
You can fine tune your modal implementation of course - above may be also executed in some cancel hook.
Bear in mind that mutation of $parent options from child is not recommended, however I think it may be justified if parent component is just customizing the abstract modal and nothing more.
If you are annoyed by the warnings, this is a different method:
const initialData = () => ({})
export default {
data() {
return initialData();
},
methods: {
resetData(){
const data = initialData()
Object.keys(data).forEach(k => this[k] = data[k])
}
}
}
No need to mess with $data.
I had to reset the data to original state inside of a child component, this is what worked for me:
Parent component, calling child component's method:
<button #click="$refs.childComponent.clearAllData()">Clear All</button >
<child-component ref='childComponent></child-component>
Child component:
defining data in an outside function,
referencing data object by the defined function
defining the clearallData() method that is to be called upon by the
parent component
function initialState() {
return {
someDataParameters : '',
someMoreDataParameters: ''
}
}
export default {
data() {
return initialState();
},
methods: {
clearAllData() {
Object.assign(this.$data, initialState());
},
There are three ways to reset component state:
Define key attribute and change that
Define v-if attribute and switch it to false to unmount the component from DOM and then after nextTick switch it back to true
Reference internal method of component that will do the reset
Personally, I think the first option is the clearest one because you control the component only via Props in a declarative way. You can use destroyed hook to detect when the component got unmount and clear anything you need to.
The only advance of third approach is, you can do a partial state reset, where your method only resets some parts of the state but preserves others.
Here is an example with all the options and how to use them:
https://jsbin.com/yutejoreki/edit?html,js,output

Categories

Resources