Laravel 7 Vue getting favorite button - laravel

is there something wrong with this part? getting this on console
app.js:38308 [Vue warn]: Unknown custom element: - did you
register the component correctly? For recursive components, make sure
to provide the "name" option.
(found in )
here is the code
<template>
<span>
<a href="#" v-if="isFavorited" #click.prevent="unFavorite(post)">
<i class="fa fa-heart"></i>
</a>
<a href="#" v-else #click.prevent="favorite(post)">
<i class="fa fa-heart-o"></i>
</a>
</span>
</template>
<script>
export default {
props: ['post', 'favorited'],
data: function() {
return {
isFavorited: '',
}
},
mounted() {
this.isFavorited = this.isFavorite ? true : false;
},
computed: {
isFavorite() {
return this.favorited;
},
},
methods: {
favorite(post) {
axios.post('/favorite/'+post)
.then(response => this.isFavorited = true)
.catch(response => console.log(response.data));
},
unFavorite(post) {
axios.post('/unfavorite/'+post)
.then(response => this.isFavorited = false)
.catch(response => console.log(response.data));
}
}
}
</script>

Related

Inertia mounted hook with search resets pagination on every action

My vue file:
data() {
return {
search: '',
}
},
mounted() {
this.search = this.filters.search ?? '';
},
watch: {
search(value) {
Inertia.get('/contacts', { search: value }, {
preserveState: true,
replace: true,
})
}
The Laravel Controller:
$contacts = Model::query()
->paginate(10)
->withQueryString();
return Inertia::render('Contacts/List', [
'contacts' => $contacts,
'filters' => request()->only(['search']),
'currentPage' => request()->page,
]);
It all works perfectly if the mounted block is missing.
With it, on every Inertia reload a "new search" is registered (since it's changed in the mounted hook) and it returns to page 1, so basically, every time you change the page it returns you to page 1.
It should be working perfectly with the Composition API's setup, but not sure why can't I get it to work here.
I use axios instead of Inertia.js in pagination
<template>
<div class="row mt-4">
<div class="col-md-5">
<span
>showing {{ data.from }}-{{ data.to }} of
{{ data.total }} items</span
>
</div>
<div class="col-lg-7">
<nav aria-label="...">
<ul class="pagination float-end">
<template v-for="link in data.links" :key="link.id">
<li v-if="!link.url" class="page-item">
<a #click.prevent="paginate(link.label)" class="page-link">
<span
aria-hidden="true"
v-html="link.label"
></span>
</a>
</li>
<li
v-else
:class="{ active: link.active }"
class="page-item"
>
<a
#click.prevent="paginate(link.label)"
v-html="link.label"
class="page-link"
></a>
</li>
</template>
</ul>
</nav>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
props: ["data", "url"],
methods: {
paginate(label) {
console.log(`${route(this.url)}?page=${label}`);
axios.get(`${route(this.url)}?page=${label}`).then((res) => {
this.$emit('paginate', res.data)
});
}
}
}
</script>
I ran into the same issue, and after some digging I found this answer. The gist is that you should set your search parameter directly in the data attribute instead of using mounted():
data() {
return {
search: this.filters.search ?? '',
}
},
watch: {
search(value) {
Inertia.get('/contacts', { search: value }, {
preserveState: true,
replace: true,
})
}

Edit default pagination in vuejs?

I handle vuejs + laravel
I Controller :
public function listData (Request $request)
{
$currentPage = !empty($request->currentPage) ? $request->currentPage : 1;
$pageSize = !empty($request->pageSize) ? $request->pageSize : 30;
$skip = ($currentPage - 1) * $pageSize;
$totalProduct = Product::select(['id', 'name'])->get();
$listProduct = Product::select(['id', 'name'])
->skip($skip)
->take($pageSize)
->get();
return response()->json([
'listProduct' => $listProduct,
'total' => $totalProduct,
]);
}
In vuejs
data() {
return {
pageLength: 30,
columns: [
{
label: "Id",
field: "id",
},
{
label: "Name",
field: "name",
},
],
total: "",
rows: [],
currentPage: 1,
};
},
created() {
axios
.get("/api/list")
.then((res) => {
this.rows = res.data. listProduct;
this.total = res.data.total;
})
.catch((error) => {
console.log(error);
});
},
methods: {
changePagination() {
axios
.get("/api/list", {
params: {
currentPage: this.currentPage,
pageSize: this.pageLength,
},
})
.then((res) => {
this.rows = res.data. listProduct;
this.total = res.data.total;
})
.catch((error) => {
console.log(error);
});
},
},
Template :
<vue-good-table
:columns="columns"
:rows="rows"
:rtl="direction"
:search-options="{
enabled: true,
externalQuery: searchTerm,
}"
:select-options="{
enabled: false,
selectOnCheckboxOnly: true,
selectionInfoClass: 'custom-class',
selectionText: 'rows selected',
clearSelectionText: 'clear',
disableSelectInfo: true,
selectAllByGroup: true,
}"
:pagination-options="{
enabled: true,
perPage: pageLength,
}"
>
<template slot="pagination-bottom">
<div class="d-flex justify-content-between flex-wrap">
<div class="d-flex align-items-center mb-0 mt-1">
<span class="text-nowrap"> Showing 1 to </span>
<b-form-select
v-model="pageLength"
:options="['30', '50', '100']"
class="mx-1"
#input="changePagination"
/>
<span class="text-nowrap"> of {{ total }} entries </span>
</div>
<div>
<b-pagination
:value="1"
:total-rows="total"
:per-page="pageLength"
first-number
last-number
align="right"
prev-class="prev-item"
next-class="next-item"
class="mt-1 mb-0"
v-model="currentPage"
#input="changePagination"
>
<template #prev-text>
<feather-icon icon="ChevronLeftIcon" size="18" />
</template>
<template #next-text>
<feather-icon icon="ChevronRightIcon" size="18" />
</template>
</b-pagination>
</div>
</div>
</template>
I am dealing with a product list that has 500,000 products. I don't want to take it out once. I want it to pull out 30 products each time, when I click on the partition, it will call to the api to call the next 30 products.. But my problem is the default pageLength is 30 products, When I choose show showing 50 products , it still shows 30 products on the page list (But I console.log (res.data.listProduct)) it shows 50 products, how do I change the default value pageLength.
Is there any way to fix this, Or am I doing something wrong. Please advise. Thanks.
Add this into computed =>
paginationOptionsComputed(){
return { enabled: true, perPage: Number(this.pageLength), }
}
And change :pagination-options="paginationOptionsComputed"
Note: actual problem is that vue-good-table expects perPage as number. If you look at the initializePagination method in here you can see this:
if (typeof perPage === 'number') {
this.perPage = perPage;
}

How to prevent vote multiple/fast clicking in Vue exploit?

I have a vote system that allows the users to vote-up or unvote.
The issue is when a user clicks on the vote button multiple times quickly. It causes the number of upvotes to go up and down in a weird way (It goes up 1,2,3 and then down to below 0, probably has to do with the Vote.vue which adds and subtracts 1's from the total votes so when clicked fast enough it causes this weirdness) .
Also, I took care to have unique questions id and user id in the vote table.
I have these simple vote/unvote methods in the QuestionsController:
/**
* Vote a question up.
*/
public function voteUp(Question $question)
{
Auth::user()->votes()->attach($question->id);
}
/**
* Vote a question down.
*/
public function voteDown(Question $question)
{
Auth::user()->votes()->detach($question->id);
}
And maybe more importantly and probably causing the issue is the Vote.vue, as you may see I add/subtract 1 to the total #votes displayed.
methods: {
voteUp(question) {
axios.post('/voteup/'+question)
.then(response => this.isVoted = true, this.votes = this.votes + 1)
.catch(response => console.log(response.data));
},
voteDown(question) {
axios.post('/votedown/'+question)
.then(response => this.isVoted = false, this.votes = this.votes - 1)
.catch(response => console.log(response.data));
}
}
How do I overcome this so users won't exploit this + make it behave more stable?
EDIT: I tried these solutions but it didn't solve the issue:
#1 - I tried #Radu Diță solution:
<vote
:votes="{{ $question->votes()->count() }}"
:question="{{ $question->id }}"
:voted="{{ $question->currentUserVoted() ? 'true' : 'false' }}"
:disabled="voteInAir"
></vote>
and the Vote.vue script:
// resources/js/components/Vote.vue
<template>
<span>
<a href="#" v-if="isVoted" #click.prevent="voteDown(question)">
<i class="fas fa-caret-up fa-3x text-primary vote-effect vote-up-effect"></i>
</a>
<a href="#" v-else #click.prevent="voteUp(question)">
<i class="fas fa-caret-up fa-3x vote-gray vote-effect"></i>
</a>
<span class="vote-count-post "><strong>{{ this.votes }}</strong></span>
</span>
</template>
<script>
export default {
props: ['question', 'voted', 'votes'],
data: function() {
return {
isVoted: '',
voteInAir: false
}
},
mounted() {
this.isVoted = this.isVote ? true : false;
},
computed: {
isVote() {
return this.voted;
},
},
methods: {
voteUp(question) {
this.voteInAir = true;
axios.post('/voteup/'+question)
.then(response => this.isVoted = true, this.votes = this.votes + 1, this.voteInAir = false)
.catch(response => console.log(response.data), this.voteInAir = false);
},
voteDown(question) {
this.voteInAir = true;
axios.post('/votedown/'+question)
.then(response => this.isVoted = false, this.votes = this.votes - 1, this.voteInAir = false)
.catch(response => console.log(response.data), this.voteInAir = false);
}
}
}
</script>
#2 - I also tried #Rijosh solution:
<vote
:votes="{{ $question->votes()->count() }}"
:question="{{ $question->id }}"
:voted="{{ $question->currentUserVoted() ? 'true' : 'false' }}"
></vote>
And this is with time interval delay which didnt work:
<template>
<span>
<a href="#" v-if="isVoted" #click.prevent="voteDown(question)">
<i class="fas fa-caret-up fa-3x text-primary vote-effect vote-up-effect"></i>
</a>
<a href="#" v-else #click.prevent="voteUp(question)">
<i class="fas fa-caret-up fa-3x vote-gray vote-effect"></i>
</a>
<span class="vote-count-post "><strong>{{ this.votes }}</strong></span>
</span>
</template>
<script>
export default {
props: ['question', 'voted', 'votes'],
data: function() {
return {
isVoted: '',
timer: null,
interval: 200,
}
},
mounted() {
this.isVoted = this.isVote ? true : false;
},
computed: {
isVote() {
return this.voted;
},
},
methods: {
voteUp(question) {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
axios.post('/voteup/'+question)
.then(response => this.isVoted = true, this.votes = this.votes + 1)
.catch(response => console.log(response.data));
}, this.interval);
},
voteDown(question) {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
axios.post('/votedown/'+question)
.then(response => this.isVoted = false, this.votes = this.votes - 1)
.catch(response => console.log(response.data));
}, this.interval);
}
}
}
</script>
Since you are only allowing one vote (or that is what i understand) i would not increase the upvotes directly but make kind of a system with booleans kind of like this:
<button v-on:click="activateUpVote">UpVote</button>
<button v-on:click="activateDownVote">DownVote</button>
.
.
.
data(){
return{
upVote: false,
downVote: false,
hasVoted: false
}
},
methods: {
activateUpVote(){
this.upVote = !this.upVote
if(this.upVote && (!this.hasVoted || this.downVote)){
this.downVote = false
this.hasVoted = true
this.vote('/votedown/'+this.question, this.votes + 1)
}else{
this.hasVoted = false
}
},
//downVote same as above but the other way around except for the hasVoted part
vote(route, votes) {
axios.post(route)
.then(response => votes)
.catch(response => console.log(response.data));
},
}
Try this solution
Template
<div>
<button #click="voteUp">UpVote</button>
<button #click="voteDown">DownVote</button>
</div>
Data
data: () => {
return {
timer: null,
interval: 200
}
},
Methods
methods: {
voteUp: function () {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
console.log('up vote')
// your action
}, this.interval);
},
voteDown: function () {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
console.log('down vote')
// your action
}, this.interval);
}
}
I'm using setTimeout and clearTimeout in the methods. This will prevent consecutive actions on the buttons. Continuous clicks will trigger only one action. The time between each click can be controlled using the interval value in the data.
You should disable the button after it was pressed and enabled it again after you get a response. This is usable for any button that you wan't to disable spamming.
In your data add a boolean for each button
data () {
return {
//other data
upVoteInAir: false
}
}
then bind your button based on the flag
<button :disabled="upVoteInAir">Up</button>
update the flag based on the http request
methods: {
voteUp(question) {
this.upVoteInAir = true
axios.post('/voteup/'+question)
.then(response => this.isVoted = true, this.votes = this.votes + 1, this.upVoteInAir = false)
.catch(response => console.log(response.data), this.upVoteInAir = false);
},
}

Vue 2 Imported Component Property or Method not defined

I'm attempting to nest some components - ultimately I would like to have a component which displays Posts, with a PostItem component used to render each post. Inside the PostItem, I want a list of related comments, with CommentItem to render each comment. I have the posts displayed using the PostItem with no errors, but as soon as I add the Comments I get errors. So to simplify, I've pulled the CommentsList component out and I'm just trying to display it on the page, manually loading in all comments - it's an exact copy of PostsList, except with comment replacing post, but it generates an error:
[Vue warn]: Property or method "commment" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
found in
---> <Comment> at resources/assets/js/components/CommentItem.vue
<CommentsList> at resources/assets/js/components/CommentsList.vue
<Root>
CommentsList.vue
<template>
<div class="media-list media-list-divided bg-lighter">
<comment v-for="comment in comments"
:key="comment.id"
:comment="comment">
</comment>
</div>
</template>
<script>
import comment from './CommentItem'
export default {
components: { comment },
data: function() {
return {
comment: {
id: 1,
content: "",
edited: false,
created_at: new Date().toLocaleString(),
user: {
id: 1,
name: '',
}
},
comments: [
{
id: 1,
content: "",
edited: false,
created_at: new Date().toLocaleString(),
user: {
id: 1,
name: '',
}
}
]
};
},
created() {
this.fetchCommentsList();
},
methods: {
fetchCommentsList() {
axios.get('/api/comments').then((res) => {
//alert(JSON.stringify(res.data[0], null, 4));
this.comments = res.data;
});
},
createComment() {
axios.post('api/comments', {content: this.comment.content, user_id: Laravel.userId, vessel_id: Laravel.vesselId })
.then((res) => {
this.comment.content = '';
// this.comment.user_id = Laravel.userId;
// this.task.statuscolor = '#ff0000';
this.edit = false;
this.fetchCommentsList();
})
.catch((err) => console.error(err));
},
deleteComment(id) {
axios.delete('api/comments' + id)
.then((res) => {
this.fetchCommentsList()
})
.catch((err) => console.error(err));
},
}
}
</script>
CommentItem.vue
<template>
<div>
<a class="avatar" href="#">
<img class="avatar avatar-lg" v-bind:src="'/images/user' + comment.user.id + '-160x160.jpg'" alt="...">
</a>
<div class="media-body">
<p>
<strong>{{ commment.user.name }}</strong>
</p>
<p>{{ comment.content }}</p>
</div>
</div>
</template>
<script>
export default {
name: 'comment',
props: {
comment: {
required: true,
type: Object,
default: {
content: "",
id: 1,
user: {
name: "",
id: 1
}
}
}
},
data: function() {
return {
}
}
}
</script>
I'm pretty new to Vue - I've read so many tutorials and have been digging through the documentation and can't seem to figure out why its working for me with my PostsList component thats an exact copy. It also seems odd that I need both comment and comments in the data return - something that my working Posts version requires.
I'll drop in my working Posts components:
PostsList.vue
<template>
<div>
<div v-if='posts.length === 0' class="header">There are no posts yet!</div>
<post v-for="post in posts"
:key="post.id"
:post="post">
</post>
<form action="#" #submit.prevent="createPost()" class="publisher bt-1 border-fade bg-white">
<div class="input-group">
<input v-model="post.content" type="text" name="content" class="form-control publisher-input" autofocus>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">New Post</button>
</span>
</div>
<span class="publisher-btn file-group">
<i class="fa fa-camera file-browser"></i>
<input type="file">
</span>
</form>
</div>
</template>
<script>
// import CommentsManager from './CommentsManager.vue';
import post from './PostItem.vue';
export default {
components: {
post
},
data: function() {
return {
post: {
id: 1,
content: "",
edited: false,
created_at: new Date().toLocaleString(),
user: {
id: 1,
name: '',
}
},
posts: [
{
id: 1,
content: "",
edited: false,
created_at: new Date().toLocaleString(),
user: {
id: 1,
name: '',
}
}
]
};
},
created() {
this.fetchPostsList();
},
methods: {
fetchPostsList() {
axios.get('/api/posts').then((res) => {
//alert(JSON.stringify(res.data[0], null, 4));
this.posts = res.data;
});
},
createPost() {
axios.post('api/posts', {content: this.post.content, user_id: Laravel.userId, vessel_id: Laravel.vesselId })
.then((res) => {
this.post.content = '';
// this.post.user_id = Laravel.userId;
// this.task.statuscolor = '#ff0000';
this.edit = false;
this.fetchPostsList();
})
.catch((err) => console.error(err));
},
deletePost(id) {
axios.delete('api/posts' + id)
.then((res) => {
this.fetchPostsList()
})
.catch((err) => console.error(err));
},
}
}
</script>
PostItem.vue
<template>
<div class="box">
<div class="media bb-1 border-fade">
<img class="avatar avatar-lg" v-bind:src="'/images/user' + post.user.id + '-160x160.jpg'" alt="...">
<div class="media-body">
<p>
<strong>{{ post.user.name }}</strong>
<time class="float-right text-lighter" datetime="2017">24 min ago</time>
</p>
<p><small>Designer</small></p>
</div>
</div>
<div class="box-body bb-1 border-fade">
<p class="lead">{{ post.content }}</p>
<div class="gap-items-4 mt-10">
<a class="text-lighter hover-light" href="#">
<i class="fa fa-thumbs-up mr-1"></i> 0
</a>
<a class="text-lighter hover-light" href="#">
<i class="fa fa-comment mr-1"></i> 0
</a>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'post',
props: {
post: {
required: true,
type: Object,
default: {
content: "",
id: 1,
user: {
name: "",
id: 1
}
}
}
},
data: function() {
return {
}
}
}
</script>

