vue managing form editing state, boilerplate code - forms

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>

Related

Server Error: Error: Error serializing. Reason: `object` (“[object Object]”) cannot be serialized as JSON Next.js MongoDB

I am working on an online shopping project and implying a review system for shopping products. I am using Next.js and MongoDB for my project. It works fine without any reviews submitted but when I submit my review and the error happened as below;
the error is from getServerSideProps in pages/product/[slug].js and the code is as below;
import axios from 'axios';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router'
import React, { useContext, useState, useEffect } from 'react'
import Layout from '../../components/Layout'
import Product from '../../models/Product';
import db from '../../utils/db';
import { Store } from '../../utils/Store';
import { toast } from 'react-toastify';
import Rating from '#material-ui/lab/Rating';
import { getSession } from 'next-auth/react';
import { useForm } from "react-hook-form";
import { getError } from '../../utils/error';
export default function ProductScreen (props) {
const { product, user } = props;
const {state, dispatch} = useContext(Store);
const router = useRouter();
const [ reviews, setReviews ] = useState([]);
const [ rating, setRating ] = useState(0);
const [ comment, setComment ] = useState('');
const [ loading, setLoading ] = useState(false);
const {
handleSubmit,
register,
formState: { errors },
setValue,
} = useForm();
const submitHandler = async () => {
setLoading(true);
try {
await axios.post(`/api/products/${product._id}/reviews`, { rating, comment, user });
setLoading(false);
toast.success('Review submitted successfully');
fetchReviews();
} catch (err) {
setLoading(false);
return toast.error(getError(err));
}
}
const fetchReviews = async () => {
try {
const { data } = await axios.get(`/api/products/${product._id}/reviews`);
setReviews(data);
} catch (err) {
return toast.error('fetchReview err');
}
}
useEffect(() => {
fetchReviews();
}, []);
if (!product) {
return <Layout title="Product Not Found">Product Not Found</Layout>;
}
const addToCartHandler = async () => {
const existItem = state.cart.cartItems.find((x) => x.slug === product.slug);
const quantity = existItem ? existItem.quantity + 1 : 1;
const { data } = await axios.get(`/api/products/${product._id}`);
if (data.countInStock < quantity) {
return toast.error('Sorry. Product is out of stock');
}
dispatch ({ type: 'CART_ADD_ITEM', payload: { ...product, quantity }});
router.push("/cart");
};
return (
<Layout title={product.name}>
<div className='py-2'>
<Link href="/">back to products</Link>
</div>
<div className='grid md:grid-cols-4 md:gap-3'>
<div className='md:col-span-2'>
<Image
src={product.image}
alt={product.name}
width={640}
height={640}
layout='responsive'
/>
</div>
<div>
<ul>
<li className='mt-4'>
<h1 className='text-lg'>{product.name}</h1>
</li>
<li className='mt-4'>Category: {product.category}</li>
<li className='mt-4'>Brand: {product.brand}</li>
<li className='flex mt-4'>
<Rating value={product.rating} readOnly />
<Link href='#reviews'>
<a>({product.numReviews} reviews)</a>
</Link>
</li>
<li className='mt-4'>Description: {product.description}</li>
</ul>
</div>
<div>
<div className='card p-5'>
<div className='mb-2 flex justify-between'>
<div>Price</div>
<div>${product.price}</div>
</div>
<div className='mb-2 flex justify-between'>
<div>Status</div>
<div>{product.countInStock > 0 ? 'In stock' : 'Unavailable'}</div>
</div>
<button className='primary-button w-full' onClick={addToCartHandler}>Add to cart</button>
</div>
</div>
</div>
<div id='reviews' className='mt-4'>
<h2 className='text-xl'>Customer Reviews</h2>
{reviews.length === 0 && <div>No reviews</div>}
{reviews.map((review) => (
<div key={review._id}>
{review.name}
{review.createdAt.substring(0, 10)}
<Rating value={review.rating} readOnly />
{review.comment}
</div>
))}
</div>
<div>
{user ? (
<form onSubmit={handleSubmit(submitHandler)}>
<h1>Leave your review</h1>
<div>
<Rating
name='simple-controlled'
value={rating}
onChange={(e) => setRating(e.target.value)}
/>
<label htmlFor='comment'>Comment</label>
<input
type='text'
{...register('comment', { required: 'Please enter comment',})}
id='comment'
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
{errors.comment && (
<div className='text-red-500'>{errors.comment.message}</div>
)}
<button className='primary-button'>
{loading? 'Loading' : 'Submit'}
</button>
</div>
</form>
) : (
<div>
Please <Link href={`/login?redirect=/product/${product.slug}`}>Login</Link> to write a review.
</div>
)}
</div>
</Layout>
)
}
export async function getServerSideProps(context) {
const { params, req } = context;
const { slug } = params;
const session = await getSession({ req });
await db.connect();
const product = await Product.findOne({ slug }).lean();
await db.disconnect();
return (
{
props: {
product: product ? db.convertDocToObj(product) : null,
user: session ? session : null,
},
}
);
}
as my understanding, the getServerSideProps function at the bottom should convert the props into object with below code;
product: product ? db.convertDocToObj(product) : null,
am I using it correctly?
my product schema with review is as below;
import mongoose from 'mongoose';
const reviewSchema = new mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
name: { type: String, required: true },
rating: { type: Number, default: 5 },
comment: { type: String, required: true },
},
{
timestamps: true,
});
const productSchema = new mongoose.Schema(
{
name: { type: String, required: true },
slug: { type: String, required: true, unique: true },
category: { type: String, required: true },
image: { type: String, required: true },
price: { type: Number, required: true },
brand: { type: String, required: true },
rating: { type: Number, required: true, default: 0 },
numReviews: { type: Number, required: true, default: 0 },
countInStock: { type: Number, required: true, default: 0 },
description: { type: String, required: true },
reviews: [reviewSchema],
}, {
timestamps: true,
}
);
const Product = mongoose.models.Product || mongoose.model('Product', productSchema);
export default Product;
the MongoDB Compass for Product is as below;
So, how can I fix my problew?
Thanks
Stan
latest Axios version(v1.2.0) changed header field, it is know issue
Not working old code, so you can avoid this error by adding header information.
https://github.com/axios/axios/issues/5298
Your three axios API call needs to add header with correct content type.
(I suppose all of json type in your code)
before
await axios.post(`/api/products/${product._id}/reviews`, { rating, comment, user });
const { data } = await axios.get(`/api/products/${product._id}`);
const { data } = await axios.get(`/api/products/${product._id}/reviews`);
after
await axios.post(`/api/products/${product._id}/reviews`, { rating, comment, user }, {
headers: {
'Content-Type': 'application/json'
}
});
const { data } = await axios.get(`/api/products/${product._id}`,
{
headers: {
'Accept-Encoding': 'application/json'
}
});
const { data } = await axios.get(`/api/products/${product._id}/reviews`,
{
headers: {
'Accept-Encoding': 'application/json'
}
});

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");
});
}
},
};

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

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,
}

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>

