Creating a Cloudinary Vue Component
While React.js has taken much of the attention during the latest wave of JavaScript frameworks, Vue.js has quietly become a favorite of many developers that find React overly complex and don't want to deal with the webpack tooling. With Vue you can simply include the Vue JavaScript file in the page, create a few templates, and you're on your way -- a throwback of the original JavaScript framework days.
Cloudinary, the awesome media storage and delivery service, provides APIs in just about every language to aid developers in using their service, including Node.js, Python, PHP, React, etc. I've been meaning to check out Vue and thought what better way to do so than creating media-centric components with the help of Cloudinary's API. My goal was to create a video component that mirrors what you see on many video-centric sites: load a thumbnail, play preview upon hover, and finally play the video when clicked. Let's go!
Quick note: the Vue.js component I'm creating for this post could be further optimized (use a single <video>
element, swap out controls, animated transitions, etc.) but I want to keep this post as simple and focused as possible. The main goal is to illustrate how Cloudinary and Vue.js are complimentary to each other and they're both incredibly easy to use!
Vue Component
I wanted to create a component because, much like React, they're easily contained and reusable. Let's start by taking a look at the component template.
Component Template
Seeing the HTML skeleton will provide insight into what we'll be manipulating:
<div v-on:mouseenter="showPreview()" v-on:mouseleave="hidePreview()" class="cloudinary-video-item" :style="dimensions"> <div class="cloudinary-video-item-image"> <img :src="poster" :width="width" :height="height" alt="Video Preview"> </div> <div class="cloudinary-video-item-active"> <video ref="previewVideo" autoplay loop :width="width" :height="height"></video> </div> <div class="cloudinary-video-item-video"> <video ref="fullVideo" autoplay controls :width="width" :height="height"></video> </div> <svg v-on:click="play()" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" id="play-icon" version="1.1" height="50" width="50" viewBox="0 0 1200 1200"> <path d="M 600,1200 C 268.65,1200 0,931.35 0,600 0,268.65 268.65,0 600,0 c 331.35,0 600,268.65 600,600 0,331.35 -268.65,600 -600,600 z M 450,300.45 450,899.55 900,600 450,300.45 z" id="path16995" /> </svg> </div>
Our component has four immediate child elements: three elements which show or display based on a CSS state, and a SVG "play" icon. The states are:
- (default) Show a thumbnail/poster image for the video
- (hover) Show a stitched, aggregated video preview (much like this post)
- (active) Shows the entire video
state
attribute; their visibility will be manipulated by CSS selector matching each state.
Component Properties
In the interest of keeping this component simple, I limit the number of properties to only those that are truly needed:
Vue.component('cloudinary-video', { props: { account: { type: String, required: true, default: 'david-wash-blog' }, alias: { type: String, required: true }, // These can be strings as they come in as attributes width: { type: String, default: 400 }, height: { type: String, default: 300 } },
Realize that Cloudinary's transformation API is so powerful that I could add dozens of properties to harness its every power, but this post would balloon into a novel! There are a few further properties which require computed values based on the simple properties, so let's also create those:
computed: { dimensions: function() { return `width:${this.width}px; height:${this.height}px;`; }, poster: function() { return `http://res.cloudinary.com/${this.account}/video/upload/${this.alias}.jpg`; }, preview: function() { return `http://res.cloudinary.com/${this.account}/video/upload/so_0,du_2/l_video:${this.alias},fl_splice,so_12/du_2/fl_layer_apply/l_video:${this.alias},fl_splice,so_24/du_2/fl_layer_apply/l_video:${this.alias},fl_splice,so_36/du_2/fl_layer_apply/l_video:${this.alias},fl_splice,so_48/du_2/fl_layer_apply/l_video:${this.alias},fl_splice,so_80/du_2/fl_layer_apply/${this.alias}.mp4`; }, fullVideo: function() { return `http://res.cloudinary.com/${this.account}/video/upload/${this.alias}.mp4`; } },
Computed properties can reference simple properties, which I employ heavily in this component.
Component Methods
Component methods will be employed to trigger functionality during our component's mouseenter
, mouseleave
, and click
events:
methods: { play: function () { // Hide the preview this.hidePreview(); // Set the state to "play" to show full video element this.$el.setAttribute('state', 'playing'); // Set the full video element src this.$refs.fullVideo.src = this.fullVideo; }, showPreview: function() { // If the full video is loaded and playing, ignore this event if(this.$el.getAttribute('state') === 'playing') { return; } // Update state for CSS / component's child element visibility this.$el.setAttribute('state', 'preview'); // Set the preview video source this.$refs.previewVideo.src = this.preview; }, hidePreview: function() { // If the full video is loaded and playing, ignore this event if(this.$el.getAttribute('state') === 'playing') { return; } // Update state for CSS / component's child element visibility this.$el.setAttribute('state', ''); // Stop the video this.$refs.previewVideo.pause(); } },
While I do use the attribute state
, realize that I'm not using Flux or any other state management utility -- the attribute simply represents which of the three component states should be shown or hidden.
Component CSS
The CSS required for this component seems like a lot but it mostly manages the simple layout as well as "state": showing and hiding each component child element as required:
.cloudinary-video-item { position: relative; } .cloudinary-video-item > div { position: absolute; top: 0; left: 0; } .cloudinary-video-item img { display: block; } .cloudinary-video-item svg { position: absolute; top: 40%; left: 45%; cursor: pointer; opacity: 0.6; } .cloudinary-video-item svg:hover { opacity: 0.9; } /* Default / image only state */ .cloudinary-video-item .cloudinary-video-item-active, .cloudinary-video-item .cloudinary-video-item-video { display: none; } /* Preview state */ .cloudinary-video-item[state=preview] .cloudinary-video-item-active { display: block; } .cloudinary-video-item[state=preview] .cloudinary-video-item-image { display: none; } /* Playing state */ .cloudinary-video-item[state=playing] .cloudinary-video-item-video { display: block; } .cloudinary-video-item[state=playing] .cloudinary-video-item-image, .cloudinary-video-item[state=playing] .cloudinary-video-item-active, .cloudinary-video-item[state=playing] svg { display: none; }
There's a fair amount there but minified would hardly leave a footprint!
Using the Component
With each prop in props
containing a default value, except for the media alias of course, the component usage can be simple:
<!-- simplest usage --> <cloudinary-video alias="cartoon"></cloudinary-video> <!-- customized usage --> <cloudinary-video account="david-wash-blog" alias="cartoon" width="640" height="360"> </cloudinary-video>
And lastly adding a new Vue
call to kick everything off:
new Vue({ el: '#video-holder' })
That's how easy it is to create a Vue.js component for your Cloudinary media!
Closing
Creating a Vue component that uses multiple types of generated media from a single source was easy thanks to Cloudinary. Cloudinary generated the sample image, the video poster, the preview video, and quickly delivered those resources as well as the source video. Vue's simple API made creating the Cloudinary component fun and short on code. I look forward to playing around with Vue and Cloudinary to make some truly powerful media components!
Just in case you find this useful, I’ve created a vue-cloudinary library a year ago: https://github.com/diegopamio/vue-cloudinary