I'm stuck on a problem and was hoping that a Javascript Jedi could help point me in the right direction.
Scope of the problem:
I'm passing a Laravel collection to my Vue component. Inside the component, I'm iterating through the collection and submitting a form via axios. The form submits, the data is updated in the database, but __I'm not clear on how to show the updated value without a page refresh.__
Expected Outcome:
The updated data is reflected in the {{ collection.value }} inside the Vue template after form submission
What's going wrong:
The data is being updated in the database, but the {{ collection.value }} remains the same until page is reloaded.
Web.php:
Route::post('/updateQty', 'MyController#update');
MyController:
public function update(Request $request)
{
$product = Product::where('id', $request->productId)
->update([ 'qty' => $request->qty ]);
return response()->json($product);
}
public function index()
{
$collection = DB::table('products')->get();
return view('my-blade', [
'collections' => $collection,
]);
}
Structure of $collection as stored in the database:
'qty' => decimal(8,2),
'class' => varchar(255),
'description' => varchar(255),
'value' => decimal(8,2),
'productId' => int(11)
my-blade:
<my-component :collections="{{ $collections }}"></my-component>
MyComponent.vue:
<template>
<div class="container">
<div class="row">
<div class="col-lg-12">
<table class="table table-sm">
<div v-if="collections.length">
<tr v-for="collection in collections" v-bind:key="collection.id">
<td>
<form #submit="updateQty">
<input type="hidden" id="productId" :value="collection.productId" name="productId">
<select class="form-control" name="qty" id="qty" #change="updateQty">
<option :value="collection.qty">{{ collection.qty }}</option>
<option v-for="(x, index) in 200" v-bind:key="index" :value="index">{{ index }}</option>
</select>
</form>
</td>
<td>{{ collection.value }}</td>
</tr>
</div>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['collections'],
data() {
return {
qty: '',
}
}
mounted() {
console.log('MyComponent.vue mounted successfully');
},
methods: {
updateQty(e) {
e.preventDefault();
let currentObj = this;
let url = '/updateQty';
axios.post(url, {
qty: qty.value,
})
.then(function (response) {
currentObj.value = (response.data);
let collections = response.data;
})
},
}
}
</script>
App.js
Vue.component('my-component', require('./components/MyComponent.vue'));
I'm sure it's something simple, but for the life of me I can't wrap my head around it. Thank you very much in advance!
You just need to change up your script a bit.
First, save the collections property to a data property, or Vue will scream when you try to update it. To do this, I would rename the incoming prop as something like collections_prop. Then save it to the collections data property.
Then change let collections = to this.collections = in your update response.
EDIT: I changed the .then function to ES6 syntax as you may have trouble accessing the this variable otherwise. No need for the currentObj stuff.
export default {
props: ['collections_prop'],
mounted() {
console.log('MyComponent.vue mounted successfully');
},
data() {
return {
collections: this.collections_prop;
}
},
methods: {
updateQty(e) {
e.preventDefault();
let url = '/updateQty';
// not sure where qty is coming from
// but you said that's all worked out
// on your end
axios.post(url, {
qty: qty.value,
})
.then(response => {
this.collections = response.data;
})
},
}
}
And finally, don't forget to update the prop in your view.
<my-component :collections_prop="{{ $collections }}"></my-component>
Or if you want to later specify prop type as JSON:
<my-component :collections_prop='#json($collections)'></my-component>
Related
I am trying to create a simple CRUD app with Vuejs 3.
I have a homepage with a form (as a child component) and a table with created items (as another child component). I submit data via the form to API/database and the table updates. So far so good.
Then, for the update phase, I would like to have a detail page for each item where I also would have the form (the same component reused). But the idea is that form fields would be pre-populated with data from API/Database.
The table on the homepage has a route-link to a detail page and I am passing the id of the item as params. The detail page makes request to API based on id, receives item data and passes them as props into the form component.
If I try to render data directly into template like this, it works fine:
<p v-if="submitType === 'update' && item.id">{{ item.id }}</p>
Now, form fields are bound by v-model to data (form.id for example). But when I try to repopulate it as below, I always get undefined values.
data() {
return {
form: {
id: this.submitType === 'update' ? this.item.id : 0,
}
}
},
I suspect that problem is that the parent call to API is asynchronous and the passing of props is delayed. Because when I pass as props some hardcoded value, it appears as a value in the form field with no problem. Also if the form is shown only when props are received (with the v-if directive), the data.form.id is still undefined.
So is there any way how to pre-populate bound form fields with received props and still have the form component reused for insert and update actions? The rest of the relevant code is below. Thank you very much in advance
// Detail Page
<template>
<Form :item="item" submit-type="update"></Form>
</template>
<script>
export default {
data() {
return {
item: {}
}
},
created() {
callAPI(id).then( response => this.item = response.data )
}
}
</script>
// Form Component
<template>
<p v-if="submitType === 'update' && item.id">{{ item.id }}</p>
<div v-if="submitType === 'insert' || (submitType === 'update' && item.id )">
<section>
<form #submit.prevent="onSubmit">
<div>
<label for="id">ID</label>
<input id="id" name="id" v-model="form.id" type="number" placeholder="ID">
</div>
<input type="submit" value="Save">
</form>
</section>
</div>
</template>
<script>
export default {
name: 'Form',
props: {
item: {
type: Object
},
submitType: {
type: String
}
},
data() {
return {
form: {
id: this.submitType === 'update' ? this.item.id : 0,
}
}
},
}
</script>
You can try with watchers, take a look at following snippet:
const app = Vue.createApp({
data() {
return {
item: {},
type: 'update'
}
},
methods: {
change() {
this.type === 'update' ? this.type = 'insert' : this.type = 'update'
}
},
created() {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => this.item = json)
//callAPI(id).then( response => this.item = response.data )
}
})
app.component('myForm', {
template: `
<p v-if="submitType === 'update' && item.id">{{ item.id }}</p>
<div v-if="submitType === 'insert' || (submitType === 'update' && item.id )">
<section>
<form #submit.prevent="onSubmit">
<div>
<label for="id">ID</label>
<input id="id" name="id" v-model="form.id" type="number" placeholder="ID">
</div>
<input type="submit" value="Save">
</form>
</section>
</div>
`,
props: {
item: {
type: Object
},
submitType: {
type: String
}
},
data() {
return {
form: {}
}
},
methods: {
fillData() {
this.submitType === 'update' ? this.form = {...this.item} : this.form = {id: 0}
}
},
watch: {
item() {
this.fillData()
},
submitType() {
this.fillData()
}
},
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<button #click="change">switch type</button>
{{type}}
<my-form :item="item" :submit-type="type"></my-form>
</div>
I am attempting to do a SPA using Vue.js but unfortunately I know almost nothing about it, I followed a tutorial and got something up and running. This should hopefully be relatively simple!
I'm trying to create a simple page that:
Does a REST API call and pulls some JSON
A list with links of a particular field in the list of results is displayed on the left side of the screen
(I've managed until here)
Now I would like to be able to click on one of the links and see on the right side of the screen the value of another field for the same record.
For instance, suppose my JSON is:
{
"jokes":{
[
"setup":"setup1",
"punchline":"punchline1"
],
[
"setup":"setup2",
"punchline":"punchline2"
],
[
"setup":"setup3",
"punchline":"punchline3"
]
}
}
So in my screen I would see:
setup1
setup2
setup3
So if I click in setup1 I see punchline1, setup2 displays punchline2 and so on.
Here is my code - I'm basically trying to display the punchline in the moduleinfo div. I realise the current solution does not work. I've been searching but can't find any similar examples. Any pointers would be greatly appreciated.
<template>
<div class="home">
<div class="module-list">
<input type="text" v-model.trim="search" placeholder="Search"/>
<div>
<ul>
<li class="modules" v-for="value in modulesList" :key="value.id">
{{ value.setup }}
</li>
</ul>
</div>
</div>
<div class="moduleinfo">
<h2>Module info</h2>
<!-- <p>{{ value.punchline }}</p> -->
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Home',
data: function(){
return {
jokes: [],
search : ""
}
},
mounted() {
this.getModules();
},
methods: {
getModules() {
var self = this
const options = {
method: 'GET',
url: 'https://dad-jokes.p.rapidapi.com/joke/search',
params: {term: 'car'},
headers: {
'x-rapidapi-key': '...',
'x-rapidapi-host': 'dad-jokes.p.rapidapi.com'
}
};
axios.request(options)
.then(response => {
self.jokes = response.data;
console.log(response.data);
}).catch(function (error) {
console.error(error);
});
}
},
computed: {
modulesList: function () {
var jokes = this.jokes.body;
var search = this.search;
if (search){
jokes = jokes.filter(function(value){
if(value.setup.toLowerCase().includes(search.toLowerCase())) {
return jokes;
}
})
}
return jokes;
}
},
};
</script>
Thanks!
I was building a sample Single File Component in my Vue 2 CLI app, and when I came back to post it, Ryoko had already answered the question with the same approach that I recommend, adding a new property to track showing the punchline.
Since I already built it, I figured that I might as well post my component, which does change the layout, using a table instead of a list, but the functionality works.
<template>
<div class="joke-list">
<div class="row">
<div class="col-md-6">
<table class="table table-bordered">
<thead>
<tr>
<th>SETUP</th>
<th>PUNCHLINE</th>
</tr>
</thead>
<tbody>
<tr v-for="(joke, index) in jokes" :key="index">
<td>
{{ joke.setup }}
</td>
<td>
<span v-if="joke.showPunchline">{{ joke.punchline }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
jokes: [
{
setup: "setup1",
punchline: "punchline1"
},
{
setup: "setup2",
punchline: "punchline2"
},
{
setup: "setup3",
punchline: "punchline3"
}
]
}
},
methods: {
getPunchline(index) {
this.jokes[index].showPunchline = true;
},
addPropertyToJokes() {
// New property must be reactive
this.jokes.forEach( joke => this.$set(joke, 'showPunchline', false) );
}
},
mounted() {
this.addPropertyToJokes();
}
}
</script>
You can add a new property inside the data object and then make a new method to set it accordingly when you click the <a> tag. Have a look at the code below, it was a copy of your current solution, edited & simplified to show the addition that I made to make it easier for you to find it.
The select method will insert the object of the clicked joke to the selectedJoke so you can render it below the Module Info.
Because it's defaults to null, and it might be null or undefined, you have to add v-if to the attribute to check wether there is a value or not so you don't get error on the console.
<template>
<div class="home">
<div class="module-list">
<input type="text" v-model.trim="search" placeholder="Search"/>
<div>
<ul>
<li class="modules" v-for="value in modulesList" :key="value.id">
{{ value.setup }}
</li>
</ul>
</div>
</div>
<div class="moduleinfo">
<h2>Module info</h2>
<p v-if="selectedJoke">{{ selectedJoke.punchline }}</p>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Home',
data: function(){
return {
jokes: [],
search : "",
selectedJoke: null,
}
},
methods: {
select(joke) {
this.selectedJoke = joke;
},
},
};
</script>
I am creating commenting system using vue.js and laravel5.8.
I have done with models and seeding, so I have now 10 comments to one post (id is 51).
But I got this error,
Property or method "comment" is not defined on the instance but
referenced during render
and
Cannot read property 'user' of undefined
I have problems with fetching data.
I created a new endpoint for a comment function.
web.php
Route::get('results/{post}', 'ResultsController#show')->name('posts.show');
Route::get('results/{post}/comments', 'CommentsController#index');
I want to show comments in show.blade.php.
ResultsController.php
public function show(Post $post)
{
$recommended_posts = Post::latest()
->whereDate('date','>',date('Y-m-d'))
->where('category_id','=',$post->category_id)
->where('id','!=',$post->id)
->limit(7)
->get();
$posts['particular_post'] = $post;
$posts['recommended_posts'] = $recommended_posts;
$post->comments()->with('user')->get();
return view('posts.show',compact('posts'));
}
show.blade.php
<comments-component :post="{{ $posts['particular_post']->comments }}"></comments-component>
comments.vue
<div class="reply-comment" :v-for="comment in comments">
<div class="user-comment" >
<div class="user">
<!--<img src="" alt="" >-->
<avatar :username="comment.user.name" :size="30" ></avatar>
</div>
<div class="user-name">
<span class="comment-name">{{ comment.user.name }}</span>
<p> {{ comment.body }} </p>
</div>
</div>
<div class="reply">
<div class="seemorecomments">
see more
</div>
<button class="reply-button">
<i class="fas fa-reply"></i>
</button>
</div>
</div>
<script>
import Avatar from 'vue-avatar'
export default {
props: ['post'],
components: {
Avatar
},
mounted() {
this.fetchComments()
},
data: () => ({
comments: {
data: []
}
}),
methods: {
fetchComments() {
axios.get(`/results/${this.post.id}/comments`).then(({data}) => {
this.comments = data
})
}
}
}
CommentsController.php
public function index(Post $post)
{
return $post->comments()->paginate(5);
$post->comments()->with('user')->get();
}
comment.php
protected $with = ['user'];
I cannot get data object here.
Within axios, you may need to access data from the response that is returned (see console.log examples here), try the following within your comments component:
methods: {
fetchComments() {
axios.get(`/results/${this.post.id}/comments`).then((response) => {
this.comments = response.data.data
})
}
}
Note response.data.data is used.
I assume returning the ->paginate() will put the results within a data key in the returned array. If not, then just use response.data.
Also, in the controller getting the comments change to the following:
public function index(Post $post)
{
return $post->comments()->with('user')->paginate(5);
}
This will eager load the users with the queried comments.
I cant update the data properties from created() functions. I tried using 'this' too but i just seem out of scope. Any help?
Anyways a sibling component is emitting info on click, which this component should recieve and display as data, very simple, but i when i try to update the main properties of data, they just always stay the same. Im new to vue2 so any help would be appreciated.
const singleAc = Vue.component('singleAc', {
template: `<div class="helper_text">
<div> Aircraft with ID : {{ $route.params.aircraftId }} </div>
<div><img class="airline_logo" src="//logo.clearbit.com/Ryanair.com"></div>
<div> Model : {{modelName}} </div>
<div v-if="fromAp"> From: {{fromAp}} </div>
<div v-if="toAp"> To: {{toAp}} </div>
</div>`,
data: function() {
return {
company: null,
modelName: null,
fromAp: null,
toAp: null
}
},
created() {
bus.$on('op', function(op) {
singleAc.company = op;
console.log(op)
})
bus.$on('model', function(model) {
singleAc.modelName = model;
console.log(model)
})
bus.$on('from', function(from) {
singleAc.fromAp = from;
console.log(from)
})
bus.$on('to', function(to) {
singleAc.toAp = to;
console.log(to)
})
}
});
singleAc is a Vue component and not a Vue instance. That's why changing data like singleAc.company won't work
You still gotta use this
Solution 1: use arrow functions so that this can be used
const singleAc = Vue.component("singleAc", {
created() {
bus.$on("op", op => {
this.company = op;
console.log(op);
});
}
});
Solution 2: store this in a variable
const singleAc = Vue.component("singleAc", {
created() {
var _t = this;
bus.$on("op", op => {
_t.company = op;
console.log(op);
});
}
});
Hope this helps.
binding this actually solved the problem
bus.$on('to', function(to) {
this.toAp = to;
}.bind(this))
Forget about global events for now, try passing your aircraft's data with props
then your component should access aircraft data by adding:
props: ['aircraft']
Don't forget to point to the aircraft data model. It should look somewhere like this:
`<div :aircraft="aircraft" class="helper_text">
<div> Aircraft with ID : {{ aircraft.id }} </div>
<div><img class="airline_logo" src="//logo.clearbit.com/Ryanair.com"></div>
<div> Model : {{aircraft.modelName}} </div>
<div v-if="fromAp"> From: {{fromAp}} </div>
<div v-if="toAp"> To: {{toAp}} </div>
</div>`
Hope it helps.
I'm using Vue 2 for a small blog project. I have a separate component for post's form and one of the form inputs is a select (for post's category). The select gets populated after the categories are fetched from DB. The component also gets a post object from parent component, that is also fetched from the db (see props: ['post']).
Here's the code:
// HTML
...
<select class="form-control" v-model="post.category_id">
<option
v-for="cat in categories"
v-bind:value="cat.id">
{{ cat.name }}
</option>
</select>
...
// JS
module.exports = {
props: ['post', 'url'],
name: 'postForm',
created: function() {
this.syncCats()
},
methods: {
syncCats: function() {
this.$http.get("/api/categories")
.then(function(res) {
this.categories = res.data
})
}
},
data: function() {
return {
categories: {}
}
}
}
The problem I'm having is that none of the options is selected by default. It looks like this. But when I open the select I see both categories from my db like this.
I want to select the correct (post.category_id == cat.id) value by default. How would I do this?
I've tried <select ... :v-bind:selected="post.category_id == cat.id"> but same happened.
Edit
Okay so now I've tried dumping both post.category_id and cat.id like this:
<div class="form-group">
<label>Category</label>
<select class="form-control" v-model="post.category_id">
<option
v-for="cat in categories"
:value="cat.id"
:selected="cat.id == post.category_id">
{{ cat.name }} {{ cat.id }} {{ post.category_id }}
</option>
</select>
</div>
And the result before I select any option is this - only cat.id gets printed, post.category_id does not. However after I select some option I post.category_id appears as well, like this. Notice how the "1" at the end only appears in 2nd screenshot, after I've selected one of the options, which is the {{ post.category_id }}.
This implies that the post is loaded after the categories and that I should somehow reinitialize the select after it's loaded. How would I do this? For reference this is the parent component that fetches the post.
<template>
<span id="home">
<postForm :post="post" :url="url"></postForm>
</span>
</template>
<script>
var postForm = require('../forms/post.vue')
module.exports = {
name: 'postEdit',
created: function() {
this.$http.get('api/posts/slug/' + this.$route.params.slug)
.then(function(response) {
if(response.status == 200) {
this.post = response.data
this.url = "/api/posts/slug/" + response.data.slug
}
})
},
data: function() {
return {
post: {},
url: ""
}
},
components: {
postForm
}
}
</script>
You'll need to set the selected attribute on the appropriate <option> and adhere to Vue's one-way data flow paradigm.
You can even add some extra usability sugar by disabling the <select> until both the post and categories are loaded...
<select class="form-control"
:disabled="!(post.category_id && categories.length)"
#input="setCategoryId($event.target.value)">
<option v-for="cat in categories"
:value="cat.id"
:selected="cat.id == post.category_id">
{{cat.name}}
</option>
</select>
and
methods: {
setCategoryId(categoryId) {
this.$emit('input', parseInt(categoryId))
}
}
Then, in the Vue instance / component that includes the above one, simply use
<post-form :post="post" :url="url"
v-model="post.category_id"></post-form>
See Components - Form Input Components using Custom Events for more information.
JSFiddle demo ~ https://jsfiddle.net/1oqjojjx/267/
FYI, I'd also initialise categories to an array, not an object...
data () {
return {
categories: []
}
}
You should be able to do something like following:
methods: {
syncCats: function() {
this.$http.get("/api/categories")
.then(function(res) {
this.categories = res.data
if(!this.post.category_id) {
this.post.category_id = this.categories[0].id
}
})
}
},