Using ref() not reacting to reassignments - composition API - javascript

Here is a simple carousel:
<template>
<div ref="wrapperRef" class="js-carousel container">
<div class="row">
<slot></slot>
</div>
<div class="row">
<template v-for="n in noOfSlides" :key="n">
<span style="margin-right: 25px;" #click="console.log(n)">O</span>
</template>
</div>
</div>
</template>
This works using Options API. The noOfSlides is properly getting changed and the for-loop re-renders after mounting
export default {
name: 'carousel',
data() {
return {
noOfSlides: 0
}
},
mounted(){
this.noOfSlides = this.$refs.wrapperRef.querySelectorAll('.js-carousel-item').length;
}
}
It doesn't work while using composition API like below. The noOfSlides is not getting changed and thus for-loop not re-rendering
export default {
name: 'carousel',
setup() {
const wrapperRef = ref(null);
let noOfSlides = ref(0);
onMounted(function () {
noOfSlides = wrapperRef.value.querySelectorAll('.js-carousel-item').length;// this here properly returning the number of slides(more than 0 anyway)
})
return {
wrapperRef,
noOfSlides
}
}
}
What do I miss here?

This is not the Vue way to work with DOM, you could use slots :
export default {
name: 'carousel',
setup(props, { slots }) {
const wrapperRef = ref(null);
let noOfSlides = ref(0);
onMounted(function () {
console.log('slots ', slots.default());
noOfSlides.value = slots.default().length;
});
function log(n) {
console.log('n ', n);
}
return {
wrapperRef,
noOfSlides,
log,
};
},
};
DEMO

Related

Vue not reacting to a computed props change

I am using the Vue composition API in one of my components and am having some trouble getting a component to show the correct rendered value from a computed prop change. It seems that if I feed the prop directly into the components render it reacts as it should but when I pass it through a computed property it does not.
I am not sure why this is as I would have expected it to be reactive in the computed property too?
Here is my code:
App.vue
<template>
<div id="app">
<Tester :testNumber="testNumber" />
</div>
</template>
<script>
import Tester from "./components/Tester";
export default {
name: "App",
components: {
Tester,
},
data() {
return {
testNumber: 1,
};
},
mounted() {
setTimeout(() => {
this.testNumber = 2;
}, 2000);
},
};
</script>
Tester.vue
<template>
<div>
<p>Here is the number straight from the props: {{ testNumber }}</p>
<p>
Here is the number when it goes through computed (does not update):
{{ testNumberComputed }}
</p>
</div>
</template>
<script>
import { computed } from "#vue/composition-api";
export default {
props: {
testNumber: {
type: Number,
required: true,
},
},
setup({ testNumber }) {
return {
testNumberComputed: computed(() => {
return testNumber;
}),
};
},
};
</script>
Here is a working codesandbox:
https://codesandbox.io/s/vue-composition-api-example-forked-l4xpo?file=/src/components/Tester.vue
I know I could use a watcher but I would like to avoid that if I can as it's cleaner the current way I have it
Don't destruct the prop in order to keep its reactivity setup({ testNumber }) :
setup(props) {
return {
testNumberComputed: computed(() => {
return props.testNumber;
}),
};
}

Passing vue.js store data to a event click handler

I am using regular Vue.js in my project. I store data in a store made from scratch and use it in a template:
<template>
<div>
<div class="row">
<div v-for="(picture, index) in storeState.pictures"
:key="index"
class="col-md-2 my-auto">
<div >
<img class="img-thumbnail img-responsive"
:src="picture.url"
#click="deleteMe">
</div>
</div>
</div>
</div>
</template>
<script>
import { store } from "../common/store.js"
export default {
name:"PictureUpload",
data() {
return {
storeState: store.state,
};
},
methods: {
deleteMe() {
let apiUrl = this.picture.url
console.log(apiUrl)
}
}
}
</script>
My pictures are rendering well but now I want to add a delete() function to the picture #click and whenever I click on the button I get:
TypeError: Cannot read property 'url' of undefined
So how can I access my picture data inside my method?
You should pass picture as parameter in the click handler :
#click="deleteMe(picture)">
and refer to it in the method like :
methods: {
deleteMe(picture) {
let apiUrl = picture.url //omit this
console.log(apiUrl)
}
}
the storeState should be a computed property :
export default {
name:"PictureUpload",
data() {
return {
};
},
computed:{
storeState(){
return store.state;
}
},
methods: {
deleteMe(picture) {
let apiUrl = picture.url
console.log(apiUrl)
}
}
}

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;
}
},

Problems with data communication between components

