We covered the basics of Component props in the last section and we discussed how it used to pass data from a parent component to a child component.

In this tutorial, we will use another example to uncover some additional in-depth details about Component Props.

In this example, we will build a simple Countdown component in VueJS.

Here is what the final output looks like.

timer component in VueJS

It has a countdown timer and also has three buttons on the timer to control the Timer.

Alright, let's begin with the implementation.

# Component Template

Before diving into the steps, Let's first set up our Component, For this, we will initialize a new Vue Instance an along with that we'll also define a new Component named countdown-timer.

         const TIMER_STATE = {
          running: 'Running',
          stopped: 'Stopped',
          paused: 'Paused'
      };

      Vue.component('countdown-timer',{
            template: `<div>
                            <span>{{totalMinutes}}:{{totalSeconds}}</span><br/>
                            <button class="btn btn-success">Start</button>
                            <button class="btn btn-warning">Pause</button>
                            <button class="btn btn-danger">Reset</button>
                        </div>`,
            data(){
                return {
                    timerState : TIMER_STATE.stopped
                }
            }
        });


        let app = new Vue({
            el: '#app',
        });
  • We have defined a global const named TIMER_STATE to keep track of the current state of the Timer.
  • A basic template that shows {{totalMinutes}}:{{totalSeconds}} counter and along with that three control buttons, which we have styled using Boostrap class styling.
  • The default state of the timer is stopped.

You can use this component in your HTML by putting the following markup.

<h1><countdown-timer></countdown-timer></h1>

If you run this application in your browser, you won't see the minutes and seconds, and you will receive an error in the console. Property or method "totalMinutes" is not defined. This is expected since we haven't yet defined the totalMinutes and totalSeconds data properties in our component.

Let's move to the next to resolve this error.

# The need for Props.

As the next step , we are looking to define totalMinutes and totalSeconds data properties in our component. This is the total time for which the countdown will run.

We can define these data properties inside the Vue component, but instead, we want the ability for the user to pass any desired data of minutes and seconds. Thus we will have to utilize component props. With props a parent component can pass on data to child component via setting it as an attribute value in the component itself.

Let's define the additional properties that we are going to require in our component.


...
data(){
    return {
        timerState : TIMER_STATE.stopped
    }
},
props: ['totalMinutes', 'totalSeconds'],
...

# Props Casing.

We have defined two props in our component named totalMinutes and totalSeconds, Now since HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you’re using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents:

To pass this attribute values from our component, we'll have to use kebab case.

<!-- kebab-case in HTML -->
<h1><countdown-timer total-minutes="2" total-seconds="30"></countdown-timer></h1>

If you run the application now, you should see the values passed in props on the screen.

# Props Default Value.

Even though we now have the ability to set the totalMinutes and totalSeconds via component props. It's a good idea to declare a default value of both the properties in case user choose not to specify the values.

Here is how you can do that.

props: {
    totalMinutes: {
        default: 1
    },
    totalSeconds: {
        default: 0
    }
}

Earlier, we defined our props as an Array, but VueJS also gives ability to define it as an object and specify additional details for each of the properties. Here we have defined the default value of totalMinutes to be 1 and totalSeconds to be 0.

That means if the user does not pass values, the countdown timer will run for default of 1 minute.

# Props Validation.

Since we are accepting the props values from User, Shouldn't we add validation as well? VueJS got us covered there too. You can specify the type of props we are expecting as well if the property is required. In our case we want both the properties to be of type Number, Let's specify that.

props: {
    totalMinutes: {
        type: Number,
        default: 1
    },
    totalSeconds: {
        type: Number,
        default: 0
    }
}

When prop validation fails, Vue will produce a console warning (if using the development build).

For more details on what different types of validation you can specify refer -> VueJS Prop Validation

# Passing Static or Dynamic Props.

With the changes in the last step, we have introduced a warning in our application. Refresh the page and you'll find this warning in your console.

[Vue warn]: Invalid prop: type check failed for prop "totalMinutes". Expected Number with value 2, got String with value "2".

Even though the value we have passed is a number, but since we have passed it as Static value, Vue considers it as a String. All values passed as a static value are a String to Vue. Now, how do we pass it as a Number?

We'll have to bind the value so that Vue knows the correct type of the value.

<h1><countdown-timer :total-minutes="2" :total-seconds="30"></countdown-timer></h1>

Here we have a shorthand of v-bind directive to bind the properties totalMinutes and totalSeconds.

For more details on what how you can bind different types of values -> VueJS Passing Static or Dynamic Props

# Countdown Timer Methods and Computed Properties

Now since we are set with the required data properties, Let's get back to the Countdown timer, and let's add some methods in our component for the countdown timer to function.


...
            methods:{
                start(){
                    this._tick();
                    this.ticking = setInterval(this._tick, 1000);
                    this.timerState = TIMER_STATE.running;
                },

                _tick(){
                    if(this.totalMinutes !== 0){
                        this.totalSeconds--;
                        return
                    }

                    if(this.totalMinutes !== 0){
                        this.totalMinutes--;
                        this.totalSeconds = 59;
                        return;
                    }
                }
            },