Uncaught TypeError: axios.ad is not a function in vue script

I'm having this error with my vue script, this are the files i'm using
Can't find the error reported with the axios.ad (favoriteAd.vue).
It's basically a favourite feature using:
https://scotch.io/tutorials/implement-a-favoriting-feature-using-laravel-and-vue-js#comments-section
(replacing post with ad)
app.js
require('./bootstrap');
window.Vue = require('vue');
Vue.component('favorite', require('./components/FavoriteAd.vue'));
const app = new Vue({
el: '#favorite'
});
favoriteAd.vue
<template>
<span>
<a href="#" v-if="isFavorited" #click.prevent="unFavorite(ad)">
<i class="fa fa-heart"></i>
</a>
<a href="#" v-else #click.prevent="favorite(ad)">
<i class="fa fa-heart-o"></i>
</a>
</span>
</template>
<script>
export default {
props: ['ad', 'favorited'],
data: function() {
return {
isFavorited: '',
}
},
mounted() {
this.isFavorited = this.isFavorite ? true : false;
},
computed: {
isFavorite() {
return this.favorited;
},
},
methods: {
favorite(ad) {
console.log(ad);
axios.ad('/favorite/'+ad) // this is the error line
.then(response => this.isFavorited = true)
.catch(response => console.log(response.data));
},
unFavorite(ad) {
axios.ad('/unfavorite/'+ad)
.then(response => this.isFavorited = false)
.catch(response => console.log(response.data));
}
}
}
view
#if (Auth::check())
<favorite
:ad={{ $aviso->id }}
:favorited={{ $aviso->favorited() ? 'true' : 'false' }}
></favorite>
#endif
You never included axios in your app. Try npm install axios then require('./axios') on the app.js file. Also, according to the axios reference manual, there's no method named ad.

Resources