But this doesn’t do anything … yet. Components add their own HTML through their Component.extend({
tag: "video-player",
view: `<h2>I am a player!</h2>`
});
A component’s ViewModel. For example, we can make a
<video>
display
"http://bit.ly/can-tom-n-jerry"
by defining a
src
property on the
ViewModel
and using it in the Component.extend({
tag: "video-player",
view: `
<video>
<source src="{{src}}"/>
</video>
`,
ViewModel: {
src: {type: "string", default: "http://bit.ly/can-tom-n-jerry"}
}
});
But we want the <video-player>
to take a src
attribute value itself and use that for the
<source>
’s src
. For example, if
we wanted the video to play "http://dl3.webmfiles.org/big-buck-bunny_trailer.webm"
instead of "http://bit.ly/can-tom-n-jerry"
, we would:
- Update
<video-player>
to pass "http://dl3.webmfiles.org/big-buck-bunny_trailer.webm"
with toChild:raw:
<video-player src:raw="http://dl3.webmfiles.org/big-buck-bunny_trailer.webm"/>
- Update the ViewModel to define a
src
property like:
Component.extend({
tag: "video-player",
view: `
<video>
<source src="{{src}}"/> {{!}}
</video>
`,
ViewModel: {
src: "string"
}
});
Finally, to have a <video>
element show the native controls, add a controls
attribute like:
<video controls>
The solution
Update the JS tab to:
import {Component} from "//unpkg.com/can@5/core.mjs";
Component.extend({ //
tag: "video-player", //
view: ` {{!}}
<video controls> {{!}}
<source src="{{src}}"/> {{!}}
</video> {{!}}
`, //
ViewModel: { //
src: "string", //
} //
}); //
Update the HTML to:
<video-player src:raw="http://bit.ly/can-tom-n-jerry"></video-player> <!---->
Make play / pause button change as video is played and paused
The problem
When the video is played or paused using the native controls, we want to change the content of a <button>
to say “Play” or “Pause”.
When the video is played, the button should say “Pause”.
When the video is paused, the button should say “Play”.
We want the button to be within a <div>
after the video element like:
</video>
<div>
<button>Play</button>
</div>
What you need to know
-
To change the HTML content of the page, use {{#if(expression)}} and {{else}} like:
<button>{{#if(playing)}} Pause {{else}} Play {{/if}}</button>
-
The ViewModel. To create a boolean
value in the ViewModel do:
ViewModel: {
// ...
playing: "boolean",
}
-
Methods can be used to change the ViewModel. The following might create methods that change the playing
value:
ViewModel: {
// ...
play() {
this.playing = true;
},
pause() {
this.playing = false;
},
}
-
You can listen to events on the DOM with on:event. For example, the following might
listen to a click on a <div>
and call doSomething()
:
<div on:click="doSomething()">
<video>
elements have a variety of useful events, including play and
pause events that are emitted when the video is played or paused.
The solution
Update the JavaScript tab to:
import {Component} from "//unpkg.com/can@5/core.mjs";
Component.extend({
tag: "video-player",
view: `
<video controls
on:play="play()" {{!}}
on:pause="pause()"> {{!}}
<source src="{{src}}"/>
</video>
<div> {{!}}
<button> {{!}}
{{#if(playing)}} Pause {{else}} Play {{/if}} {{!}}
</button> {{!}}
</div> {{!}}
`,
ViewModel: {
src: "string",
playing: "boolean", //
play() { //
this.playing = true; //
}, //
pause() { //
this.playing = false; //
}, //
}
});
Make clicking the play/pause button play or pause the video
The problem
When the play/pause <button>
we created in the previous section is clicked, we want to
either play or pause the video.
What you need to know
CanJS prefers to manage the state of your application in ViewModel. The <video>
player has state, such as
if the video is playing
. When the play/pause button is clicked, we want to update the state
of the ViewModel and have the ViewModel update the state of the video player as a side effect.
What this means is that instead of something like:
togglePlay() {
if ( videoElement.paused ) {
videoElement.play()
} else {
videoElement.pause()
}
}
We update the state like:
togglePlay() {
this.playing = !this.playing;
}
And listen to when playing
changes and update the video
element like:
viewModel.listenTo("playing", function(event, isPlaying) {
if ( isPlaying ) {
videoElement.play()
} else {
videoElement.pause()
}
})
This means that you need to:
- Listen to when the
<button>
is clicked and call a ViewModel method that updates the playing
state.
- Listen to when the
playing
state changes and update the state of the <video>
element.
You already know everything you need to know for step #1. (Have the button call a togglePlay
method with on:click="togglePlay()"
and make the togglePlay()
method toggle the state of the playing
property.)
For step #2, you need to use the connectedCallback lifecycle hook. This
hook gives you access to the component’s element and is a good place to do side-effects. Its use looks
like this:
ViewModel: {
// ...
connectedCallback(element) {
// perform mischief
}
}
connectedCallback
gets called once the component’s element
is in the page. You can use
listenTo to listen to changes in the ViewModel’s properties and
perform side-effects. The following listens to when playing
changes:
ViewModel: {
// ...
connectedCallback(element) {
this.listenTo("playing", function(event, isPlaying) {
})
}
}
Use querySelector
to get the <video>
element from the <video-player>
like:
element.querySelector("video")
<video>
elements have a .play() and .pause() methods that can start and stop a video.
The solution
Update the JavaScript tab to:
import {Component} from "//unpkg.com/can@5/core.mjs";
Component.extend({
tag: "video-player",
view: `
<video controls
on:play="play()"
on:pause="pause()">
<source src="{{src}}"/>
</video>
<div>
<button on:click="togglePlay()"> {{!}}
{{#if(playing)}} Pause {{else}} Play {{/if}}
</button>
</div>
`,
ViewModel: {
src: "string",
playing: "boolean",
play() {
this.playing = true;
},
pause() {
this.playing = false;
},
togglePlay() { //
this.playing = !this.playing; //
}, //
connectedCallback(element) { //
this.listenTo("playing", function(event, isPlaying) { //
if (isPlaying) { //
element.querySelector("video").play(); //
} else { //
element.querySelector("video").pause(); //
} //
}); //
} //
}
});
Show current time and duration
The problem
Show the current time and duration of the video element. The time and duration should be
formatted like: mm:SS
. They should be presented within two spans like:
</button>
<span>1:22</span>
<span>2:45</span>
What you need to know
-
Methods can be used to format values in can-stache. For example, you can uppercase values like this:
<span>{{upper(value)}}</span>
With a method like:
ViewModel: {
// ...
upper(value) {
return value.toString().toUpperCase();
}
}
The following can be used to format time:
formatTime(time) {
if (time === null || time === undefined) {
return "--";
}
const minutes = Math.floor(time / 60);
let seconds = Math.floor(time - minutes * 60);
if (seconds < 10) {
seconds = "0" + seconds;
}
return minutes + ":" + seconds;
}
-
Time is given as a number. Use the following to create a number property on
the ViewModel:
ViewModel: {
// ...
duration: "number",
currentTime: "number"
}
-
<video>
elements emit a loadmetadata event when they know how long
the video is. They also emit a timeupdate event when the video’s current play position
changes.
videoElement.duration
reads the duration of a video.
videoElement.currentTime
reads the current play position of a video.
-
You can get the element in an stache on:event
binding with scope.element like:
<video on:timeupdate="updateTimes(scope.element)"/>
The solution
Update the JavaScript tab to:
import {Component} from "//unpkg.com/can@5/core.mjs";
Component.extend({
tag: "video-player",
view: `
<video controls
on:play="play()"
on:pause="pause()"
on:timeupdate="updateTimes(scope.element)" {{!}}
on:loadedmetadata="updateTimes(scope.element)"> {{!}}
<source src="{{src}}"/>
</video>
<div>
<button on:click="togglePlay()">
{{#if(playing)}} Pause {{else}} Play {{/if}}
</button>
<span>{{formatTime(currentTime)}}</span> / {{!}}
<span>{{formatTime(duration)}} </span> {{!}}
</div>
`,
ViewModel: {
src: "string",
playing: "boolean",
duration: "number", //
currentTime: "number", //
updateTimes(videoElement) { //
this.currentTime = videoElement.currentTime || 0; //
this.duration = videoElement.duration; //
}, //
formatTime(time) { //
if (time === null || time === undefined) { //
return "--"; //
} //
const minutes = Math.floor(time / 60); //
let seconds = Math.floor(time - minutes * 60); //
if (seconds < 10) { //
seconds = "0" + seconds; //
} //
return minutes + ":" + seconds; //
}, //
play() {
this.playing = true;
},
pause() {
this.playing = false;
},
togglePlay() {
this.playing = !this.playing;
},
connectedCallback(element) {
this.listenTo("playing", function(event, isPlaying) {
if (isPlaying) {
element.querySelector("video").play();
} else {
element.querySelector("video").pause();
}
});
}
}
});
Make range show position slider at current time
The problem
Create a <input type="range"/>
element that changes its position as
the video playing position changes.
The <input type="range"/>
element should be after the <button>
and before the
currentTime
span like:
</button>
<input type="range"/>
<span>{{formatTime(currentTime)}}</span> /
What you need to know
-
The range input can have an initial value, max value, and step size
specified like:
<input type="range" value="0" max="1" step="any"/>
-
The range will have values from 0 to 1. We will need to translate the currentTime to
a number between 0 and 1. We can do this with a computed getter property like:
ViewModel: {
// ...
get percentComplete() {
return this.currentTime / this.duration;
},
}
-
Use key:from to update a value from a ViewModel property like:
<input value:from="percentComplete"/>
The solution
Update the JavaScript tab to:
import {Component} from "//unpkg.com/can@5/core.mjs";
Component.extend({
tag: "video-player",
view: `
<video controls
on:play="play()"
on:pause="pause()"
on:timeupdate="updateTimes(scope.element)"
on:loadedmetadata="updateTimes(scope.element)">
<source src="{{src}}"/>
</video>
<div>
<button on:click="togglePlay()">
{{#if(playing)}} Pause {{else}} Play {{/if}}
</button>
<input type="range" value="0" max="1" step="any" {{!}}
value:from="percentComplete"/> {{!}}
<span>{{formatTime(currentTime)}}</span> /
<span>{{formatTime(duration)}} </span>
</div>
`,
ViewModel: {
src: "string",
playing: "boolean",
duration: "number",
currentTime: "number",
get percentComplete() { //
return this.currentTime / this.duration; //
}, //
updateTimes(videoElement) {
this.currentTime = videoElement.currentTime || 0;
this.duration = videoElement.duration;
},
formatTime(time) {
if (time === null || time === undefined) {
return "--";
}
const minutes = Math.floor(time / 60);
let seconds = Math.floor(time - minutes * 60);
if (seconds < 10) {
seconds = "0" + seconds;
}
return minutes + ":" + seconds;
},
play() {
this.playing = true;
},
pause() {
this.playing = false;
},
togglePlay() {
this.playing = !this.playing;
},
connectedCallback(element) {
this.listenTo("playing", function(event, isPlaying) {
if (isPlaying) {
element.querySelector("video").play();
} else {
element.querySelector("video").pause();
}
});
}
}
});
Make sliding the range update the current time
The problem
In this section we will:
- Remove the native controls from the video player. We don’t need them anymore!
- Make it so when a user moves the range slider, the video position updates.
What you need to know
Similar to when we made the play/pause button play or pause the video, we will want to update the
currentTime
property and then listen to when currentTime
changes and update the <video>
element’s currentTime
as a side-effect.
This time, we need to translate the sliders values between 0 and 1 to currentTime
values. We can do this by creating a percentComplete
setter that updates currentTime
like:
ViewModel: {
// ...
get percentComplete() {
return this.currentTime / this.duration;
},
set percentComplete(newVal) {
this.currentTime = newVal * this.duration;
},
// ...
}
Use key:bind to two-way bind a value to a ViewModel property:
<input value:bind="someViewModelProperty"/>
The solution
Update the JavaScript tab to:
import {Component} from "//unpkg.com/can@5/core.mjs";
Component.extend({
tag: "video-player",
view: `
<video {{!}}
on:play="play()"
on:pause="pause()"
on:timeupdate="updateTimes(scope.element)"
on:loadedmetadata="updateTimes(scope.element)">
<source src="{{src}}"/>
</video>
<div>
<button on:click="togglePlay()">
{{#if(playing)}} Pause {{else}} Play {{/if}}
</button>
<input type="range" value="0" max="1" step="any"
value:bind="percentComplete"/> {{!}}
<span>{{formatTime(currentTime)}}</span> /
<span>{{formatTime(duration)}} </span>
</div>
`,
ViewModel: {
src: "string",
playing: "boolean",
duration: "number",
currentTime: "number",
get percentComplete() {
return this.currentTime / this.duration;
},
set percentComplete(newVal) { //
this.currentTime = newVal * this.duration; //
}, //
updateTimes(videoElement) {
this.currentTime = videoElement.currentTime || 0;
this.duration = videoElement.duration;
},
formatTime(time) {
if (time === null || time === undefined) {
return "--";
}
const minutes = Math.floor(time / 60);
let seconds = Math.floor(time - minutes * 60);
if (seconds < 10) {
seconds = "0" + seconds;
}
return minutes + ":" + seconds;
},
play() {
this.playing = true;
},
pause() {
this.playing = false;
},
togglePlay() {
this.playing = !this.playing;
},
connectedCallback(element) {
this.listenTo("playing", function(event, isPlaying) {
if (isPlaying) {
element.querySelector("video").play();
} else {
element.querySelector("video").pause();
}
});
this.listenTo("currentTime", function(event, currentTime) { //
const videoElement = element.querySelector("video"); //
if (currentTime !== videoElement.currentTime) { //
videoElement.currentTime = currentTime; //
} //
}); //
}
}
});
Result
When finished, you should see something like the following JS Bin:
The post Create Custom Video Player Controls with CanJS appeared first on David Walsh Blog.