...

We have added a start method, which call's the _tick method in the interval of 1 second and keeps the countdown running until it goes to 0.

Let's bind the click event on Start button to call start method.

<button class="btn btn-success" @click="start">Start</button>

Refresh the page, and click on the start button you should see the countdown timer ticking.

But there is a problem, Open the console and you'll see the following warning

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "totalMinutes"

Let's see what this means, and how we can solve this in the next step.

# Mutating Props?

In the last step, inside the _tick method, we are directly changing the props totalMinutes and totalSeconds. As per Vue mutating the props directly isn't allowed.

Mutating the properties isn't Allowed, Why?

All props form a one-way-down binding between the child property and the parent one: This means that changing the props when passing from parent to child is allowed but not the other way. This prevents child components from accidentally mutating the parent’s state, which can make your app’s data flow harder to understand.

So how do we solve this ?

The property which is passed as an initial value can be stored as a local data property.

Let's do this.

...
...
template: `<div>
    <span>{{timerMinutes}}:{{timerSeconds}}</span><br/>
    <button class="btn btn-success" @click="start">Start</button>
    <button class="btn btn-warning">Pause</button>
    <button class="btn btn-danger">Reset</button>
</div>`,
data(){
    return {
        timerMinutes: 0,
        timerSeconds: 0,
        timerState : TIMER_STATE.stopped
    }
},
mounted(){
    this.timerMinutes = this.totalMinutes;
    this.timerSeconds = this.totalSeconds;
},
methods:{
    start(){
        this._tick();
        this.ticking = setInterval(this._tick, 1000);
        this.timerState = TIMER_STATE.running;
    },
    _tick(){
        if(this.timerSeconds !== 0){
            this.timerSeconds--;
            return
        }

        if(this.timerMinutes !== 0){
            this.timerMinutes--;
            this.timerSeconds = 59;
            return;
        }
    }
},
...
...

We have defined two additional local data properties named timerMinutes and timerSeconds. And we have utilized the life-cycle event method mounted to assign values from props to local data properties. As soon as our component is mounted we take the values from props and assign them to local data properties.

Notice that we have also changed the display properties in our template to be local data properties instead of props variables.

That's all about the in-depth discussion of props.

In the next step, Let's write some more methods and computed properties to complete our countdown timer component.

# Countdown Timer Methods & Computed Properties.

Here is the full code of our countdown timer.


Vue.component('countdown-timer',{
            template: `<div>
                            <span>{{min}}:{{sec}}</span><br/>
                            <button class="btn btn-success" @click="start" :disabled="!isTimerRunning">Start</button>
                            <button class="btn btn-warning" v-on="isTimerPaused ? { click: start } : { click : pause }" :disabled="isTimerRunning">{{isTimerPaused ? 'Resume' : 'Pause'}}</button>
                            <button class="btn btn-danger" @click="reset" :disabled="isTimerRunning">Reset</button>
                        </div>`,
            data(){
                return{
                    timerMinutes: 0,
                    timerSeconds: 0,
                    timerState : TIMER_STATE.stopped
                }

            },
            props:{
                totalMinutes: {
                    type: Number,
                    default: 1
                },
                totalSeconds: {
                    type: Number,
                    default: 0
                }
            },
            mounted(){
                this.timerMinutes = this.totalMinutes;
                this.timerSeconds = this.totalSeconds;
            },
            methods:{
                start(){
                    this._tick();
                    this.ticking = setInterval(this._tick, 1000);
                    this.timerState = TIMER_STATE.running;
                },
                pause(){
                    clearInterval(this.ticking);
                    this.timerState = TIMER_STATE.paused;
                },
                reset(){
                    this.timerMinutes = this.totalMinutes;
                    this.timerSeconds = this.totalSeconds;
                    clearInterval(this.ticking);
                    this.timerState = TIMER_STATE.stopped;
                },

                _tick(){
                    if(this.timerSeconds !== 0){
                        this.timerSeconds--;
                        return
                    }

                    if(this.timerMinutes !== 0){
                        this.timerMinutes--;
                        this.timerSeconds = 59;
                        return;
                    }
                }
            },
            computed: {
                min() {
                    if(this.timerMinutes < 10){
                        return '0'+this.timerMinutes;
                    }
                    return this.timerMinutes;
                },
                sec() {
                    if(this.timerSeconds < 10){
                        return '0'+this.timerSeconds;
                    }
                    return this.timerSeconds;
                },
                isTimerRunning(){
                    return this.timerState === 'Stopped';
                },
                isTimerPaused(){
                    return this.timerState == 'Paused';
                }
            }
        });

We have added a few more methods and bound them to buttons on the click events. We have also added a few computed properties, which helps us to show the time incorrect format, and also helps to decide when to disable and enable the buttons.

Here is the working Demo.

[WP-Coder id="14"]

Comments