Update array items asynchronously - watches not firing - javascript

I have created a component that displays blog article previews. This component has pagination and upon selecting a new page I refresh the array of article previews. The list of articles is fetched from a JSON api from server1. The response contains information to fetch each article from server 2. Then I fire x asynchronous fetches to server 2, as many as items in the first response. In those responses I update the items in the array.
I am new to vue but after some struggling I got this to work. Now I'm trying to add a spinner in the article previews while the separate articles are loading. My idea was to watch in the previewcomponent for an article update and show the spinner depending on that. Unfortunately it doesn't work and now I'm starting to doubt my implementation. I notice that the watch in the preview is not called for every previewcomponent but still every preview is updated and shown correctly. I assume this is because of the messaging system but I don't manage to fix it.
My question is twofold:
Is my implementation a correct way of handling this problem? To get this to work I nicely I need to 'erase' the array because otherwise new articles were 'overwriting' old ones and this was visible.
How can I handle the spinners. Why are the watches not triggered and how can I fix this? In the code below I have some console writes. I see 10 times 'async' and each time a different amount of 'watch', never 10.
The complete code is on github here: Home and ArticlePreview. These are the most relevant parts:
Home:
<template>
<div class="container article">
<div class="row" v-for="(article, index) in articles" :key="index">
<ArticlePreview v-bind:blogEntry="article"></ArticlePreview>
</div>
<b-pagination-nav :use-router="true" :link-gen="generateLink" align="center" :number-of-pages="nofPages" v-model="pageIndex" />
</div>
</template>
data: function ()
{
return {
articles: <BlogEntry[]> [],
nofPages: 1
}
},
loadContent()
{
fetch("./api/v1/articles.php?page=" + this.pageIndex)
.then(response => response.json())
.then((data) =>
{
this.nofPages = Math.ceil(data.nofItems/10);
this.articles.splice(0);
this.articles.splice(data.data.length);
let index :number;
for(index = 0; index < data.data.length; index++)
{
createArticleAsync(data.data[index].name, data.data[index].permlink).then(function(this: any, index: number, article: BlogEntry)
{
console.log('async');
Vue.set(this.articles, index, article);
}.bind(this, index));
}
})
},
ArticlePreview:
<template>
<div class="container-fluid">
<div class="row" v-if="blogEntry">
<template v-if="blogEntry">
<div class="imageframe col-md-3">
<div class="blog-image">
<img :src="blogEntry.previewImage" style="border-radius: 5px;">
</div>
</div>
<div class="col-md-9">
<h5 class="font-weight-bold" style="margin-top:5px;"><router-link :to="{ name: 'Article', params: {author: blogEntry.author, permlink: blogEntry.permlink } }">{{blogEntry.title}}</router-link></h5>
<div class="multiline-ellipsis">
<p>{{blogEntry.previewBody}}</p>
</div>
<span class="metadata"><i>by <a :href="AuthorBlogLink">{{blogEntry.author}}</a> on {{blogEntry.created | formatDate}}</i></span>
</div>
</template>
<template v-else>
<p>Loading</p>
</template>
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import VueRouter from 'vue-router';
import {formatDate} from "../utils/utils";
export default Vue.extend({
props: [
'blogEntry'
],
data: function ()
{
return {
loading: true
}
},
watch:
{
blogEntry(newValue)
{
console.log('watch');
if(newValue)
this.loading = false;
else
this.loading = true;
}
}
});
</script>

I think the method of getting the detailed data of the article should be encapsulated inside the component, and the loading state is also maintained internally.just like the code below:(It doesn't work properly because Mockjs cannot execute correctly in snippet)
Mock.setup({timeout: 2000})
const URL_ARTICLE_LIST = '/api/v1/articles.php'
const URL_ARTICLE_DETAIL = '/api/v1/article_detail.php'
Mock.mock(/\/api\/v1\/articles\.php.*/,function(options){
return {
nofItems: 33,
data: Mock.mock({
'list|10': [{
'title': '#title',
'url': URL_ARTICLE_DETAIL
}]
}).list
}
})
Mock.mock(URL_ARTICLE_DETAIL,function(options){
return Mock.mock({
content: '#paragraph'
})
})
Vue.component('article-card',{
template: `
<div>
<template v-if="!loading">
<div class="article-title">{{articleTitle}}</div>
<div class="article-content">{{article.content}}</div>
</template>
<template v-else>
<div>loading...</div>
</template>
</div>`,
data () {
return {
loading: false,
article: {}
}
},
props: {
articleTitle: {
required: true,
type: String
},
articleUrl: {
required: true,
type: String
}
},
watch: {
articleUrl (url,oldUrl) {
if(url && url!=oldUrl){
this.loadContent()
}
}
},
methods: {
loadContent () {
this.loading = true;
//or your own async functions
axios.get(this.articleUrl).then(res=>{
this.article = res.data
this.loading = false;
})
}
},
created () {
this.loadContent()
}
});
var app = new Vue({
el: '#app',
data () {
return {
articles: [],
nofPages: 1,
page: 1 //you should get page param from vue-router just like this.$route.query.page
}
},
created () {
//you can also use fetch here
axios.get(URL_ARTICLE_LIST+'?page='+this.page).then(res=>{
console.log(res.data)
this.nofPages = Math.ceil(res.data.nofItems/10);
this.articles = res.data.data
})
}
})
ul,li{
list-style: none;
}
.article_list{
display: flex;
flex-wrap: wrap;
}
.article_list>li{
width: 300px;
background: skyblue;
color: white;
margin: 10px;
}
.article-content{
text-indent: 2em;
}
.pagination-wrapper>li{
display:inline-block;
padding: 5px;
border: 1px solid skyblue;
margin: 3px;
}
.pagination-wrapper>li.active{
background: skyblue;
color: #fff;
}
<script src="https://cdn.bootcss.com/Mock.js/1.0.1-beta3/mock-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<div id="app">
<ul class="article_list">
<li v-for="article of articles">
<article-card :article-title="article.title" :article-url="article.url"></article-card>
</li>
</ul>
<ul class="pagination-wrapper">
<li v-for="x in nofPages" :class="{active: page==x}">{{x}}</li>
</ul>
</div>

Related

How to render dynamic data on clicking dynamic link within the same component without refreshing the page in Vue.js

I have a Single Page that renders two component BlogDetailComponent and SidebarComponent and I am passing data using props to the component. BlogDetailComponent renders the blog detail and SidebarComponent renders the Related Blog
In Single Page route I am passing the slug which is dynamic to get the blog details.
Below is my vue-router code
import Vue from "vue";
import VueRouter from "vue-router";
import Single from "../views/Single.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/blog-details/:slug",
name: "blog_details",
component: Single
}
];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
Problem that I am facing -
Whenever I click on the related blog link, the slug in the route changes but does not renders the page with updated blog data for the new slug.
I have implemented using beforeRouteUpdate, but the issue is that I have to call the getBlogDetail() and getRelatedBlogs() in created() as well as beforeRouteUpdate hooks in same page i.e. Single Page which I feel that is not the proper way to code, so any suggestion on how can I acheive this without having to call api two times in Single Page.
Below is my code for the single page
Single Page Code
import axios from 'axios'
import BlogDetailComponent from '#/components/BlogDetailComponent.vue'
import SidebarComponent from '#/components/SidebarComponent.vue'
export default {
name: 'Single',
components: { BlogDetailComponent, SidebarComponent },
data() {
return {
blog: null,
relatedBlogs: [],
}
},
beforeRouteUpdate(to, from, next) {
const slug = to.params.slug
this.getBlogDetail(slug)
this.getRelatedBlogs(slug)
next()
},
created() {
const slug = this.$route.params.slug
this.getBlogDetail(slug)
this.getRelatedBlogs(slug)
},
methods: {
getBlogDetail(slug) {
axios
.get(`${process.env.VUE_APP_BASE_URL}/blog-detail/${slug}`)
.then(response => {
if (response.data) {
this.blog = response.data
}
})
.catch(error => console.log(error))
},
getRelatedBlogs() {
axios
.get(
`${process.env.VUE_APP_BASE_URL}/blog/related/${this.$route.params.slug}`
)
.then(response => {
if (response.data) {
this.relatedBlogs = response.data
}
})
.catch(error => console.log(error))
},
}
}
1. You could trigger the event with a watcher:
const RelatedItems = {
template: `
<div>
Related items:<br />
{{ $attrs }}
</div>
`,
}
const BlogItems = {
template: `
<div>
Blog items:<br />
{{ $attrs }}
</div>
`,
}
const ViewItems = {
components: {
RelatedItems,
BlogItems,
},
data() {
return {
related: {},
blog: {},
}
},
watch: {
'$route.params.slug': {
handler(val) {
if (val) {
this.fetchRelated(val)
this.fetchBlog(val)
}
},
immediate: true,
deep: true,
},
},
methods: {
async fetchRelated(slug) {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${slug}`)
const json = await response.json()
this.related = json
},
async fetchBlog(slug) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${slug}`)
const json = await response.json()
this.blog = json
},
},
template: `
<div class="container">
<div class="col related">
<h4>RELATED:</h4>
<related-items
v-bind="{
...this.related,
}"
/>
</div>
<div class="col blog">
<h4>BLOG:</h4>
<blog-items
v-bind="{
...this.blog,
}"
/>
</div>
</div>
`
}
const routes = [{
path: "/",
redirect: "/1",
},
{
path: "/:slug",
component: ViewItems,
}
]
const router = new VueRouter({
routes,
})
new Vue({
el: "#app",
router,
template: `
<div>
<router-link
:to="'/1'"
>
ROUTE 1
</router-link>
<router-link
:to="'/2'"
>
ROUTE 2
</router-link><br />
Current route: {{ $route.params }}
<hr />
<router-view />
</div>
`
})
html,
body {
padding: 0;
margin: 0;
}
.container {
margin: 0 -16px;
display: flex;
}
.col {
padding: 0 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.col.related {
width: 120px;
background: rgba(0, 0, 0, 0.2)
}
.col.blog {
width: calc(100% - 120px);
}
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app"></div>
2. Using named routes
const RelatedItems = {
// The "slug" prop is the same as the param is called
// in the router! If you want to rename it, look at the
// other component for an example.
props: ["slug"],
data() {
return {
related: {},
}
},
watch: {
// watching the prop - not the $route!
slug: {
handler(val) {
this.fetchRelated(val)
},
immediate: true,
},
},
methods: {
async fetchRelated(slug) {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${slug}`)
const json = await response.json()
this.related = json
},
},
template: `
<div>
Related items:<br />
{{ related }}
</div>
`,
}
const BlogItems = {
// this component awaits a prop called "endpoint" - the
// router provides that with a small twist of re-naming
props: ["endpoint"],
data() {
return {
blog: {},
}
},
watch: {
// watching the prop - not the $route!
endpoint: {
handler(val) {
this.fetchBlog(val)
},
immediate: true,
},
},
methods: {
async fetchBlog(slug) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${slug}`)
const json = await response.json()
this.blog = json
},
},
template: `
<div>
Blog items:<br />
{{ blog }}
</div>
`,
}
const routes = [{
path: "/",
redirect: "/1",
},
{
path: "/:slug",
components: {
blogItems: BlogItems,
relatedItems: RelatedItems,
},
props: {
// renaming the param - just to have an example,
// where the param passed as prop is modified a bit
blogItems: (route) => ({
endpoint: route.params.slug
}),
relatedItems: true,
},
}
]
const router = new VueRouter({
routes,
})
new Vue({
el: "#app",
router,
template: `
<div>
<router-link
:to="'/1'"
>
ROUTE 1
</router-link>
<router-link
:to="'/2'"
>
ROUTE 2
</router-link><br />
Current route: {{ $route.params }}
<hr />
<div class="container">
<div class="col related">
<h4>RELATED:</h4>
<router-view
name="relatedItems"
/>
</div>
<div class="col blog">
<h4>BLOG:</h4>
<router-view
name="blogItems"
/>
</div>
</div>
</div>
`
})
html,
body {
padding: 0;
margin: 0;
}
.container {
margin: 0 -16px;
display: flex;
}
.col {
padding: 0 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.col.related {
width: 120px;
background: rgba(0, 0, 0, 0.2)
}
.col.blog {
width: calc(100% - 120px);
}
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app"></div>
This one is maybe a bit better if you have further plans with these components:
visuals & data is decoupled more,
the related & blog components are more encapsulated (thus easier to change),
by using props -> true in the router, the components are easier to control from the outside, easier to reuse
3. CONCLUSION
There are different ways to provide the same user experience - choose one, that suits your future plans:
if this is a part of your app that won't change much and/or is not that central, then I think the first is easier to see through & maintain (remember what is happening)
if these parts of your app might change more frequently (but the layout doesn't) and/or you'd like to build on these parts as more complex components, then I suggest the second.
I'm sure, that there are other solutions to your problem, but the baseline could be this: the router is pretty versatile. Using it with SFCs it can do virtually anything. :)

Vue multiple components and access to Vuex properties

I'm learning Vue, using it with Vuex (without Webpack), but I have several questions when implementing this simple example, it's not clear for me in the docs.
Don't know why, but, I can't access the Vuex store using this pointer inside component computed property, for example:
this.$store.state.nav.title, leading me to use global app variable instead. Also, this.$parent and $root do not work.
Is it correct to initialize multiple Vue components at one time such as this, and shouldn't they have been mounted automatically when I pass components property to the Vue construct object? What is the right way to initialize, for example, the header, footer and body components at the same time?
var app = new Vue({
el: document.getElementById('app'),
data: {
title:store.state.nav.title
},
computed: {},
methods:{},
mounted:function(){},
updated:function(){},
store:store,
components:{
componentheader,
componentnavbar,
componentbody,
componentfooter
}
});
for (var companent_name in app.$root.$options.components) {
if(typeof app.$root.$options.components[companent_name] === 'function') {
var MyComponent = Vue.extend(app.$root.$options.components[companent_name]);
var component = new MyComponent().$mount();
document.getElementById('app').appendChild(component.$el);
}
}
Here is the full example:
var store = new Vuex.Store({
state: {
nav: {
title: 'my site'
}
},
mutations: {
changeTitle: function(t, a) {
this.state.nav.title = a;
}
}
});
var componentheader = Vue.component('componentheader', {
computed: {
title() {
return app.$store.state.nav.title
}
},
template: '#header_tpl',
mounted: function() {},
updated: function() {}
});
var componentnavbar = Vue.component('componentnavbar', {
computed: {
title() {
return app.$store.state.nav.title
}
},
template: '#navbar_tpl',
mounted: function() {},
updated: function() {}
});
var componentbody = Vue.component('componentbody', {
computed: {
title() {
return app.$store.state.nav.title
}
},
template: '#body_tpl',
mounted: function() {},
updated: function() {}
});
var componentfooter = Vue.component('componentfooter', {
computed: {
title() {
return app.$store.state.nav.title
}
},
template: '#footer_tpl',
mounted: function() {},
updated: function() {}
});
var app = new Vue({
el: document.getElementById('app'),
data: {
title: store.state.nav.title
},
computed: {},
methods: {},
mounted: function() {},
updated: function() {},
store: store,
components: {
componentheader,
componentnavbar,
componentbody,
componentfooter
}
});
Vue.use(Vuex);
for (var companent_name in app.$root.$options.components) {
if (typeof app.$root.$options.components[companent_name] === 'function') {
var MyComponent = Vue.extend(app.$root.$options.components[companent_name]);
var component = new MyComponent().$mount();
document.getElementById('app').appendChild(component.$el);
}
}
Vue.config.devtools = false;
Vue.config.productionTip = false;
* {
margin: 0;
padding: 0;
color: #fff;
text-align: center;
font-size: 19px;
}
html,
body,
.container {
height: 100%;
}
#app {
position: relative;
height: 100%;
min-height: 100%;
}
header {
width: 100%;
height: 80px;
}
nav.navbar {
box-sizing: border-box;
min-height: 100%;
padding-bottom: 90px;
width: 80px;
height: 100%;
position: absolute;
}
.container {
box-sizing: border-box;
min-height: 100%;
padding-bottom: 90px;
color: #000;
}
footer {
height: 80px;
margin-top: -80px;
}
footer,
nav,
header {
background: #000;
}
header div,
footer div {
padding: 15px;
}
nav ul {
list-style-type: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.5.1/vuex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="app">
</div>
<script type="text/x-template" id="header_tpl">
<header class="header">
<div>
header {{ title }}
</div>
</header>
</script>
<script type="text/x-template" id="navbar_tpl">
<nav class="navbar">
<ul>
<li>navbar {{ title }}</li>
</ul>
</nav>
</script>
<script type="text/x-template" id="body_tpl">
<div class="container">
<div>
body {{ title }}
</div>
</div>
</script>
<script type="text/x-template" id="footer_tpl">
<footer class="footer">
<div>
footer {{ title }}
</div>
</footer>
</script>
</body>
</html>
You seem confused about Vue Instance and Vue Component. Basically you only need just one Vue instance with multiple components to create your app.
To answer your first question, it does not work because you didn't install the store to each Vue instance that you are created (you only install just 1 instance called app).
for (var companent_name in app.$root.$options.components) {
if (typeof app.$root.$options.components[companent_name] === 'function') {
var MyComponent = Vue.extend(app.$root.$options.components[companent_name]);
var component = new MyComponent({
store // <-- install here
}).$mount();
document.getElementById('app').appendChild(component.$el);
}
}
Working example here
Actually you can just use store since all store and app.$store and this.$store is the same object. The advantage of this.$store is you have no need to import store to every component file for Single File Components.
To answer your second question,
You are mixing about Global Registration and Local Registration. You should use only one for a component.
For render components you can define your template inside <div id="app"> just like:
<div id="app">
<componentheader></componentheader>
<componentnavbar></componentnavbar>
<componentbody></componentbody>
<componentfooter></componentfooter>
</div>
Working example here

Onload method when the page is loading VUE

I want to call the method when the page is loading. So far I call the method when I click, but when I try to use mounted to automatically call the method, it keeps failing, do you have some suggestion?
Below is the code:
<template>
<div>
<AppHeader></AppHeader>
<div style="display: flex">
<ul>
<li
style="cursor: pointer"
v-for="exchange of profileExchanges"
:key="exchange.id"
#click="getImagesByExchange(exchange.id)"
>
<div style="padding: 20px; border: solid 2px red">
<h3>Brand: {{exchange.brand}}</h3>
<p>Description: {{exchange.description}}</p>
<p>Category: {{exchange.category}}</p>
</div>
</li>
</ul>
<div>
<span style="margin: 10px" v-for="url of exchangeImageUrls" :key="url">
<img width="250px" :src="url" />
</span>
</div>
</div>
<br />
</div>
</template>
<script>
import AppHeader from '#/components/Header5'
export default {
components: {
AppHeader
},
created() {
this.$store.dispatch('exchange/getExchangesByProfile', this.$store.state.auth.user.profile.user)
},
data() {
return {
selectedExchangeId: '',
exchanges: []
}
},
computed: {
user() {
return this.$store.state.auth.user
},
profile() {
return this.user.profile || {}
},
profileExchanges() {
return this.$store.getters['exchange/profileExchanges']
},
exchangeImageUrls() {
return this.$store.getters['exchange/imageUrls']
}
},
methods: {
getImagesByExchange(exchangeId) {
this.$store.dispatch('exchange/getImagesByExchange', exchangeId)
},
getListings() {
},
updateProfile(profile, closeModal) {
this.$store.dispatch('auth/updateProfile', profile)
.then(_ => {
closeModal()
})
}
}
}
</script>
I try to put mounted like this
mounted: function() {
this.getImagesByExchange() // Calls the method before page loads
},
But it keeps failing. I guess the problem is how to access the key, but not sure.
If the selectedExchangeId is already not set, you could get the first element of the profileExchanges array and pass the id to the getImagesByExchange function:
mounted() {
this.getImagesByExchange(this.profileExchanges[0].id) // Calls the method before page loads
},
EDIT
Looking again at your code one could suppose that, at the moment the component is mounted the profileExchanges property might not yet be set. One way to handle this is to call both of them inside the created like this:
async created() {
await this.$store.dispatch(
'exchange/getExchangesByProfile',
this.$store.state.auth.user.profile.user
);
if (this.profileExchanges && this.profileExchanges.length) {
this.getImagesByExchange(this.profileExchanges[0].id);
}
}

Vue.js : Why I can't bind external property to child props component?

I have a question about Vue.js, I'm stuck on something tricky I guess.
I cannot bind passed property as component property to do some stuff on this data, here is my code, the issue is affecting Plot component.
Here is my dashboard component which is the parent :
<template>
<div>
<div v-if="!hasError && countryData">
<div v-if="loading" id="dashboard" class="ui two column grid sdg-dashboard sdg-text-center active loader">
</div>
<div v-else id="dashboard" class="ui two column grid sdg-dashboard">
<div class="column sdg-text-center">
<MapDisplay :country="countryData" :latitude="latData" :longitude="lonData"/>
</div>
<div class="column">
<TopicSelector v-on:topicSelectorToParent="onTopicSelection" :goals="goalsData"/>
</div>
<div class="two segment ui column row sdg-text-center">
<Plot :topic-selection-data="topicSelectionData"/>
</div>
</div>
<sui-divider horizontal><h1>{{ countryData }}</h1></sui-divider>
</div>
<div v-else>
<NotFound :error-type="pageNotFound"/>
</div>
</div>
</template>
<script>
import NotFound from '#/views/NotFound.vue';
import MapDisplay from '#/components/dashboard/MapDisplay.vue';
import TopicSelector from '#/components/dashboard/TopicSelector.vue';
import Plot from '#/components/dashboard/Plot.vue';
const axios = require('axios');
export default {
name: 'Dashboard',
components: {
NotFound,
MapDisplay,
TopicSelector,
Plot
},
props: {
countryCode: String
},
data: function() {
return {
loading: true,
hasError: false,
country: this.countryCode,
//Country, lat, lon
countryData: null,
latData: null,
lonData: null,
//Goals provided to Topic Selector
goalsData: null,
//Selected topic provided by Topic Selector
topicSelection: null,
//Topic Data provided to Plot component
topicData: null, //All topic data
topicSelectionData: null,
pageNotFound: "Error 500 : Cannot connect get remote data."
}
},
created: function() {
const api = process.env.VUE_APP_SDG_API_PROTOCOL + "://" + process.env.VUE_APP_SDG_API_DOMAIN + ":" + process.env.VUE_APP_SDG_API_PORT + process.env.VUE_APP_SDG_API_ROUTE;
axios.get(api + "/countrycode/" + this.countryCode)
.then(response => {
this.countryData = response.data.data.country;
this.latData = response.data.data.coordinates.latitude;
this.lonData = response.data.data.coordinates.longitude;
this.goalsData = response.data.data.goals.map(goal => {
return {
goal_code: goal["goal code"],
goal_description: goal["goal description"]
}
});
this.topicData = response.data.data.goals;
})
.catch(() => this.hasError = true)
.finally(() => this.loading = false);
},
methods: {
onTopicSelection: function(topic) {
this.topicSelection = topic;
this.topicSelectionData = this.topicData.filter(goal => this.topicSelection.includes(goal["goal code"]));
}
}
}
</script>
<style scoped lang="scss">
#dashboard {
margin-bottom: 3.1vh;
margin-left: 1vw;
margin-right: 1vw;
}
</style>
Here is the Plot component which his the child :
<template>
<div id="plot">
topic data : {{ topicData }}<br>
topicSelectionData : {{ topicSelectionData }}
</div>
</template>
<script>
export default {
name: 'Plot',
props: {
topicSelectionData: Array
},
data: function() {
return {
topicData: this.topicSelectionData //This line is not working =(
}
}
}
</script>
<style scoped lang="scss">
</style>
I can see my data in {{ topicSelectionData }} but when I bind it to topicData, I cannot retrieve the data using {{ topicData }} or doing some stuff inside a method using topicData as input.
Could you provide me some help ?
Regards
You need to assign the value on mounted as follows:
data() {
return {
topicData: ''
}
},
mounted(){
this.topicData = this.topicSelectionData;
}
And in order to update the child when the value changes in the parent:
watch: {
topicSelectionData: function(newValue, oldValue) {
this.topicData = newValue;
}
},

Adding #click or router via filter

I get the post content with REST api, stripped of all html tags.
{'post': {'Content': 'test content with #hashtag'}}
I create hashtags from the content with filter and regex.
<div v-html="$options.filters.customFilter(post.Content)"></div>
(...)
methods:
ale(tag) {
alert('asdf')
}
customFilter(val) {
val = val.replace(/#(\S*)/g, '<a #click="ale(\'asdf\')" href="http://localhost:8080/tag/$1">$1</a>')
return val
}
The problems is that this filter will create html link and force whole web reload (and not the router way)
I tried doing #click but vue doesn't interpret it (is plain text #click in dom)
How can i trigger router or #click from filter?
If i can't is there other way to do this??
EDIT
The full code is
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<li v-bind:key="index" v-for="(post, index) in posts">
<div v-html="$options.filters.customFilter(post.Content)"></div>
</li>
</div>
</template>
<script>
export default {
name: 'Posta',
data() {
return {
msg: 'Welcome to Your Vue.js App POST',
posts: []
}
},
methods: {
ale() {
alert('asdf')
}
},
mounted() {
this.posts = [{Content: 'test with #hashtag'}]
},
filters: {
customFilter(val) {
// ...
val = val.replace(/#(\S*)/g, '<a #click="ale(\'asdf\')" href="http://localhost:8080/tag/$1">$1</a>')
return val
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1,
h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
/*display: inline-block;*/
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
instead of using plain <a> tag
you must use <router-link :to="dynamicValue">
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<li v-for="(post, index) in posts" v-bind:key="index">
<router-link :to="getDynamicUrl(post.Content)">{{post.something}}</router-link>
</li>
</div>
</template>
<script>
export default {
name: 'Posta',
data() {
return {
msg: 'Welcome to Your Vue.js App POST',
posts: []
}
},
methods: {
ale() {
alert('asdf')
},
getDynamicUrl(val) {
return val; //somethingWith val
},
},
mounted() {
this.posts = [{Content: 'test with #hashtag'}]
},
}
</script>
Use #click.prevent to prevent the default action of a hard load of the page: https://v2.vuejs.org/v2/guide/events.html#Event-Modifiers
Then use Vue Router push or some other programmatic navigation to change vue routes: https://router.vuejs.org/en/essentials/navigation.html

Categories

Resources