i have a '/posts' route. This 'posts' route has a created() function which fetches data from an API via GET and outputs the data to the page.
Now I have a navbar which is included on every page. This navbar now has an input field where I can search certain posts by tags. This tag based search function is already working and runs via POST to an api.
Now the problem:
I write some tags into input field in the navigation and search for them. If I'm currently not at the posts route, the search works fine and I get directed to the posts route and see the tag related posts.
If I write some tags in the navbar input field and press the search button, WHILE i'm already on the posts route, nothing happens.
So if I'm in any other route then '/posts', the tag based search works great.
Thats why I think, the problem is, that I'm already on the '/posts' route. But it should also work this way! So I need something like a route link that replaces/refresh the route content?
Here is my code:
Relevant part of my navbar component:
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<router-link to="/posts" class="nav-link">Posts</router-link>
</li>
</ul>
<form class="form-inline my-2 my-lg-0">
<div v-for="(tag, index) in tags" class="ml-sm-2">
<h6><span class="badge badge-light" #click="removeSearchTags(index)">{{ tag }}</span></h6>
</div>
<input class="form-control ml-1 mr-sm-2" type="text" v-model="tag" v-on:keyup.enter="pushToTags"
placeholder="Search Gaming, Youtube, DrunkSlut" aria-label="Search">
<router-link :to="{name: 'posts', params: { searchTags: tags }}" reload>
<button type="button" v-if="this.tags.length > 0"
class="btn btn-outline-light my-2 my-sm-0">Search
</button>
</router-link>
</form>
Whole posts component logic:
<script>
export default {
name: "posts",
data: function () {
return {
apiUrl: '/getPosts',
posts: '',
submitted: false,
first_page_url: '',
last_page_url: '',
current_page_url: '',
next_page_url: '',
prev_page_url: '',
lastPage: '',
current_page: '',
tags: [],
}
},
methods: {
getPosts: function (url) {
this.$http.get(url).then(function (data) {
this.posts = data.body.data;
this.first_page_url = data.body.first_page_url;
this.last_page_url = data.body.last_page_url;
this.next_page_url = data.body.next_page_url;
this.current_page = data.body.current_page;
this.prev_page_url = data.body.prev_page_url;
this.lastPage = data.body.last_page;
this.current_page_url = '/getPosts?page=' + this.current_page;
});
},
getPostByTags: function (url, tags) {
this.$http.post(url, {
tags: tags
}).then(function (data) {
this.posts = data.body.data;
this.first_page_url = data.body.first_page_url;
this.last_page_url = data.body.last_page_url;
this.next_page_url = data.body.next_page_url;
this.current_page = data.body.current_page;
this.prev_page_url = data.body.prev_page_url;
this.lastPage = data.body.last_page;
this.current_page_url = '/getPostByTags?page=' + this.current_page;
});
},
},
computed: {},
created() {
if (!this.$route.params.searchTags) {
this.getPosts(this.apiUrl);
} else {
this.getPostByTags('/getPostByTags', this.$route.params.searchTags);
}
},
}
</script>
The Main html file, where vueJS starts. There is only the navbar component, thats how it's included on any other route.
<div id="app">
<navbar></navbar>
<router-view></router-view>
</div>
Try to make your created as a watch
Something like:
watch: {
'$route.params.searchTags': {
deep: true,
immediate: true, // this triggers the watch on component creation, so you can remove the created hook content
handler() {
if (!this.$route.params.searchTags) {
this.getPosts(this.apiUrl);
} else {
this.getPostByTags('/getPostByTags', this.$route.params.searchTags);
}
}
}
}
Related
I have a nuxt/vue app using apollo to query WPGraphQL on Wordpress. Im having hard time setting up my nuxt-link on my index page to route to my _slug.vue page. If I manually enter the url on the browser using the post's slug, I am able to render the data I want. In my index page, how do I use the post.slug with params to get my _slug.vue page to render?
This is my GraphQL Query:
post(id: $slug, idType: SLUG) {
title
slug
date
id
content(format: RENDERED)
author {
node {
firstName
lastName
}
}
}
}
My /blog/index.vue page has the list of blog posts and I am trying to use nuxt-link to link each post to render _slug.vue:
<template>
<div class="blog">
<h1 class="blog__title">Blog</h1>
<nuxt-link
v-for="post in posts.nodes"
:key="post.slug"
:to="'blog/' + { params: { slug: post.slug } }"
class="blog__post"
>
<h3 class="blog__post-title">
{{ post.title }}
</h3>
<div class="blog__post-content" v-html="post.content" />
</nuxt-link>
</div>
</template>
<script>
import getPostByID from '~/apollo/queries/GetPostByID'
export default {
data() {
return {
posts: [],
query: ''
}
},
apollo: {
posts: {
prefetch: true,
query: getPostByID,
update: (data) => data.post
}
}
</script>
With my _slug.vue file below, It uses the same query as my blog page and is able to render if I type the proper url with slug on the browser:
<template>
<article class="post">
<h1>{{ post.title }}</h1>
<div class="blog__post-content" v-html="post.content" />
</article>
</template>
<script>
import GetPostByID from '~/apollo/queries/GetPostById'
export default {
data() {
return {
post: []
}
},
apollo: {
post: {
prefetch: true,
query: GetPostByID,
variables() {
return { slug: this.$route.params.slug }
}
}
}
}
</script>
And what exactly does ".slug" refer to from "this.$route.params.slug"?
If your index page is correctly displaying the list of posts, then you just need to adjust the url slightly.
<nuxt-link
v-for="post in posts.nodes"
:key="post.slug"
:to="'blog/' + { params: { slug: post.slug } }"
class="blog__post"
>
Should be:
<nuxt-link
v-for="post in posts.nodes"
:key="post.slug"
:to="`blog/${post.slug}`"
class="blog__post"
>
this.$route.params.slug refers to the url parameter you named by creating the dynamic file _slug.vue. So if you have pages/blog/_slug.vue and navigate to your-app.com/blog/my-first-post, my-first-post is the parameter string you get back when accessing this.$route.params.slug.
Slug isn’t a magical keyword, it could be anything instead and depends on the file name you create in your blog directory. Given the same url and pages/blog/_unicorn.vue, you would call this.$route.params.unicorn to return my-first-post.
I am very new with vuejs and recently started to try to replace some old jquery code that I have and make it reactive with vuejs. The thing is I have a component that gets information from a nodejs server via socket.io asynchronously.
When I get the data and update my component's data I see the changes when I console log it but it does not change the DOM the way I want it to do.
What is the proper way to grab data asynchronously and use it inside a component? I post some parts of my code so you can see it. I will appreciate any advice you can give me. Thanks in advance!
Vue.component('chat', {
data() {
return {
chat: null,
commands: [],
chatOpened: false,
}
},
props: [
'io',
'messages',
'channels',
'connectChat',
'roomChat',
'user',
'userId',
'soundRoute',
],
methods: {
openChat() {
this.chatOpened = true;
},
closeChat() {
this.chatOpened = false;
},
},
created() {
this.chat = this.$io.connect(this.connectChat);
this.commands.push('clear');
let self = this;
$.each(this.channels, function(index, value) {
self.chat.emit('join', {room: index, user: self.user, userId: self.userId}, function(err, cb) {
if (!err) {
users = cb.users;
messages = cb.messages;
if (messages.length > 0) {
self.channels[index].loaded = true;
}
//some more code
}
});
});
console.log(this.channels);
},
template: `
<div>
<div id="container-chat-open-button" #click="openChat" :class="{hide : chatOpened}">
<div>+90</div>
<i class="fas fa-comment-alt"></i>
</div>
<div id="container-chat" class="chat__container" :class="{open : chatOpened}">
<div id="container-chat-close-button" #click="closeChat">
<span>
<div>
<i class="fas fa-comment-alt"></i>
#{{ messages.chat_lobby_icon_title }}
</div>
<i class="icon-arrowdown"></i>
</span>
</div>
<div id="alert-chat" class="chat__container-notifications animated flash"></div>
<div class="row">
<ul>
<li v-for="channel in channels" v-show="channel.loaded === true">Channel loaded</li>
</ul>
</div>
</div>
</div>
`
});
I would expect to see the list of channels with messsages but instead I don't see the list even thought I see my channels with the loaded attribute set to true (by default they all have this attribute set to false).
My guess is that it's this part that is not working as expected.
if (messages.length > 0) {
self.channels[index].loaded = true;
}
The reactive way of doing this is by setting the full object again.
Vue.set(self.channels, index, {
...self.channels[index],
loaded: true
}
EDIT 1:
this.channels.forEach((channel) => {
this.chat.emit('join', {room: index, user: self.user, userId: self.userId}, (err, cb) => {
if (!err) {
users = cb.users;
messages = cb.messages;
if (messages.length > 0) {
Vue.set(self.channels, index, {
...self.channels[index],
loaded: true
}
}
//some more code
}
});
})
You'll need to add support for the rest-spread-operator using babel.
<span #click="showModal = $event.target.innerHtml>Tag 1</span>
<span #click="showModal = $event.target.innerHtml>Tag 2</span>
<span #click="showModal = $event.target.innerHtml>Tag 3</span>
Clicking in any of the 3 spans will make this.showModal to have the value of each of the span content elements. But this code looks repetitive and unnecessary. I know I can create a component with v-for and have the data for the span contents somewhere else, but I want to know how to do this for very specific reasons. I'd like to have this:
<span>Tag 1</span>
<span>Tag 2</span>
<span>Tag 3</span>
And a function, e.g. in the hook mounted() of the component, that adds the v-on directive for click to each one of them.
Can you help me?
Thanks.
You could try something like this:
<template>
<span v-for="tag in tags" #click="showModal(tag)" v-text="tag"></span>
</template>
<script>
export default {
data() {
return {
tags: ['Tag 1', 'Tag 2', 'Tag 3']
}
},
methods: {
showModal(tag) {
console.log("Showing modal for tag:", tag)
}
}
}
</script>
Hope this helps!
You can add a method which is called on clicks that reads the element's HTML content.
The template:
<span #click="doStuff">Tag 1</span>
<span #click="doStuff">Tag 2</span>
<span #click="doStuff">Tag 3</span>
The method:
doStuff(e) {
this.showModal = e.target.innerHTML
}
You could set up a method to call when the tag is clicked and pass the id of the tag that was clicked through to handle appropriately.
Assuming that you have an array of the tag text:
data: function() {
return {
tagTotal: ['Tag 1', 'Tag 2', 'Tag 3'];
}
}
Then in the HTML section:
<span v-for="tag in tagTotal" #click="methodToCall(tag)">
{{ tag }}
</span>
Then in your mounted, methods, or created section you could add:
mounted: {
methodToCall: function(tag) {
showModal = tag;
// or 'this.showModal = tag' if showModal is a part of the componenet.
}
}
I've finally added the listeners manually with vanilla js, in order to save code:
mounted: function() {
let spans = document.querySelectorAll('span');
spans.forEach(el => {
el.addEventListener('click', this.clickTag);
})
}
methods: {
clickTag(event) { this.showModal = event.target.innerHTML }
}
It's important not using an arrow function for mounted because otherwise it won't bind the vue instance for this.
Thanks for your answers.
If direct-process Dom elements, custom directive will be one option.
Vue.config.productionTip = false
let vMyDirective = {}
vMyDirective.install = function install (_Vue) {
_Vue.directive('my-directive', {
inserted: function (el, binding, vnode) {
el.addEventListener('click', () => {
_Vue.set(vnode.context, binding.value.model, el.innerHTML)
}, false)
}
})
}
Vue.use(vMyDirective)
new Vue({
el: '#app',
data() {
return {
testValues: ['label a', 'label b'],
showModal: 'nothing!!!'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<h2>showModal: {{showModal}}</h2>
<div>
<p v-for="(item, index) in testValues" v-my-directive="{'model': 'showModal'}">Test:<span>{{item}}</span></p>
</div>
</div>
I work with single file components and have a list in one of them. This list should work like a accordion, but as far as I can find in the Vuejs docs, it's not that easy to make each item open separately very easily. The data (questions and answers) is retrieved from an ajax call. I use jQuery for that, but would like to know how I can make the accordion work Vuejs-style. Any help would be appreciated!
Here's the code:
export default {
name: 'faq-component',
props: ['faqid', 'faqserviceurl', 'ctx'],
data: function () {
return {
showFaq: "",
totalFaqs: this.data,
isOpen: true
}
},
watch: {
'showFaq': function(val, faqid, faqserviceurl) {
var self = this;
$.ajax ({
url: this.faqserviceurl,
type: 'GET',
data: {id: this.faqid, q: val, scope:1},
success: function (data) {
self.totalFaqs = data;
},
error: function () {
$("#answer").html('Sorry');
}
});
}
},
methods: {
'toggle': function() {
this.isOpen = !this.isOpen
}
}
}
<template>
<div class="card faq-block">
<div class="card-block">
<form>
<div class="form-group">
<input class="form-control" type="text" placeholder="Your question" id="faq" v-model="showFaq">
</div>
</form>
<div id="answer"></div>
<ul class="faq">
<li v-for="faq in totalFaqs">
<p class="question" v-html="faq.vraag" v-bind:class={open:isOpen} #click="isOpen = !isOpen"></p>
<p class="answer" v-html="faq.antwoord"></p>
</li>
</ul>
</div>
</div>
</template>
Add an isOpen property to each object in totalFaqs and use that instead of your single isOpen property in data.
<p class="question" v-html="faq.vraag" v-bind:class={open: faq.isOpen} #click="faq.isOpen = !faq.isOpen"></p>
If you can't change the model from the server side, then add it client side.
success: function (data) {
data.forEach(d => self.$set(d, 'isOpen', false))
self.totalFaqs = data
}
I work with single file components and have a list in one of them. This list should work like a accordion, but as far as I can find in the Vuejs docs, it's not that easy to make each item open separately very easily. The data (questions and answers) is retrieved from an ajax call. I use jQuery for that, but would like to know how I can make the accordion work Vuejs-style. Any help would be appreciated!
Here's the code:
export default {
name: 'faq-component',
props: ['faqid', 'faqserviceurl', 'ctx'],
data: function () {
return {
showFaq: "",
totalFaqs: this.data,
isOpen: true
}
},
watch: {
'showFaq': function(val, faqid, faqserviceurl) {
var self = this;
$.ajax ({
url: this.faqserviceurl,
type: 'GET',
data: {id: this.faqid, q: val, scope:1},
success: function (data) {
self.totalFaqs = data;
},
error: function () {
$("#answer").html('Sorry');
}
});
}
},
methods: {
'toggle': function() {
this.isOpen = !this.isOpen
}
}
}
<template>
<div class="card faq-block">
<div class="card-block">
<form>
<div class="form-group">
<input class="form-control" type="text" placeholder="Your question" id="faq" v-model="showFaq">
</div>
</form>
<div id="answer"></div>
<ul class="faq">
<li v-for="faq in totalFaqs">
<p class="question" v-html="faq.vraag" v-bind:class={open:isOpen} #click="isOpen = !isOpen"></p>
<p class="answer" v-html="faq.antwoord"></p>
</li>
</ul>
</div>
</div>
</template>
Add an isOpen property to each object in totalFaqs and use that instead of your single isOpen property in data.
<p class="question" v-html="faq.vraag" v-bind:class={open: faq.isOpen} #click="faq.isOpen = !faq.isOpen"></p>
If you can't change the model from the server side, then add it client side.
success: function (data) {
data.forEach(d => self.$set(d, 'isOpen', false))
self.totalFaqs = data
}