I had a page on which there was a header with an input that was a search engine, a list of posts, and pagination. I decided to move the header from this file to a separate component in a separate vue file. After I did this, the search for posts by title stopped working, and I can’t add a post now either. I think that I need to import my posts into a new file for my newly created component but how to do it.
My code when it worked(before my changes)
My code is not working after the changes:
The file in which my posts situated:
<template>
<div class="app">
<ul>
<li v-for="(post, index) in paginatedData" class="post" :key="index">
<router-link :to="{ name: 'detail', params: {id: post.id, title: post.title, body: post.body} }">
<img src="src/assets/nature.jpg">
<p class="boldText"> {{ post.title }}</p>
</router-link>
<p> {{ post.body }}</p>
</li>
</ul>
<div class="allpagination">
<button type="button" #click="page -=1" v-if="page > 0" class="prev"><<</button>
<div class="pagin">
<button class="item"
v-for="n in evenPosts"
:key="n.id"
v-bind:class="{'selected': current === n.id}"
#click="page=n-1">{{ n }} </button>
</div>
<button type="button" #click="page +=1" class="next" v-if="page < evenPosts-1">>></button>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Pagination',
data () {
return {
search: '',
current: null,
page: 0,
posts: [],
createTitle: '',
createBody: '',
visiblePostID: '',
}
},
watch: {
counter: function(newValue, oldValue) {
this.getData()
}
},
created(){
this.getData()
},
computed: {
evenPosts: function(posts){
return Math.ceil(this.posts.length/6);
},
paginatedData() {
const start = this.page * 6;
const end = start + 6;
return this.posts.filter((post) => {
return post.title.match(this.search);
}).slice(start, end);
},
},
methods: {
getData() {
axios.get(`https://jsonplaceholder.typicode.com/posts`).then(response => {
this.posts = response.data
})
},
}
}
</script>
Header vue:
AddPost
<script>
import axios from 'axios';
export default {
name: 'Pagination',
data () {
return {
search: '',
current: null,
posts: [],
createTitle: '',
createBody: '',
}
},
created(){
this.getData()
},
methods: {
getData() {
axios.get(`https://jsonplaceholder.typicode.com/posts`).then(response => {
this.posts = response.data
})
},
addPost() {
axios.post('http://jsonplaceholder.typicode.com/posts/', {
title: this.createTitle,
body: this.createBody
}).then((response) => {
this.posts.unshift(response.data)
})
},
}
}
</script>
App.vue:
<template>
<div id="app">
<header-self></header-self>
<router-view></router-view>
</div>
</template>
<script>
export default {
components: {
name: 'app',
}
}
</script>
You have a computed property paginatedData in your "posts" component that relies a variable this.search:
paginatedData () {
const start = this.page * 6;
const end = start + 6;
return this.posts.filter((post) => {
return post.title.match(this.search);
}).slice(start, end);
},
but this.search value is not updated in that component because you moved the search input that populates that value into the header component.
What you need to do now is make sure that the updated search value is passed into your "posts" component so that the paginatedData computed property detects the change and computes the new paginatedData value.
You're now encountering the need to pass values between components that may not have a parent/child relationship.
In your scenario, I would look at handling this need with some Simple State Management as described in the Vue docs.
Depending on the scale of you app it may be worth implementing Vuex for state management.

Let 2 single file components communicate with each other

How can I let 2 single file components communicate with each other.
For example: I have 2 file components. Content.vue and a Aside.vue
How can i create something like, when I click on a button inside Aside.vue that something will update inside Content.vue
this is how the 2 single file compontents look inside the index.html:
<div class="container articleContainer">
<article-content></article-content>
<article-aside></article-aside>
</div>
Aside.vue:
<template>
<aside>
<span #click="updateCounter">Dit is een aside.</span>
</aside>
</template>
<script>
export default {
data() {
return {
aside: "aside message"
}
}
}
</script>
Content.vue
<template>
<article>
<p>{{ counter }}</p>
<button #click="updateCounter">Update Counter</button>
</article>
</template>
<script>
export default {
data() {
return {
counter: 0
}
}
methods: {
updateCounter: function() {
this.counter = this.counter + 2;
},
}
}
</script>
When I click on the span inside the Aside template how can I use updateCounter to update the counter inside Content.vue.
if your app is not aas big or complex to use vuex , you can set up an EventBus like this:
export const EventBus = new Vue();// in your main.js file
in Aside.vue:
<template>
<aside>
<span #click="updateCounter">Dit is een aside.</span>
</aside>
</template>
<script>
import {EventBus} from './path/to/main.js'
export default {
data() {
return {
aside: "aside message"
}
},
methods:{
updateCounter(){
EventBus.emit('updateCounter');
}
}
}
</script>
in Content.vue
<template>
<article>
<p>{{ counter }}</p>
<button #click="updateCounter">Update Counter</button>
</article>
</template>
<script>
import {EventBus} from './path/to/main.js'
export default {
data() {
return {
counter: 0
}
}
created() {
EventBus.on('updateCounter', () => {
this.counter = this.counter + 2;
});
},
methods: {
updateCounter: function() {
this.counter = this.counter + 2;
},
}
}
</script>
Option 1: Have a value in the App.vue that gets reflected by both the components. (That's the this.$parent.someParentMethod(someValue);-way, which would be mixed with props).
Option 2 (way easier, cleaner and best-practice): vuex
Communication between any components using Event Bus
Event Bus is not limited to a parent-child relation. You can share information between any components.
<script>
export default
name: 'ComponentA',
methods: {
sendGlobalMessage() {
this.$root.$emit('message_from_a', arg1, arg2);
}
}
}
</script>
In the above ComponentA, we are firing an event “message_from_a” and passing arguments. Arguments are optional here. Any other component can listen to this event.
<script>
export default
name: 'ComponentB',
mounted() {
this.$root.$on('message_from_a', (arg1, arg2) => {
console.log('Message received');
});
}
}
</script>
In ComponentB to listen an event, we have to register it first. We can do so by putting an event listener inside mounted() callback. This callback will be triggered when an event is fired from any component.
source

Categories

Resources