I'm using mobx-react-form and I need to fill a form with default values pulled from an object in my store. Unfortunately, if I try to use FormModel.$("email").set(object.email); inside my component mobx complains that I can't modify observed objects outside of an action and I exceed maxdepth.
Specifically my code looks like this (some details removed for clarity)
import React from 'react';
import ReactDOM from "react-dom"
import { observer } from "mobx-react-lite"
import validatorjs from 'validatorjs';
import MobxReactForm from 'mobx-react-form';
const fields = [{
name: 'email',
label: 'Email',
placeholder: 'Email',
rules: 'required|email|string|between:5,25',
// value: user.email,
}, …
]
const FormModel = new MobxReactForm({ fields }, { plugins, hooks }); //nothing exception here standard plugins/hooks
const UserForm = observer(({open, onClose, object}) => { //My component…object has fields with names email…
FormModel.$("email").set(object.email); //This works fine if I replace object.email with "foo"
return (<MobxInput field={FormModel.$("email")} fullWidth />);
});
export default UserForm;
Yes, I've checked the object has the appropriate fields (it's just a bare object passed in from parent …not even an observable object in this case).
My first approach was to simply put everything inside UserForm and simply fill the values in fields from object but when I do this typing doesn't work in the resulting form (I suspect that mobx is trying to observe an object created inside that observer and that doesn't work).
The problem is I need to use the same form sometimes with data suppled by a user object from my user store and sometimes with blank values to create a new user and I'm kinda stuck about how to do this now.
First of all, you can't do that:
const UserForm = observer(({open, onClose, object}) => {
// This won't really work very well
FormModel.$("email").set(object.email);
return (<MobxInput field={FormModel.$("email")} fullWidth />);
});
Because every time you change value in your input your whole UserForm component also rerenders (because it observes FormModel.$("email") value which just changed) and when it rerenders you instantly change new value to your old value from object. I am not sure why exactly you getting maxdepth error, but there might even be endless loop here as you can see in some cases. Modifying anything like that inside render is usually a bad practice. You need to use useEffect at least, or something like that.
I can't modify observed objects outside of an action
This happens because you need to do all mutations inside actions by default. You can configure it though, if you don't like it:
import { configure } from "mobx"
configure({
enforceActions: "never",
})
But it is better to stick with it, it might catch some unwanted behaviour.
I've made quick Codesandbox example with some of your code, it shows how you can make several forms and pass default values to them:
const UserForm = observer(({ object }) => {
const [FormModel] = useState(() => {
const fields = [
{
name: 'email',
label: 'Email',
placeholder: 'Email',
rules: 'required|email|string|between:5,25',
value: object?.email || ''
}
];
return new MobxReactForm({ fields }, { plugins });
});
return (
<form onSubmit={FormModel.onSubmit}>
<input {...FormModel.$('email').bind()} />
<p style={{ color: 'red' }}>{FormModel.$('email').error}</p>
<button type="submit">submit</button>
</form>
);
});
That is just one of many ways, it all depends what you need in the end.
Related
I realise this is a common issue with people new to vue and vuex, but I've been using it for two years now and thought I understood the ins and outs. Yet I'm stumped. There must be something I'm overlooking.
We've got a couple of complex models that used to have layouts hard-coded in the front end, and now some of those come from the backend instead, so I added a store module to handle that:
import { ActionTree } from 'vuex';
import { RootState } from '#/store';
import request from '../../services/request';
import layouts from '../../layouts';
export const types = {
FETCH_LAYOUT: `${MODULE_NAME}${FETCH_LAYOUT}`,
};
const initialState = {
layout: layouts,
};
const actions: ActionTree<LayoutState, RootState> = {
async [FETCH_LAYOUT]({ commit, state }, id) {
if (!state[id]) {
const layout = await request.get(`layout/${id}`);
commit(types.FETCH_LAYOUT, { layout, id });
}
},
};
const mutations = {
[types.FETCH_LAYOUT](state: any, { layout, id }) {
state.layout[id] = layout;
},
};
export default {
namespaced: true,
state: initialState,
getters: {},
actions,
mutations,
};
Everything here seems to work fine: the request goes out, response comes back, state is updated. I've verified that that works. I've got two components using this, one of them the parent of the other (and there's a lot of instances of the child). They're far to big to copy them here, but the import part is simply this:
computed: {
...mapState({
layout: (state: any) => {
console.log('mapState: ', state.layout, state.layout.layout[state.modelName]);
return state.layout.layout[state.modelName];
},
}),
},
This console.log doesn't trigger. Or actually it looks like it does trigger in the parent, but not in the children. If I change anything in the front-end code and it automatically loads those changes, the child components do have the correct layout, which makes sense because it's already in the store when the components are rerendered. But doing a reload of the page, they lose it again, because the components render before the layout returns, and they somehow don't update.
I'm baffled why this doesn't work, especially since it does seem to work in the parent. Both use mapState in the same way. Both instantiate the component with Vue.component(name, definition). I suppose I could pass the layout down as a parameter, but I'd rather not because it's global data, and I want to understand how this can fail. I've considered if maybe the state.layout[id] = layout might not trigger an automatic update, but it looks like it should, and the parent component does receive the update. I originally had state[id] = layout, which also didn't work.
Using Vue 2.6.11 and Vuex 3.3.0
I'm trying to handle a form composed by two parts, one fixed and the other one that gets displayed by a switch.
To handle the forms I'm using react-hook-form.
I defined a validation scheme in the file validation.ts inside the constants folder.
About the optional part I defined a sub-object but it doesn't work and it gives a compile time error.
Because of this I opted for the solution you'll find in the link at the bottom of the page
Although I defined the optional input fields inside the validation file, they don't get recognized when I press the submit button.
How can I fix this problem?
At this link you can find a working example of the problem.
The main problem is with your components/Form component, it has this line
// components/Form.tsx
return child.props.name
? .... : child;
What you have done here is ignoring all child components without the name prop,
where as when rendering the component what you did was rendered them inside <></>, use below alternative instead.
// in App.tsx
{isEnabled ? <Input name="trailerPlate" placeholder="Targa rimorchio" /> : <></>}
{isEnabled ? <Input name="trailerEnrolment" placeholder="Immatricolazione rimorchio" /> : <></>}
Still the validation won't because you need to register the components and your current useEffect code doesn't account for change in number of input fields.
Use below code instead
React.useEffect(() => {
....
....
}, [
register,
Array.isArray(children) ?
children.filter(child => child.props.name).length : 0
]
);
We are using the count of child components with name prop as a trigger for useEffect.
And finally you also have to unregister the fields when you toggle the switch,
below is a sample code, feel free to change it according to your preference.
const { handleSubmit, register, setValue, errors, unregister, clearErrors } = useForm<VehicleForm>();
const toggleSwitch = () => {
if (isEnabled) {
unregister('trailerEnrolment');
unregister('trailerPlate');
}
clearErrors();
setIsEnabled(prev => !prev)
};
Feel free to upvote if I was helpful.
I just started learning about Mobx to implement it in my projects, and I've come across a big issue: I seem to not understand how actions work.
I've been following this nice tutorial: https://hackernoon.com/how-to-build-your-first-app-with-mobx-and-react-aea54fbb3265 (the complete code of the tutorial is located here: https://codesandbox.io/s/2z2r43k9vj?from-embed ), and it works smoothly. I've tried to do a small React App on my side, trying to do the same the tutorial mentioned, and yet it is failing. I am sure there is some small detail (since the app is pretty simple) that I am not seeing, so I would appreciate some help on it.
I've also tried to look for similar cases to mine, but I didn't find anything through a quick search (which makes me think even more the problem is insignificant...)
My code is this:
import React, { Component } from 'react';
import { decorate, observable, action, configure } from 'mobx';
import { observer } from 'mobx-react';
configure({ enforceActions: 'always' });
class Store {
my_number = 1;
addNumber() {
this.my_number += 1;
}
removeNumber() {
this.my_number -= 1;
}
}
decorate(Store, {
my_number: observable,
addNumber: action,
removeNumber: action
})
const my_store = new Store();
const Button = (props) => {
if (props.store.my_number === 1) {
return (
<div>
<button onClick={props.store.addNumber}>+</button>
</div>
)
} else if (props.store.my_number === 4) {
return (
<div>
<button onClick={props.store.removeNumber}>-</button>
</div>
)
} else {
return (
<div>
<button onClick={props.store.addNumber}>+</button>
<button onClick={props.store.removeNumber}>-</button>
</div>
)
}
}
const ObserverButton = observer(Button);
const DisplayNumber = (props) => {
return (
<h1>My number is: {props.store.my_number}</h1>
)
}
const ObserverDisplayNumber = observer(DisplayNumber);
export class SimpleMobxStore extends Component {
render() {
return (
<div>
<ObserverButton store={my_store} />
<ObserverDisplayNumber store={my_store} />
</div>
)
}
}
And my thoughts for developing it have been (I would also appreciate suggestions on how to improve my thoughts-flow if it's bad):
I want a text on the screen that shows a number between 1 and 4. Above this text I want to have a button that allows me to increase or decrease this number by adding or substracting a unit each time. I want this variable (the current number) to be stored in a separate store. That store will include:
My number
A method for increasing the number
A method for decreasing the number
In addition I will create two components: a button component that renders my button depending on the current number, and a display component.
My observable will be the number in the store, whereas the two methods will have to be decorated as actions, since they are changing the observed variable.
My button and display components will be observers, since they must be re-rendered once the number changes.
With this simple reasoning and code I was expecting it to function, but instead I'm getting a:
Error: [mobx] Since strict-mode is enabled, changing observed observable values outside actions is not allowed. Please wrap the code in an action if this change is intended. Tried to modify: Store#4.my_number
The log seems to be pointing to when I define const my_store = new Store();, but this is done in the tutorial and it works there.
Any idea on where this is failing and why?
Thank you
I think your action to the store is directly from render(). The tag to be precise. Try having a method outside the render and try changing the store state from there.
I have a component whose purpose is to display a list of items and let the user select one or more of the items.
This component is populated from a backend API and fed by a parent component with props.
However, since the data passed from the prop doesn't have the format I want, I need to transform it and provide a viewmodel with a computed property.
I'm able to render the list and handle selections by using v-on:click, but when I set selected=true the list is not updated to reflect the change in state of the child.
I assume this is because children property changes are not tracked by Vue.js and I probably need to use a watcher or something, but this doesn't seem right. It seems too cumbersome for a trivial operation so I must assume I'm missing something.
Here's the full repro: https://codesandbox.io/s/1q17yo446q
By clicking on Plan 1 or Plan 2 you will see it being selected in the console, but it won't reflect in the rendered list.
Any suggestions?
In your example, vm is a computed property.
If you want it to be reactive, you you have to declare it upfront, empty.
Read more here: reactivity in depth.
Here's your example working.
Alternatively, if your member is coming from parent component, through propsData (i.e.: :member="member"), you want to move the mapper from beforeMount in a watch on member. For example:
propsData: {
member: {
type: Object,
default: null
}
},
data: () => ({ vm: {}}),
watch: {
member: {
handler(m) {
if (!m) { this.vm = {}; } else {
this.vm = {
memberName: m.name,
subscriptions: m.subscriptions.map(s => ({ ...s }))
};
}
},
immediate: true
}
}
So, in one of my VueJS templates, I have a left sidebar that generates buttons by iterating (v-for) through a multidimensional items array.
When one of these buttons is clicked, a method is run:
this.active.notes = item.notes
active.notes is bound to a textarea in the right content section.
So, every time you click one of the item buttons, you see the (active) notes associated with that item.
I want to be able to have the user edit the active notes in the textarea. I have an AJAX call on textarea blur which updates the db. But the problem is, the items data hasn't changed. So if I click a different item, then click back to the edited item, I see the pre-edited notes. When I refresh the page, of course, everything lines up perfectly.
What is the best way to update the items data, so that it is always consistent with the textarea edits? Should I reload the items data somehow (with another AJAX call to the db)? Or is there a better way to bind the models together?
Here is the JS:
export default {
mounted () {
this.loadItems();
},
data() {
return {
items: [],
active: {
notes: ''
},
}
},
methods: {
loadItems() {
axios.get('/api/items/'+this.id)
.then(resp => {
this.items = resp.data
})
},
saveNotes () {
...api call to save in db...
},
updateActive (item) {
this.active.notes = item.notes;
},
}
}
i can't find items property in your data object.
a property must be present in the data object in order for Vue to convert it and make it reactive
Vue does not allow dynamically adding new root-level reactive properties to an already created instance
maybe you can have a look at this:
Vue Reactivity in Depth
It doesn't seem like this.items exists in your structure, unless there is something that isn't shown. If it doesn't exist set it as an empty array, which will be filled on your ajax call:
data() {
return {
active: {
notes: ''
},
items: [],
},
Now when you ajax method runs, the empty array, items, will be filled with your resp.data via this line:(this.items = resp.data). Then you should be able to iterate through your items array using v-for and your updateActive method should work as you intend it to.
use PUSH
this.items.push(resp.data);
here is a similar question
vue.js http get web api url render list