How to handle multiple input forms in Vuex 4.x? - forms

I have a Vue component with 5 input elements. As a exercise to learn VueX I wanted to manage the user input in a Vuex store. Let's assume each input represents a line in a poem. My state, mutation and actions look like that
state: {
poem: {
line1: '',
line2: '',
line3: '',
line4: '',
line5: '',
}
},
mutations: {
setPoem(state, line) {
state.poem = {...state.poem, ...line}
},
resetPoem(state) {
state.poem = {
line1: '',
line2: '',
line3: '',
line4: '',
line5: '',
}
}
},
actions: {
setPoem({commit}, line) {
commit('setPoem', line)
},
resetPoem({commit}) {
commit('resetPoem')
},
},
Looking the documentation I found that I could use v-model as usual but with a two-way computed property: https://next.vuex.vuejs.org/guide/forms.html#two-way-computed-property
But it seems not very DRY to create a computed property for each input element like to:
computed: {
line1: {
get() {
return this.$store.state.poem.line1;
},
set(value) {
this.$store.dispatch('setPoem', {line1: value})
}
},
line2: {
get() {
return this.$store.state.poem.line2;
},
set(value) {
this.$store.dispatch('setPoem', {line2: value})
}
},
line3: {
get() {
return this.$store.state.poem.line3;
},
set(value) {
this.$store.dispatch('setPoem', {line3: value})
}
},
line4: {
get() {
return this.$store.state.poem.line4;
},
set(value) {
this.$store.dispatch('setPoem', {line4: value})
}
},
line5: {
get() {
return this.$store.state.poem.line5;
},
set(value) {
this.$store.dispatch('setPoem', {line5: value})
}
}
},
My template looks like this:
<form class="form-group" v-on:submit.prevent="addDocument">
<input v-model="line1" type="text" />
<p class="error">{{errorMsg1}}</p>
<input v-model="line2" type="text" />
<p class="error">{{errorMsg2}}</p>
<input v-model="line3" type="text" />
<p class="error">{{errorMsg3}}</p>
<input v-model="line4" type="text" />
<p class="error">{{errorMsg4}}</p>
<input v-model="line5" type="text" />
<p class="error">{{errorMsg5}}</p>
<button type="submit">Send Poem</button>
</form>
How can I refactor this? Is there a best practice to manage state of multiple forms?

You can use vuex-map-fields
<script>
import { mapFields } from 'vuex-map-fields';
export default {
computed: {
...mapFields([
'poem.line1',
'poem.line2',
'poem.line3',
// ...
]),
},
};
</script>
and in your store, you can import the getField and updateField to fetch and mutate data
...
getters: {
getField,
},
mutations: {
updateField,
}

Related

Preset input values of the form with Vuex