use ReactJS to .map through response of Express API

I have an Express API that responds with data from MongoDB when requested upon mounting of a reactJS component.
app.get('/api/characters', function(req, res) {
var db = req.db;
var collection = db.get('usercollection');
collection.find({},{},function(e,docs){
res.send({
data: docs
});
});
});
Code for Characters component :
export default class Characters extends React.Component {
constructor(props) {
super(props);
this.state = CharactersStore.getState();
this.onChange = this.onChange.bind(this);
}
componentDidMount() {
CharactersStore.listen(this.onChange);
CharactersActions.getCharacters('http://localhost:3000/api/characters');
}
componentWillUnmount() {
CharactersStore.unlisten(this.onChange);
}
onChange(state) {
this.setState(state);
}
render() {
return (
<section>
<div className="container">
<div className="row">
<h2 className="text-center">{this.props.route.header}</h2>
<hr className="star-light"/>
<CharacterList data={this.state.characters}/>
</div>
</div>
</section>
)
}
}
Code for Characters component Action
class CharactersActions {
constructor() {
this.generateActions(
'getCharactersSuccess',
'getCharactersFail'
);
}
getCharacters(query) {
requestPromise(query)
.then((res) => {
this.actions.getCharactersSuccess(res)
}).catch((err) => {
console.log('error:', err);
this.actions.getCharactersFail(err)
})
}
}
export default alt.createActions(CharactersActions);
Code for Characters component Store
class CharactersStore {
constructor() {
this.bindActions(CharactersActions);
this.characters = [''];
this.isLoading = true;
}
getCharactersSuccess(res) {
this.characters = res;
this.isLoading = false;
}
getCharactersFail(err) {
toastr.error(err.responseJSON && err.responseJSON.message || err.responseText || err.statusText);
}
}
export default alt.createStore(CharactersStore);
The Characters component above requests the API upon mounting and sends the response data along to the store to be saved into State.
I then pass the Characters state into the child component CharacterList as props (named data)
Characters List Component
export default class CharacterList extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="col-lg-3 col-sm-6">
{console.log(this.props.data)}
// THIS IS WHERE I AM TRYING TO .MAP THROUGH THE RESULTS
</div>
)
}
}
I am trying to use .map to loop through the returned data but am a little unsure on how to proceed, any advice would be appreciated. Data is being returned as follows:
{
"data": [
{
"_id": "58d5044b0898f816066227f1",
"character": "Luke Skywalker"
},
{
"_id": "58d504c60898f816066227f2",
"character": "Obi Wan Kenobi"
},
{
"_id": "58d504c60898f816066227f3",
"character": "Han Solo"
}
]
}
You want to set res.data as your characters store.
getCharactersSuccess(res) {
this.characters = res.data; // <--
this.isLoading = false;
}
Then you can map over the data array, and not the full response:
const data = [
{
_id: '58d5044b0898f816066227f1',
character: 'Luke Skywalker'
},
{
_id: '58d504c60898f816066227f2',
character: 'Obi Wan Kenobi'
},
{
_id: '58d504c60898f816066227f3',
character: 'Han Solo'
}
];
const Character = ({ data }) => <div>{data.character}</div>;
class CharacterList extends React.Component {
render() {
return (
<div>
{this.props.data.map((character, i) => (
<Character key={i} data={character} />
))}
</div>
);
}
}
ReactDOM.render(<CharacterList data={data} />, document.getElementById('root'));
<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>
It is simple.I dont know where you confused. try this
<div className="col-lg-3 col-sm-6">
{
Array.isArray(this.props.data)&& this.props.data.map((val,index)=>{
return val
})
}
</div>