The API request from the Vuejs component is executed twice - mongodb

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>

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

react.js - show a message on and after form submission

On submitting the form, I want to show 'Please wait ..' and on successful submission the data returned from server. Using jQuery , it is easy to do. But there should be a React way as React does not like such kind of direct DOM manipulation - I think . 1) Am I right ? 2) How to show the message on (not after ) form submission?
var FormComp = React.createClass({
handleSubmit:function(){
var userName=this.refs.userName.getDOMNode().value.trim();
var userEmail= this.refs.userEmail.getDOMNode().value.trim();
if(!userName || !userEmail){
return;
}
this.props.onFormSubmit({userName:userName, userEmail:userEmail,url:"/api/submit"});
this.refs.userName.getDOMNode().value='';
this.refs.userEmail.getDOMNode().value='';
return;
},
render: function() {
var result=this.props.data;
return (
<div className={result}>{result.message}</div>
<form className="formElem" onSubmit={this.handleSubmit}>
Name: <input type="text" className="userName" name="userName" ref="userName" /><br/>
Email: <input type="text" className="userEmail" name="userEmail" ref="userEmail" /><br/>
<input type="submit" value="Submit" />
<form >
</div>
);
}
});
var RC= React.createClass({
getInitialState: function() {
return {data: ""};
},
onFormSubmit:function(data){
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: data,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
render:function(){
return <FormComp onFormSubmit={this.onFormSubmit} data={this.state.data}/>
}
});
React.render(
<RC/>,
document.getElementById('content')
);
This is definitely something React can handle, no direct DOM manipulation is needed. You're almost there, just need to reorganize a little. Here's one way to approach this (with comments around important changes):
var FormComp = React.createClass({
// To get rid of those input refs I'm moving those values
// and the form message into the state
getInitialState: function() {
return {
name: '',
email: '',
message: ''
};
},
handleSubmit: function(e) {
e.preventDefault();
var userName = this.state.name.trim();
var userEmail = this.state.email.trim();
if(!userName || !userEmail) return;
this.setState({
name: '',
email: '',
message: 'Please wait...'
});
// I'm adding a callback to the form submit handler, so you can
// keep all the state changes in the component.
this.props.onFormSubmit({
userName: userName,
userEmail: userEmail,
url: "/api/submit"
}, function(data) {
this.setState({ message: data });
});
},
changeName: function(e) {
this.setState({
name: e.target.value
});
},
changeEmail: function(e) {
this.setState({
email: e.target.value
});
},
render: function() {
// the message and the input values are all component state now
return (
<div>
<div className="result">{ this.state.message }</div>
<form className="formElem" onSubmit={ this.handleSubmit }>
Name: <input type="text" className="userName" name="userName" value={ this.state.name } onChange={ this.changeName } /><br />
Email: <input type="text" className="userEmail" name="userEmail" value={ this.state.email } onChange={ this.changeEmail } /><br />
<input type="submit" value="Submit" />
</form>
</div>
);
}
});
var RC = React.createClass({
onFormSubmit: function(data, callback){
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: data,
success: callback,
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
render: function() {
return <FormComp onFormSubmit={this.onFormSubmit} />
}
});
React.render(
<RC />,
document.getElementById('content')
);