Normally, parent-child communications are done via events or props. But sometimes, you need to access data, functions, or computed properties that exist in the child or the parent function.
Vue provides a way for us to interact in both ways, thereby opening doors to communications and events such as props and event listeners.
There is another way to access the data between the components: by using direct access. This can be done with the help of a special attribute in the template when using the single-file component, or by making a direct call to the object inside the JavaScript. This method is seen by some as a little lazy, but there are times when there really is no other way to do it than this.
Getting ready
The prerequisite for this recipe is Node.js 12+.
The Node.js global objects that are required for this recipe are as follows:
- @vue/cli
- @vue/cli-service-global
To complete this recipe, we will use our Vue project and the Vue CLI, as we did in the Creating functional components recipe.
How to do it...
We're going to separate this recipe into four parts. The first three parts will cover the creation of new components – StarRatingInput, StarRatingDisplay, and StarRating – while the last part will cover the direct parent-child manipulation of the data and function's access.
Creating the star rating input
In this recipe, we are going to create a star rating input, based on a five-star ranking system.
Follow these steps to create a custom star rating input:
- Create a new file called StarRatingInput.vue in the src/components folder.
- In the <script> part of the component, create a maxRating property in the props property that is a number, non-required, and has a default value of 5. In the data property, we need to create our rating property, with a default value of 0. In the methods property, we need to create three methods: updateRating, emitFinalVoting, and getStarName. The updateRating method will save the rating to the data, emitFinalVoting will call updateRating and emit the rating to the parent component through a final-vote event, and getStarName will receive a value and return the icon name of the star:
<script>
export default {
name: 'StarRatingInput',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
},
data: () => ({
rating: 0,
}),
methods: {
updateRating(value) {
this.rating = value;
},
emitFinalVote(value) {
this.updateRating(value);
this.$emit('final-vote', this.rating);
},
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
</script>
- In the <template> part of the component, we need to create a <slot> component so that we can place the text before the star rating. We'll create a dynamic list of stars based on the maxRating value that we received via the props property. Each star that is created will have a listener attached to it in the mouseenter, focus, and click events. mouseenter and focus, when fired, will call the updateRating method, and click will call emitFinalVote:
<template>
<div class="starRating">
<span class="rateThis">
<slot/>
</span>
<ul>
<li
v-for="rate in maxRating"
:key="rate"
@mouseenter="updateRating(rate)"
@click="emitFinalVote(rate)"
@focus="updateRating(rate)"
>
<i class="material-icons">
{{ getStarName(rate) }}
</i>
</li>
</ul>
</div>
</template>
- We need to import the Material Design icons into our application. Create a new styling file in the styles folder called materialIcons.css and add the CSS rules for font-family:
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/materialicons/v48/flUhRq6tzZclQEJ- Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2');
}
.material-icons {
font-family: 'Material Icons' !important;
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
- Open the main.js file and import the created stylesheet into it. The css-loader webpack will process the imported .css files in JavaScript files. This will help with development because you don't need to reimport the file elsewhere:
import { createApp } from 'vue';
import App from './App.vue';
import './style/materialIcons.css';
createApp(App).mount('#app');
- To style our component, we will create a common styling file in the src/style folder called starRating.css. There, we will add the common styles that will be shared between the StarRatingDisplay and StarRatingInput components:
.starRating {
user-select: none;
display: flex;
flex-direction: row;
}
.starRating * {
line-height: 0.9rem;
}
.starRating .material-icons {
font-size: .9rem !important;
color: orange;
}
ul {
display: inline-block;
padding: 0;
margin: 0;
}
ul > li {
list-style: none;
float: left;
}
- In the <style> part of the component, we need to create all the CSS rules. Then, inside the StarRatingInput.vue component file located in the src/components folder, we need to add the scoped attribute to <style> so that none of the CSS rules affect any of the other elements in our application. Here, we will import the common styles that we created and add new ones for the input:
<style scoped>
@import '../style/starRating.css';
.starRating {
justify-content: space-between;
}
.starRating * {
line-height: 1.7rem;
}
.starRating .material-icons {
font-size: 1.6rem !important;
}
.rateThis {
display: inline-block;
color: rgba(0, 0, 0, .65);
font-size: 1rem;
}
</style>
-
To run the server and see your component, you will need to open a Terminal (macOS or Linux) or Command Prompt/PowerShell (Windows) and execute the following command:
> npm run serve
Here is the component rendered and running:
Creating the StarRatingDisplay component
Now that we have our input, we need a way to display the selected choice to the user. Follow these steps to create a StarRatingDisplay component:
- Create a new component called StarRatingDisplay.vue in the src/components folder.
- In the <script> part of the component, in the props property, we need to create three new properties: maxRating, rating, and votes. All three of them will be numbers, non-required and have a default value. In the methods property, we need to create a new method called getStarName, which will receive a value and return the icon name of the star:
<script>
export default {
name: 'StarRatingDisplay',
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
methods: {
getStarName(rate) {
if (rate <= this.rating) {
return 'star';
}
if (Math.fround((rate - this.rating)) < 1) {
return 'star_half';
}
return 'star_border';
},
},
};
</script>
- In <template>, we need to create a dynamic list of stars based on the maxRating value that we received via the props property. After the list, we need to display that we received votes, and if we receive any more votes, we will display them too:
<template>
<div class="starRating">
<ul>
<li
v-for="rate in maxRating"
:key="rate"
>
<i class="material-icons">
{{ getStarName(rate) }}
</i>
</li>
</ul>
<span class="rating">
{{ rating }}
</span>
<span
v-if="votes"
class="votes"
>
({{ votes }})
</span>
</div>
</template>
- In the <style> part of the component, we need to create all the CSS rules. We need to add the scoped attribute to <style> so that none of the CSS rules affect any of the other elements in our application. Here, we will import the common styles that we created and add new ones for the display:
<style scoped>
@import '../style/starRating.css';
.rating, .votes {
display: inline-block;
color: rgba(0, 0, 0, .65);
font-size: .75rem;
margin-left: .4rem;
}
</style>
-
To run the server and see your component, you need to open a Terminal (macOS or Linux) or Command Prompt/PowerShell (Windows) and execute the following command:
> npm run serve
Here is the component rendered and running:
Creating the StarRating component
Now that we've created the input and the display, we need to join them together inside a single component. This component will be the final component that we'll use in the application.
Follow these steps to create the final StarRating component:
- Create a new file called StarRating.vue in the src/components folder.
- In the <script> part of the component, we need to import the StarRatingDisplay and StarRatingInput components. In the props property, we need to create three new properties: maxRating, rating, and votes. All three of them will be numbers, non-required, and have a default value. In the data property, we need to create our rating property, with a default value of 0, and a property called voted, with a default value of false. In the methods property, we need to add a new method called vote, which will receive rank as an argument. It will define rating as the received value and define the inside variable of the voted component as true:
<script>
import StarRatingInput from './StarRatingInput.vue';
import StarRatingDisplay from './StarRatingDisplay.vue';
export default {
name: 'StarRating',
components: { StarRatingDisplay, StarRatingInput },
props: {
maxRating: {
type: Number,
required: false,
default: 5,
},
rating: {
type: Number,
required: false,
default: 0,
},
votes: {
type: Number,
required: false,
default: 0,
},
},
data: () => ({
rank: 0,
voted: false,
}),
methods: {
vote(rank) {
this.rank = rank;
this.voted = true;
},
},
};
</script>
- For the <template> part, we will place both components here, thereby displaying the input of the rating:
<template>
<div>
<StarRatingInput
v-if="!voted"
:max-rating="maxRating"
@final-vote="vote"
>
Rate this Place
</StarRatingInput>
<StarRatingDisplay
v-else
:max-rating="maxRating"
:rating="rating || rank"
:votes="votes"
/>
</div>
</template>
Data manipulation on child components
Now that all of our components are ready, we need to add them to our application. The base application will access the child component, and it will set the rating to 5 stars.
Follow these steps to understand and manipulate the data in the child components:
- In the App.vue file, in the <template> part of the component, remove the main-text attribute of the MaterialCardBox component and set it as the default slot of the component.
- Before the placed text, we will add the StarRating component. We will add a ref attribute to it. This attribute will tell Vue to link this component directly to a special property in the this object of the component. In the action buttons, we will add the listeners for the click event – one for resetVote and another for forceVote:
<template>
<div id="app">
<MaterialCardBox
header="Material Card Header"
sub-header="Card Sub Header"
show-media
show-actions
img-src="https://picsum.photos/300/200"
>
<p>
<StarRating
ref="starRating"
/>
</p>
<p>
The path of the righteous man is beset on all sides by the
iniquities of the selfish and the tyranny of evil men.
</p>
<template v-slot:action>
<MaterialButton
background-color="#027be3"
text-color="#fff"
@click="resetVote"
>
Reset
</MaterialButton>
<MaterialButton
background-color="#26a69a"
text-color="#fff"
is-flat
@click="forceVote"
>
Rate 5 Stars
</MaterialButton>
</template>
</MaterialCardBox>
</div>
</template>
- In the <script> part of the component, we will create a methods property and add two new methods: resetVote and forceVote. These methods will access the StarRating component and reset the data or set the data to a 5-star vote, respectively:
<script>
import MaterialCardBox from './components/MaterialCardBox.vue';
import MaterialButton from './components/MaterialButton.vue';
import StarRating from './components/StarRating.vue';
export default {
name: 'App',
components: {
StarRating,
MaterialButton,
MaterialCardBox,
},
methods: {
resetVote() {
this.$refs.starRating.vote(0);
this.$refs.starRating.voted = false;
},
forceVote() {
this.$refs.starRating.vote(5);
},
},
How it works...
When the ref property is added to the component, Vue adds a link to the referenced element to the $refs property inside the this property object of JavaScript. From there, you have full access to the component.
This method is commonly used to manipulate HTML DOM elements without the need to call for document query selector functions.
However, the main function of this property is to give access to the Vue component directly, enabling you to execute functions and see the computed properties, variables, and changed variables of the component – this is like having full access to the component from the outside.
There's more...
In the same way that a parent can access a child component, a child can access a parent component by calling $parent on the this object. An event can access the root element of the Vue application by calling the $root property.
See also
You can find out more information about parent-child communication at https://v3.vuejs.org/guide/migration/custom-directives.html#edge-case-accessing-the-component-instance.