In my project (laravel/vue spa) I have a form opened in a modal window, the form shall display the initial values from the object kept in the vuex store. The problem is the input doesn't see objet property that I set through value, so with my code now when the modal is opened, no data is displayed inside the input, although if i output the section object in the markup it generally sees it.
How can I make the input work with the preset values?
Here's my code:
ModalForm.vue markup
<template>
<app-modal>
<Form #submit="submitSectionForm">
<div class="form-group">
<label for="title" class="control-label">Title</label>
//<p>{{ section }}</p>
<Field
:value="sectionTitle" #input="handleUpdateTitle"
:rules="isRequired"
type="text"
name="section_title"
class="form-control"
/>
<ErrorMessage
name="section_title"
class="pr-1 display-block color-red"
/>
</div>
<button class="btn btn-default" #click="closeModal">Cancel</button>
<button class="btn btn-primary ml-1">Ok</button>
</Form>
</app-modal>
</template>
ModalForm.vue functionality
<script>
import AppModal from "../../components/Modals/Modal.vue";
import { Field, ErrorMessage } from "vee-validate";
import { mapGetters, mapActions, mapState, mapMutations } from "vuex";
export default {
props: {
menu_type_id: Number,
menu_id: Number,
},
components: {
AppModal,
Field,
ErrorMessage,
},
methods: {
...mapActions("menu_sections", ["saveSection"]),
...mapMutations("menu_sections", ['updateTitle']),
isRequired(value) {
if (value && value.trim()) {
return true;
}
handleUpdateTitle(e) {
this.updateTitle(e.target.value);
}
submitSectionForm(value) {
console.log(value);
this.saveSection(value);
this.closeModal();
},
computed: {
...mapGetters("menu_sections", { section: "getSection" } ),
...mapGetters("menu_sections", { sectionTitle: "getSectionTitle" }),
},
};
</script>
section_store.js
import axios from "axios";
export default {
namespaced: true,
state: {
section: {},
message: "",
},
getters: {
indexById: (state) => (id) =>
state.sections.findIndex((item) => item.id == id),
sectionById: (state) => (id) =>
state.sections.filter((item) => item.id == id),
getSection: (state) => state.section,
getSectionTitle: (state )=>state.section.title
},
mutations: {
setSection(state, section) {
state.section = { ...section };
},
saveSection(state, data) {
state.message = data.message;
},
updateTitle(state, title) {
state.section.title = title;
console.log(state.section.title);
},
},
actions: {
saveSection({ commit }, section_object) {
axios
.post("/api/menu-sections/" + section_object)
.then((response) => {
commit("saveSection", response.data);
})
.catch((e) => {
console.log("Section save error");
});
}
},
};

The API request from the Vuejs component is executed twice

I am creating To-do Application using Vuejs, Expressjs, and Mongodb.
I have already written through the New Component with a separate link. This is normal, but I am currently using the New Component by importing it into the Root Component without any additional links.
This works, but the API request is executed twice. (Two articles are written.)
I just imported the component, what's the problem?
I have only sent one request but strangely two responses are flying. (id is different, title and content are the same.)
frontend/components/TodoForm.vue
<template>
<form #submit.prevent="create">
title : <input v-model="todo.title">
content : <input v-model="todo.content">
<button v-on:click="create" >Create</button>
</form>
</template>
<script>
export default {
data: function () {
return {
todo: {}
}
},
methods: {
create: function() {
this.$http.post('/api/todos/create', this.todo)
.then(
(response) => {
console.log(response.data)
this.$router.push({name: 'Todo', params: { id: response.data._id }})
},
(err) => {
alert('Error')
}
)
.catch(function (error) {
alert('error')
})
}
}
}
</script>
fromtend/components/TodoList.vue
<template>
<todo-form/>
<div class="todos">
<h1>Todos</h1>
<div v-for="todo in todos" v-bind:key="todo" class="todo">
<div>
<strong>{{todo.title}}</strong>
<router-link :to="{ name: 'Todo', params: { id: todo._id }}">detail</router-link>
<router-link :to="{ name: 'Edit', params: { id: todo._id }}">edit</router-link>
<button type="submit" #click="deleteTodo(todo._id)">Delete</button>
</div>
</div>
</div>
</template>
<script>
import TodoForm from './TodoForm';
export default {
data () {
return {
todos: {}
}
},
created () {
this.$http.get('/api/todos')
.then((response) => {
this.todos= response.data
})
},
components: {
TodoForm
},
methods: {
deleteTodo (id) {
const targetIndex = this.todos.findIndex(v => v._id === id)
this.$http.delete(`/api/todos/${id}`)
.then((response) => {
this.todos.splice(targetIndex, 1)
})
}
}
}
</script>
backend/api
router.post ('/create', (req, res) => {
let todo = new Todo({
title: req.body.title || req.body.todo.title,
content: req.body.content || req.body.todo.content
});
todo.save((err) => {
if (err) {
res.status(500).send('Something broke!');
}
res.json(todo)
});
});
Try to change the following code :
<template>
<form>
title : <input v-model="todo.title">
content : <input v-model="todo.content">
<button v-on:click.once="create" >Create</button>
</form>
</template>

How To Submit Form Data From B-modal In Vue

So I am having a issue trying to submit form data from a b-modal in my Vue component. I can see in my dev tools that the data object is being populated with the data however when I submit my form It doesn't send my data.
here is the form
<b-modal v-model="modalShow" id="myModal">
<form>
<div>
<br>
<input type="text" placeholder="Name" v-model="user.name">
<br>
<input type="text" placeholder="Email" v-model="user.email">
<br>
<input type="text" placeholder="Password" v-model="user.password">
<br>
</div>
<div>
<b-btn #click="modalShow = false">Cancel</b-btn>
<b-btn variant="outline-primary" #click="addUser">Create</b-btn>
</div>
</form>
</b-modal>
Here is the data object and method from the component
data() {
return {
modalShow: false,
user: {
name: '',
email: '',
password: '',
}
}
},
components:{
'b-modal': bModal,
},
directives: {
'b-modal': bModalDirective
},
computed: {
...mapGetters(['users']),
},
methods: {
addUser() {
this.$store.dispatch('addUser', {
name: this.user.name,
email: this.user.email,
password: this.user.password,
})
}
},
and here is the store method being dispatched
addUser(user) {
return new Promise((resolve, reject) => {
axios.post('/register', {
name: user.name,
email: user.email,
password: user.password,
})
.then(response => {
console.log(response)
resolve(response)
})
.catch(error => {
reject(error.response.data)
})
})
},
if I do a return response before processing the data on my back end it shows a empty data object. any ideas why?
The first argument in vuex action is context object, the payload is passed as the second argument
addUser(context, user) {
return new Promise((resolve, reject) => {
axios.post('/register', {
name: user.name,
email: user.email,
password: user.password,
})
.then(response => {
console.log(response)
resolve(response)
})
.catch(error => {
reject(error.response.data)
})
})
}

vue managing form editing state, boilerplate code

I have a decent number of forms in my app, in which the user can choose to edit, revert, or save changes to the object, which are eventually saved to the backend.
Very similar to this: (code found in another question)
https://jsfiddle.net/k5j6zj9t/22/
var app = new Vue({
el: '#app',
data: {
isEditing: false,
user: {
firstName: 'John',
lastName: 'Smith',
}
},
mounted() {
this.cachedUser = Object.assign({}, this.user);
},
methods: {
save() {
this.cachedUser = Object.assign({}, this.user);
this.isEditing = false;
},
cancel() {
this.user = Object.assign({}, this.cachedUser);
this.isEditing = false;
}
}
})
Since v-model binding immediately changes the underlying object, I have to first create a clone of the object. Also I need to save a data member whether the object is in editing state.
Multiply this code for more forms and fields, and I end up with too much data members and a lot of boilerplate code.
In server frameworks like django, a model is in 'temporary state' until it is saved, so I can edit like this
user.first_name = 'aa' # temporary object in memory
user.save() # saved to the db
My question, is there a model component/pattern for vue to handle this task better?
Something that will hold the model state - i.e isEditing, automatically clone the object for form editing, revert the changes, etc.
So I won't have to write such code for so many objects?
Uses Scoped Slots may meet your requirements.
My solution:
Create one component with one slot
Then this slot will bind values with clonedValues (if closeMode is false, clondedValues = values)
finally, in parent component, generate your template with the properties of scoped slot, then pass it to the slot.
Like below demo:
Vue.component('child', {
template: `
<div>
<div>
<slot v-bind:values="clonedValues"></slot>
</div>
<p>
<button #click="saveAction(clonedValues)">Save</button>
<button #click="resetAction()">Reset</button>
</p>
</div>`,
props: {
'cloneMode': {
type: Boolean,
default: true
},
'values': {
type: Object,
default: () => { return new Object() }
},
'saveAction': {
type: Function,
default: function (newValues) {
this.$emit('save', newValues)
}
},
'resetAction': {
type: Function,
default: function () {
this.syncValues(this.values)
}
}
},
data() {
return {
clonedValues: {}
}
},
created: function () {
this.syncValues(this.values)
},
watch: {
values: {
handler: function (newVal) {
this.syncValues(newVal)
},
deep: true
},
cloneMode: function () {
this.syncValues(this.values)
}
},
methods: {
syncValues: function (newVal) {
this.clonedValues = this.cloneMode ? Object.assign({}, newVal) : newVal // if you'd like to support nested object, you have to deep clone
}
}
})
Vue.config.productionTip = false
app = new Vue({
el: "#app",
data: {
mode: true,
labels: ['id', 'name'],
childForm: {
'id': 1,
'name': 'test'
}
},
methods: {
saveForm: function (ev) {
Object.keys(this.childForm).forEach((item) => {
this.childForm[item] = ev[item]
})
// call backend to update the data
},
changeCurrentValue: function () {
this.childForm.id += '#'
this.childForm.name += '#'
}
}
})
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<p><button #click="mode=!mode">Mode: {{mode}}</button></p>
<p>Current: {{childForm}} --<button #click="changeCurrentValue()">Change Current</button></p>
<child :values="childForm" #save="saveForm($event)" :clone-mode="mode">
<template slot-scope="slotProps">
<p>ID: <input v-model="slotProps.values['id']"/></p>
<p>Name: <input v-model="slotProps.values['name']"/></p>
</template>
</child>
</div>
Edit for OP requested:
change default slot to named slot=edit, then create one slot=view
added data property=editing, if true, show 'Edit' slot, if false, show 'View' slot.
in parent component, design the template for slot=view.
Like below demo:
Vue.component('child', {
template: `
<div>
<div v-show="editing">
<slot name="edit" v-bind:values="clonedValues"></slot>
<button #click="saveForm(clonedValues)">Save</button>
<button #click="resetAction()">Reset</button>
</div>
<div v-show="!editing">
<slot name="view"></slot>
<button #click="editing = true">Edit</button>
</div>
</div>`,
props: {
'values': {
type: Object,
default: () => { return new Object() }
},
'saveAction': {
type: Function,
default: function (newValues) {
this.$emit('save', newValues)
}
},
'resetAction': {
type: Function,
default: function () {
this.syncValues(this.values)
}
}
},
data() {
return {
editing: false,
clonedValues: {}
}
},
created: function () {
this.syncValues(this.values)
},
watch: {
editing: function (newVal) {
if(newVal) this.syncValues(this.values)
},
values: {
handler: function (newVal) {
if(this.editing) this.syncValues(newVal) //comment out this if don't want to sync latest props=values
},
deep:true
}
},
methods: {
syncValues: function (newVal) {
this.clonedValues = Object.assign({}, newVal) // if you'd like to support nested object, you have to deep clone
},
saveForm: function (values) {
this.saveAction(values)
this.editing = false
}
}
})
Vue.config.productionTip = false
app = new Vue({
el: "#app",
data: {
childForm: {
'id': 1,
'name': 'test'
}
},
methods: {
saveForm: function (ev) {
Object.keys(this.childForm).forEach((item) => {
this.childForm[item] = ev[item]
})
// call backend to update the data
},
changeCurrentValue: function () {
this.childForm.id += '#'
this.childForm.name += '#'
}
}
})
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<p>Current: {{childForm}} --<button #click="changeCurrentValue()">Change Current</button></p>
<child :values="childForm" #save="saveForm($event)">
<template slot-scope="slotProps" slot="edit">
<h3>---Edit---</h3>
<p>ID: <input v-model="slotProps.values['id']"/></p>
<p>Name: <input v-model="slotProps.values['name']"/></p>
</template>
<template slot="view">
<h3>---View---</h3>
<p>ID: <span>{{childForm['id']}}</span></p>
<p>Name: <span>{{childForm['name']}}</span></p>
</template>
</child>
</div>

How to pass input value to form onSubmit without using state in component that renders multiple forms?

This is a bit of a longwinded problem and giving me a ton of headache to solve.
I'm making a voting app. On the page there will be a list of polls on which you can vote. Each poll is a form consisting of input radio buttons representing the different options available for that poll.
What I was doing previously was saving the option you choose to component state in this.state.value and then passing it as an argument to an action creator when the form is submitted.
Problem with this approach is that if I click an option of one poll, and then click submit on another poll, I've actually submitted the wrong option to the wrong poll.
Is there a way to pass input value to form onSubmit without storing it in component state?
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import Loading from '../Loading';
class MyPolls extends Component {
constructor(props) {
super(props);
this.state = {
skip: 0,
isLoading: true,
isLoadingMore: false,
value: ''
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
this.props.fetchMyPolls(this.state.skip)
.then(() => {
setTimeout(() => {
this.setState({
skip: this.state.skip + 4,
isLoading: false
});
}, 1000);
});
}
sumVotes(acc, cur) {
return acc.votes + cur.votes
}
loadMore(skip) {
this.setState({ isLoadingMore: true });
setTimeout(() => {
this.props.fetchMyPolls(skip)
.then(() => {
const nextSkip = this.state.skip + 4;
this.setState({
skip: nextSkip,
isLoadingMore: false
});
});
}, 1000);
}
handleSubmit(title, e) {
// console.log(e.target);
e.preventDefault();
const vote = {
title,
option: this.state.value
};
console.log(vote)
}
handleChange(event) {
this.setState({ value: event.target.value });
}
renderPolls() {
return this.props.polls.map(poll => {
return (
<div
className='card'
key={poll._id}
style={{ width: '350px', height: '400px' }}>
<div className='card-content'>
<span className='card-title'>{poll.title}</span>
<p>
Total votes: {poll.options.reduce((acc, cur) => { return acc + cur.votes }, 0)}
</p>
<form onSubmit={e => this.handleSubmit(poll.title, e)}>
{poll.options.map(option => {
return (
<p key={option._id}>
<input
name={poll.title}
className='with-gap'
type='radio'
id={option._id}
value={option.option}
onChange={this.handleChange}
/>
<label htmlFor={option._id}>
{option.option}
</label>
</p>
)
})}
<button
type='text'
className='activator teal btn waves-effect waves-light'
style={{
position: 'absolute',
bottom: '10%',
transform: 'translateX(-50%)'
}}
>
Submit
<i className='material-icons right'>
send
</i>
</button>
</form>
</div>
<div className='card-reveal'>
<span className='card-title'>{poll.title}
<i className='material-icons right'>close</i>
</span>
<p>
dsfasfasdf
</p>
</div>
</div>
)
})
}
render() {
return (
<div className='center-align container'>
<h2>My Polls</h2>
{this.state.isLoading ? <Loading size='big' /> :
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-evenly',
alignItems: 'center',
alignContent: 'center'
}}>
{this.renderPolls()}
</div>}
<div className='row'>
{this.state.isLoadingMore ? <Loading size='small' /> :
<button
className='btn red lighten-2 wave-effect waves-light' onClick={() => this.loadMore(this.state.skip)}>
Load More
</button>}
</div>
</div>
);
}
}
function mapStateToProps({ polls }) {
return { polls }
}
export default connect(mapStateToProps, actions)(MyPolls);
App demo: https://voting-app-drhectapus.herokuapp.com/
(use riverfish#gmail.com and password 123 to login)
Github repo: https://github.com/drhectapus/voting-app
I'm open to any suggestions. Thanks!
The more "React'ish" pattern would be to break it down to more components.
a Poll is a component, a PollOption could be a component as well.
Where each can handle the state internally.
This will allow you to keep global state in your App or some other state manager like redux that will hold all of your polls and each can reference to the selected option (id).
Another thing worth pointing, is that you tend to pass a new function reference on each render call.
For example:
onSubmit={e => this.handleSubmit(poll.title, e)}
This is considered as bad practice because you can interfere with the Reconciliation and The Diffing Algorithm of react.
When you break it down to components that each can fire back a callback with its
props, then you don't need to pass the handler this way.
Here is a small example with your data:
const pollsFromServer = [
{
_id: "5a0d308a70f4b10014994490",
title: "Cat or Dog",
_user: "59f21388843e737de3738a3a",
__v: 0,
dateCreated: "2017-11-16T06:30:34.855Z",
options: [
{ option: "Cat", _id: "5a0d308a70f4b10014994492", votes: 0 },
{ option: "Dog", _id: "5a0d308a70f4b10014994491", votes: 0 }
]
},
{
_id: "5a0c7941e655c22b8cce43d7",
title: "Blonde or Brunette?",
_user: "59f21388843e737de3738a3a",
__v: 0,
dateCreated: "2017-11-15T17:28:33.909Z",
options: [
{ option: "Blonde", _id: "5a0c7941e655c22b8cce43d9", votes: 0 },
{ option: "Brunette", _id: "5a0c7941e655c22b8cce43d8", votes: 0 }
]
},
{
_id: "5a0c7924e655c22b8cce43d4",
title: "Coke or Pepsi",
_user: "59f21388843e737de3738a3a",
__v: 0,
dateCreated: "2017-11-15T17:28:04.119Z",
options: [
{ option: "Coke", _id: "5a0c7924e655c22b8cce43d6", votes: 0 },
{ option: "Pepsi", _id: "5a0c7924e655c22b8cce43d5", votes: 0 }
]
},
{
_id: "5a0c78c2e655c22b8cce43d0",
title: "Favourite german car?",
_user: "59f21388843e737de3738a3a",
__v: 0,
dateCreated: "2017-11-15T17:26:26.724Z",
options: [
{ option: "BMW", _id: "5a0c78c2e655c22b8cce43d3", votes: 0 },
{ option: "Mercedes", _id: "5a0c78c2e655c22b8cce43d2", votes: 0 },
{ option: "Audi", _id: "5a0c78c2e655c22b8cce43d1", votes: 0 }
]
}
];
class Poll extends React.Component {
onSubmit = optionId => {
const { pollId, onSubmit } = this.props;
onSubmit(pollId, optionId);
};
render() {
const { title, options, selectedOption } = this.props;
return (
<div>
<h3>{title}</h3>
<ul>
{options.map((o, i) => {
return (
<PollOption
isSelected={selectedOption === o._id}
onClick={this.onSubmit}
name={o.option}
optionId={o._id}
/>
);
})}
</ul>
</div>
);
}
}
class PollOption extends React.Component {
onClick = () => {
const { optionId, onClick } = this.props;
onClick(optionId);
};
render() {
const { name, isSelected } = this.props;
const selectedClass = isSelected ? "selected" : '';
return (
<li
className={`poll-option ${selectedClass}`}
onClick={this.onClick}
>
{name}
</li>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
polls: pollsFromServer,
submittedPolls: []
};
}
onPollSubmit = (pollId, optionId) => {
this.setState({
submittedPolls: {
...this.state.submittedPolls,
[pollId]: optionId
}
});
};
render() {
const { polls, submittedPolls } = this.state;
return (
<div>
{polls.map((p, i) => {
const selectedPoll = submittedPolls[p._id];
return (
<Poll
selectedOption={selectedPoll}
pollId={p._id}
onSubmit={this.onPollSubmit}
title={p.title}
options={p.options}
/>
);
})}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
.poll-option{
cursor: pointer;
display: inline-block;
box-shadow: 0 0 1px 1px #333;
padding: 15px;
}
.selected{
background-color: green;
color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>