-
-
Save darkwing/611b83fa578e919742c6f817b8682de1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<p>In this guide, you will learn how to create a custom video player using the <code><video></code> element and <a href="http://canjs.com">CanJS</a>. The | |
custom video player will:</p> | |
<ul> | |
<li>Have custom play and pause buttons.</li> | |
<li>Show the current time and duration of the video.</li> | |
<li>Have a <code><input type="range"></code> slider that can adjust the position of the video.</li> | |
</ul> | |
<p>The final player looks like:</p> | |
<div class="cp_embed_wrapper"><iframe id="cp_embed_qyRqMx" src="//codepen.io/justinbmeyer/embed/qyRqMx?height=350&theme-id=dark&slug-hash=qyRqMx&default-tab=js%2Cresult&user=justinbmeyer&embed-version=2&pen-title=CanJS%205.0%20Video%20Player%20-%20Final" scrolling="no" frameborder="0" height="350" allowtransparency="true" allowfullscreen="true" allowpaymentrequest="true" name="CodePen Embed" title="CanJS 5.0 Video Player - Final" class="cp_embed_iframe " style="width: 100%; overflow: hidden;"></iframe></div> | |
<p>The following sections are broken down into the following parts:</p> | |
<ul> | |
<li><strong>The problem</strong> — A description of what the section is trying to accomplish.</li> | |
<li><strong>What you need to know</strong> — Browser or CanJS APIs that are useful for solving the problem.</li> | |
<li><strong>The solution</strong> — The solution to the problem.</li> | |
</ul> | |
<h2 id="Setup">Setup</h2> | |
<p><strong>START THIS TUTORIAL BY Forking THE FOLLOWING CodePen</strong>:</p> | |
<blockquote> | |
<p>Click the <code>Edit in CodePen</code> button. The CodePen will open in a new window. Click the <code>Fork</code> button.</p> | |
</blockquote> | |
<div class="cp_embed_wrapper"><iframe id="cp_embed_LBxbam" src="//codepen.io/justinbmeyer/embed/LBxbam?height=320&theme-id=dark&slug-hash=LBxbam&default-tab=js%2Cresult&user=justinbmeyer&embed-version=2&pen-title=CanJS%205.0%20Video%20Player%20-%20Start" scrolling="no" frameborder="0" height="320" allowtransparency="true" allowfullscreen="true" allowpaymentrequest="true" name="CodePen Embed" title="CanJS 5.0 Video Player - Start" class="cp_embed_iframe " style="width: 100%; overflow: hidden;"></iframe></div> | |
<p>This CodePen:</p> | |
<ul> | |
<li>Creates a <code><video></code> element that loads a video. <em>Right click and select “Show controls” to see the video’s controls</em>.</li> | |
<li>Loads CanJS's custom element library: <a href="https://canjs.com/doc/can-component.html" title="Create a custom element that can be used to manage widgets or application logic.">Component</a>.</li> | |
</ul> | |
<h3>The problem</h3> | |
<p>In this section, we will:</p> | |
<ul> | |
<li><p>Create a custom <code><video-player></code> element that takes a <code>src</code> attribute and creates a <code><video></code> element | |
within itself. We should be able to create the video like:</p> | |
<pre class="html"><video-player src:raw="http://bit.ly/can-tom-n-jerry"> | |
</video-player></pre></li> | |
<li><p>The embedded <code><video></code> element should have the native controls enabled.</p></li> | |
</ul> | |
<p>When complete, the result will look exactly the same as the player when you started. The | |
only difference is that we will be using a custom <code><video-player></code> element in the <code>HTML</code> | |
tab instead of the native <code><video></code> element.</p> | |
<h3>What you need to know</h3> | |
<p>To set up a basic CanJS application (or widget), you define a custom element in JavaScript and | |
use the custom element in your page’s <code>HTML</code>.</p> | |
<p>To define a custom element, extend <a href="https://canjs.com/doc/can-component.html" title="Create a custom element that can be used to manage widgets or application logic.">Component</a> with a <a href="https://canjs.com/doc/can-component.prototype.tag.html" title="Specifies the HTML tag (or node-name) the can-component will be created on.">tag</a> | |
that matches the name of your custom element. For example:</p> | |
<pre class="js">Component.extend({ | |
tag: "video-player" | |
})</pre> | |
<p>Then you can use this tag in your HTML page:</p> | |
<pre class="html"><video-player></video-player></pre> | |
<p>But this doesn’t do anything ... yet. Components add their own HTML through their <a href="https://canjs.com/doc/can-component.prototype.view.html" title="Provides a view to render directly within the component’s element. The view is rendered with the | |
component’s can-component::ViewModel instance. <content/> elements within the view are replaced by the source elements within the component’s tag.">view</a> | |
property:</p> | |
<pre class="js">Component.extend({ | |
tag: "video-player", | |
view: `<h2>I am a player!</h2>` | |
});</pre> | |
<p>A component’s <a href="https://canjs.com/doc/can-component.prototype.view.html" title="Provides a view to render directly within the component’s element. The view is rendered with the | |
component’s can-component::ViewModel instance. <content/> elements within the view are replaced by the source elements within the component’s tag.">view</a> is rendered with its <a href="https://canjs.com/doc/can-component.prototype.ViewModel.html" title="Provides or describes a constructor function that provides values and methods | |
to the component’s view. The constructor function | |
is initialized with values specified by the component element’s data bindings.">ViewModel</a>. For example, we can make a <code><video></code> display <code>"http://bit.ly/can-tom-n-jerry"</code> by defining a <code>src</code> property on the <code>ViewModel</code> and using it in the <a href="https://canjs.com/doc/can-component.prototype.view.html" title="Provides a view to render directly within the component’s element. The view is rendered with the | |
component’s can-component::ViewModel instance. <content/> elements within the view are replaced by the source elements within the component’s tag.">view</a> like:</p> | |
<pre class="js">Component.extend({ | |
tag: "video-player", | |
view: ` | |
<video> | |
<source src="{{src}}"/> | |
</video> | |
`, | |
ViewModel: { | |
src: {type: "string", default: "http://bit.ly/can-tom-n-jerry"} | |
} | |
});</pre> | |
<p>But we want the <code><video-player></code> to take a <code>src</code> attribute value itself and use that for the | |
<code><source></code>’s <code>src</code>. For example, if | |
we wanted the video to play <code>"http://dl3.webmfiles.org/big-buck-bunny_trailer.webm"</code> instead of <code>"http://bit.ly/can-tom-n-jerry"</code>, we would:</p> | |
<ol> | |
<li>Update <code><video-player></code> to pass <code>"http://dl3.webmfiles.org/big-buck-bunny_trailer.webm"</code> with <a href="https://canjs.com/doc/can-stache-bindings.raw.html" title="One-way bind a string value to the ViewModel or element.">toChild:raw</a>: | |
<pre class="html"><video-player src:raw="http://dl3.webmfiles.org/big-buck-bunny_trailer.webm"/></pre></li> | |
<li>Update the <a href="https://canjs.com/doc/can-component.prototype.ViewModel.html" title="Provides or describes a constructor function that provides values and methods | |
to the component’s view. The constructor function | |
is initialized with values specified by the component element’s data bindings.">ViewModel</a> to define a <code>src</code> property like: | |
<pre class="js" data-line="5">Component.extend({ | |
tag: "video-player", | |
view: ` | |
<video> | |
<source src="{{src}}"/> //👀 | |
</video> | |
`, | |
ViewModel: { | |
src: "string" | |
} | |
});</pre> | |
</ol> | |
<p>Finally, to have a <code><video></code> element show the <em>native</em> controls, add a <code>controls</code> | |
attribute like:</p> | |
<pre class="html"><video controls></pre> | |
<h3>The solution</h3> | |
<p>Update the <strong>JS</strong> tab to:</p> | |
<pre class="js" data-line="3-13">import {Component} from "//unpkg.com/can@5/core.mjs"; | |
Component.extend({ //👀 | |
tag: "video-player", //👀 | |
view: ` {{!👀}} | |
<video controls> {{!👀}} | |
<source src="{{src}}"/> {{!👀}} | |
</video> {{!👀}} | |
`, //👀 | |
ViewModel: { //👀 | |
src: "string", //👀 | |
} //👀 | |
}); //👀 | |
</pre> | |
<p>Update the <strong>HTML</strong> to:</p> | |
<pre class="html" data-line="1"><video-player src:raw="http://bit.ly/can-tom-n-jerry"></video-player> <!--👀--></pre> | |
<div line-highlight="1"></div> | |
<h2 id="Makeplay_pausebuttonchangeasvideoisplayedandpaused">Make play / pause button change as video is played and paused</h2> | |
<h3>The problem</h3> | |
<p>When the video is played or paused using the native controls, we want to change the content of a <code><button></code> | |
to say <em>“Play”</em> or <em>“Pause”</em>.</p> | |
<p>When the video is played, the button should say <em>“Pause”</em>. | |
When the video is paused, the button should say <em>“Play”</em>.</p> | |
<p>We want the button to be within a <code><div></code> after the video element like:</p> | |
<pre class="html"></video> | |
<div> | |
<button>Play</button> | |
</div></pre> | |
<h3>What you need to know</h3> | |
<ul> | |
<li><p>To change the HTML content of the page, use <a href="https://canjs.com/doc/can-stache.helpers.if.html" title="can-stache.helpers.if">{{#if(expression)}}</a> and <a href="https://canjs.com/doc/can-stache.helpers.else.html" title="">{{else}}</a> like:</p> | |
<pre class="html"><button>{{#if(playing)}} Pause {{else}} Play {{/if}}</button></pre></li> | |
<li><p>The <a href="https://canjs.com/doc/can-component.prototype.view.html" title="Provides a view to render directly within the component’s element. The view is rendered with the | |
component’s can-component::ViewModel instance. <content/> elements within the view are replaced by the source elements within the component’s tag.">view</a> responds to values in the <a href="https://canjs.com/doc/can-component.prototype.ViewModel.html" title="Provides or describes a constructor function that provides values and methods | |
to the component’s view. The constructor function | |
is initialized with values specified by the component element’s data bindings.">ViewModel</a>. To create a <code>boolean</code> value in the <a href="https://canjs.com/doc/can-component.prototype.ViewModel.html" title="Provides or describes a constructor function that provides values and methods | |
to the component’s view. The constructor function | |
is initialized with values specified by the component element’s data bindings.">ViewModel</a> do:</p> | |
<pre class="js">ViewModel: { | |
// ... | |
playing: "boolean", | |
}</pre></li> | |
<li><p>Methods can be used to change the <a href="https://canjs.com/doc/can-component.prototype.ViewModel.html" title="Provides or describes a constructor function that provides values and methods | |
to the component’s view. The constructor function | |
is initialized with values specified by the component element’s data bindings.">ViewModel</a>. The following might create methods that change the <code>playing</code> value:</p> | |
<pre class="js">ViewModel: { | |
// ... | |
play() { | |
this.playing = true; | |
}, | |
pause() { | |
this.playing = false; | |
}, | |
}</pre></li> | |
<li><p>You can listen to events on the DOM with <a href="https://canjs.com/doc/can-stache-bindings.event.html" title="Respond to events on elements or component ViewModels.">on:event</a>. For example, the following might | |
listen to a click on a <code><div></code> and call <code>doSomething()</code>:</p> | |
<pre class="html"><div on:click="doSomething()"></pre> | |
<p><code><video></code> elements have a variety of useful <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement">events</a>, including <a href="https://developer.mozilla.org/en-US/docs/Web/Events/play">play</a> and | |
<a href="https://developer.mozilla.org/en-US/docs/Web/Events/pause">pause</a> events that are emitted when the video is played or paused.</p></li> | |
</ul> | |
<h3>The solution</h3> | |
<p>Update the <strong>JavaScript</strong> tab to:</p> | |
<pre class="js" data-line="7-8,11-15,19,21-25">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; //👀 | |
}, //👀 | |
} | |
}); | |
</pre> | |
<div line-highlight="7-8,11-15,19,21-25"></div> | |
<h2 id="Makeclickingtheplay_pausebuttonplayorpausethevideo">Make clicking the play/pause button play or pause the video</h2> | |
<h3>The problem</h3> | |
<p>When the <em>play/pause</em> <code><button></code> we created in the previous section is clicked, we want to | |
either play or pause the video.</p> | |
<h3>What you need to know</h3> | |
<p>CanJS prefers to manage the state of your application in <a href="https://canjs.com/doc/can-component.prototype.ViewModel.html" title="Provides or describes a constructor function that provides values and methods | |
to the component’s view. The constructor function | |
is initialized with values specified by the component element’s data bindings.">ViewModel</a>. The <code><video></code> player has state, such as | |
if the video is <code>playing</code>. When the <em>play/pause</em> button is clicked, we want to update the state | |
of the <a href="https://canjs.com/doc/can-component.prototype.ViewModel.html" title="Provides or describes a constructor function that provides values and methods | |
to the component’s view. The constructor function | |
is initialized with values specified by the component element’s data bindings.">ViewModel</a> and have the <a href="https://canjs.com/doc/can-component.prototype.ViewModel.html" title="Provides or describes a constructor function that provides values and methods | |
to the component’s view. The constructor function | |
is initialized with values specified by the component element’s data bindings.">ViewModel</a> update the state of the video player as a side effect.</p> | |
<p>What this means is that instead of something like:</p> | |
<pre class="js">togglePlay() { | |
if ( videoElement.paused ) { | |
videoElement.play() | |
} else { | |
videoElement.pause() | |
} | |
}</pre> | |
<p>We update the state like:</p> | |
<pre class="js">togglePlay() { | |
this.playing = !this.playing; | |
}</pre> | |
<p>And listen to when <code>playing</code> changes and update the <code>video</code> element like:</p> | |
<pre class="js">viewModel.listenTo("playing", function(event, isPlaying) { | |
if ( isPlaying ) { | |
videoElement.play() | |
} else { | |
videoElement.pause() | |
} | |
})</pre> | |
<p>This means that you need to:</p> | |
<ol> | |
<li>Listen to when the <code><button></code> is clicked and call a ViewModel method that updates the <code>playing</code> state.</li> | |
<li>Listen to when the <code>playing</code> state changes and update the state of the <code><video></code> element.</li> | |
</ol> | |
<p>You already know everything you need to know for step <strong>#1</strong>. (Have the button call a <code>togglePlay</code> method with <code>on:click="togglePlay()"</code> and make the <code>togglePlay()</code> method toggle the state of the <code>playing</code> property.)</p> | |
<p>For step <strong>#2</strong>, you need to use the <a href="https://canjs.com/doc/can-component/connectedCallback.html" title="A lifecycle hook called after the component's element is inserted into the document.">connectedCallback</a> 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:</p> | |
<pre class="js">ViewModel: { | |
// ... | |
connectedCallback(element) { | |
// perform mischief | |
} | |
}</pre> | |
<p><code>connectedCallback</code> gets called once the component’s <code>element</code> is in the page. You can use | |
<a href="https://canjs.com/doc/can-event-queue/map/map.listenTo.html" title="Listen to an event and register the binding for simplified unbinding.">listenTo</a> to listen to changes in the <a href="https://canjs.com/doc/can-component.prototype.ViewModel.html" title="Provides or describes a constructor function that provides values and methods | |
to the component’s view. The constructor function | |
is initialized with values specified by the component element’s data bindings.">ViewModel</a>’s properties and | |
perform side-effects. The following listens to when <code>playing</code> changes:</p> | |
<pre class="js">ViewModel: { | |
// ... | |
connectedCallback(element) { | |
this.listenTo("playing", function(event, isPlaying) { | |
}) | |
} | |
}</pre> | |
<p>Use <code>querySelector</code> to get the <code><video></code> element from the <code><video-player></code> like:</p> | |
<pre class="js">element.querySelector("video")</pre> | |
<p><code><video></code> elements have a <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play">.play()</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/pause">.pause()</a> methods that can start and stop a video.</p> | |
<h3>The solution</h3> | |
<p>Update the <strong>JavaScript</strong> tab to:</p> | |
<pre class="js" data-line="12,27-30,31-39">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(); //👀 | |
} //👀 | |
}); //👀 | |
} //👀 | |
} | |
}); | |
</pre> | |
<div line-highlight="12,27-30,31-39"></div> | |
<h2 id="Showcurrenttimeandduration">Show current time and duration</h2> | |
<h3>The problem</h3> | |
<p>Show the current time and duration of the video element. The time and duration should be | |
formatted like: <code>mm:SS</code>. They should be presented within two spans like:</p> | |
<pre class="js"></button> | |
<span>1:22</span> | |
<span>2:45</span></pre> | |
<h3>What you need to know</h3> | |
<ol> | |
<li><p>Methods can be used to format values in <a href="https://canjs.com/doc/can-stache.html" title="Live binding Mustache and Handlebars-compatible templates.">can-stache</a>. For example, you can uppercase values like this:</p> | |
<pre class="html"><span>{{upper(value)}}</span></pre> | |
<p>With a method like:</p> | |
<pre class="js">ViewModel: { | |
// ... | |
upper(value) { | |
return value.toString().toUpperCase(); | |
} | |
}</pre> | |
<p>The following can be used to format time:</p> | |
<pre class="js">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; | |
}</pre></li> | |
<li><p>Time is given as a number. Use the following to create a number property on | |
the <a href="https://canjs.com/doc/can-component.prototype.ViewModel.html" title="Provides or describes a constructor function that provides values and methods | |
to the component’s view. The constructor function | |
is initialized with values specified by the component element’s data bindings.">ViewModel</a>:</p> | |
<pre class="js">ViewModel: { | |
// ... | |
duration: "number", | |
currentTime: "number" | |
}</pre></li> | |
<li><p><code><video></code> elements emit a <a href="https://developer.mozilla.org/en-US/docs/Web/Events/loadedmetadata">loadmetadata event</a> when they know how long | |
the video is. They also emit a <a href="https://developer.mozilla.org/en-US/docs/Web/Events/timeupdate">timeupdate event</a> when the video’s current play position | |
changes.</p> | |
<ul> | |
<li><code>videoElement.duration</code> reads the duration of a video.</li> | |
<li><code>videoElement.currentTime</code> reads the current play position of a video.</li> | |
</ul></li> | |
<li><p>You can get the element in an stache <code>on:event</code> binding with <a href="https://canjs.com/doc/can-stache/keys/scope.html#scope_element" title="The template context">scope.element</a> like:</p> | |
<pre class="html"><video on:timeupdate="updateTimes(scope.element)"/></pre></li> | |
</ol> | |
<h3>The solution</h3> | |
<p>Update the <strong>JavaScript</strong> tab to:</p> | |
<pre class="js" data-line="9,10,17,18,24-41">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(); | |
} | |
}); | |
} | |
} | |
}); | |
</pre> | |
<div line-highlight="9,10,17,18,24-41"></div> | |
<h2 id="Makerangeshowpositionslideratcurrenttime">Make range show position slider at current time</h2> | |
<h3>The problem</h3> | |
<p>Create a <code><input type="range"/></code> element that changes its position as | |
the video playing position changes.</p> | |
<p>The <code><input type="range"/></code> element should be after the <code><button></code> and before the | |
<code>currentTime</code> span like:</p> | |
<pre class="html" data-line="2"></button> | |
<input type="range"/> | |
<span>{{formatTime(currentTime)}}</span> /</pre> | |
<div line-highlight="2"></div> | |
<h3>What you need to know</h3> | |
<ul> | |
<li><p>The range input can have an initial value, max value, and step size | |
specified like:</p> | |
<pre class="html"><input type="range" value="0" max="1" step="any"/></pre></li> | |
<li><p>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 <a href="https://canjs.com/doc/can-define.types.get.html" title="Specify what happens when a certain property is read on a map. get functions | |
work like a can-compute and automatically update themselves when a dependent | |
observable value is changed.">computed getter property</a> like:</p> | |
<pre class="js">ViewModel: { | |
// ... | |
get percentComplete() { | |
return this.currentTime / this.duration; | |
}, | |
}</pre></li> | |
<li><p>Use <a href="https://canjs.com/doc/can-stache-bindings.toChild.html" title="One-way bind a value in the parent scope to the ViewModel or element.">key:from</a> to update a value from a <a href="https://canjs.com/doc/can-component.prototype.ViewModel.html" title="Provides or describes a constructor function that provides values and methods | |
to the component’s view. The constructor function | |
is initialized with values specified by the component element’s data bindings.">ViewModel</a> property like:</p> | |
<pre class="html"><input value:from="percentComplete"/></pre></li> | |
</ul> | |
<h3>The solution</h3> | |
<p>Update the <strong>JavaScript</strong> tab to:</p> | |
<pre class="js" data-line="17-18,29-31">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(); | |
} | |
}); | |
} | |
} | |
}); | |
</pre> | |
<div line-highlight="17-18,29-31"></div> | |
<h2 id="Makeslidingtherangeupdatethecurrenttime">Make sliding the range update the current time</h2> | |
<h3>The problem</h3> | |
<p>In this section we will:</p> | |
<ul> | |
<li>Remove the native controls from the video player. We don’t need them anymore!</li> | |
<li>Make it so when a user moves the range slider, the video position updates.</li> | |
</ul> | |
<h3>What you need to know</h3> | |
<p>Similar to when we <a href="#Makeclickingtheplay_pausebuttonplayorpausethevideo">made the play/pause button play or pause the video</a>, we will want to update the | |
<code>currentTime</code> property and then listen to when <code>currentTime</code> changes and update the <code><video></code> | |
element’s <code>currentTime</code> as a <em>side-effect</em>.</p> | |
<p>This time, we need to translate the sliders values between 0 and 1 to <code>currentTime</code> | |
values. We can do this by creating a <code>percentComplete</code> <a href="https://canjs.com/doc/can-define.types.set.html" title="Specify what happens when a property value is set.">setter</a> that updates <code>currentTime</code> like:</p> | |
<pre class="js">ViewModel: { | |
// ... | |
get percentComplete() { | |
return this.currentTime / this.duration; | |
}, | |
set percentComplete(newVal) { | |
this.currentTime = newVal * this.duration; | |
}, | |
// ... | |
}</pre> | |
<p>Use <a href="https://canjs.com/doc/can-stache-bindings.twoWay.html" title="Two-way bind a value in the viewModel or the element to the parent scope.">key:bind</a> to two-way bind a value to a <a href="https://canjs.com/doc/can-component.prototype.ViewModel.html" title="Provides or describes a constructor function that provides values and methods | |
to the component’s view. The constructor function | |
is initialized with values specified by the component element’s data bindings.">ViewModel</a> property:</p> | |
<pre class="html"><input value:bind="someViewModelProperty"/></pre> | |
<h3>The solution</h3> | |
<p>Update the <strong>JavaScript</strong> tab to:</p> | |
<pre class="js" data-line="6,18,32-34,69-74">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; | |
} | |
}); | |
} | |
} | |
}); | |
</pre> | |
<div line-highlight="6,18,32-34,69-74"></div> | |
<h2 id="Result">Result</h2> | |
<p>When finished, you should see something like the following JS Bin:</p> | |
<div class="cp_embed_wrapper"><iframe id="cp_embed_qyRqMx" src="//codepen.io/justinbmeyer/embed/qyRqMx?height=360&theme-id=dark&slug-hash=qyRqMx&default-tab=js%2Cresult&user=justinbmeyer&embed-version=2&pen-title=CanJS%205.0%20Video%20Player%20-%20Final" scrolling="no" frameborder="0" height="360" allowtransparency="true" allowfullscreen="true" allowpaymentrequest="true" name="CodePen Embed" title="CanJS 5.0 Video Player - Final" class="cp_embed_iframe " style="width: 100%; overflow: hidden;"></iframe></div> | |
<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment