2
\$\begingroup\$

I'm learning BackboneJS and I just made an attempt at converting a pre-existing module to a Backbone.View. I was hoping to get some feedback on my attempt and learn. I've been using the annotated ToDo source as a guide.

Here's some HTML to give you a rough idea:

<div id="VolumeControl">
    <div id="MuteButton" class="volumeControl" title="Toggle Volume">
        <svg width="16" height="16">
            <path d="M0,6 L3,6 L7,2 L7,14 L3,10 L0,10Z" fill="#fff" />
            <rect class="MuteButtonBar" id="MuteButtonBar1" x="9" y="6.5" width="1" height="3" />
            <rect class="MuteButtonBar"id="MuteButtonBar2" x="11" y="5" width="1" height="6" />
            <rect class="MuteButtonBar" id="MuteButtonBar3" x="13" y="3.5" width="1" height="9" />
            <rect class="MuteButtonBar" id="MuteButtonBar4" x="15" y="2" width="1" height="12" />
        </svg>
    </div>

    <div id="VolumeSliderWrapper" class="volumeControl">
        <input type="range" id="VolumeSlider" class="volumeControl" title="Click or drag to change the volume." min="0" max="100" step="1" value="0" />
    </div>
</div>

It's essentially a two-part control consisting of a mute button and an HTML5 range slider which expands out.

Here's a quick screenshot to bring things together mentally:

enter image description here

Here's my Backbone.View:

//  Responsible for controlling the volume indicator of the UI.
define(['player'], function (player) {
    'use strict';

    var volumeControlView = Backbone.View.extend({
        el: $('#VolumeControl'),

        events: {
            'change #VolumeSlider': 'setVolume',
            'click #MuteButton': 'toggleMute',
            'mousewheel .volumeControl': 'scrollVolume',
            'mouseenter .volumeControl': 'expand',
            'mouseleave': 'contract'
        },

        render: function () {
            var volume = player.get('volume');

            //  Repaint the amount of white filled in the bar showing the distance the grabber has been dragged.
            var backgroundImage = '-webkit-gradient(linear,left top, right top, from(#ccc), color-stop(' + volume / 100 + ',#ccc), color-stop(' + volume / 100 + ',rgba(0,0,0,0)), to(rgba(0,0,0,0)))';
            this.volumeSlider.css('background-image', backgroundImage);

            var activeBars = Math.ceil((volume / 25));
            this.muteButton.find('.MuteButtonBar:lt(' + (activeBars + 1) + ')').css('fill', '#fff');
            this.muteButton.find('.MuteButtonBar:gt(' + activeBars + ')').css('fill', '#666');

            if (activeBars === 0) {
                this.muteButton.find('.MuteButtonBar').css('fill', '#666');
            }

            var isMuted = player.get('muted');

            if (isMuted) {
                this.muteButton
                    .addClass('muted')
                    .attr('title', 'Click to unmute.');
            } else {
                this.muteButton
                    .removeClass('muted')
                    .attr('title', 'Click to mute.');
            }

            return this;
        },

        //  Initialize player's volume and muted state to last known information or 100 / unmuted.
        initialize: function () {
            this.volumeSliderWrapper = this.$('#VolumeSliderWrapper');
            this.volumeSlider = this.$('#VolumeSlider');
            this.muteButton = this.$('#MuteButton');

            //  Set the initial volume of the control based on what the YouTube player says is the current volume.
            var volume = player.get('volume');
            this.volumeSlider.val(volume).trigger('change');

            this.listenTo(player, 'change:muted', this.render);

            this.render();
        },

        //  Whenever the volume slider is interacted with by the user, change the volume to reflect.
        setVolume: function () {

            var newVolume = parseInt(this.volumeSlider.val(), 10);
            player.set('volume', newVolume);

            this.render();
        },

        //  Adjust volume when user scrolls mousewheel while hovering over volumeControl.
        scrollVolume: function (event, delta) {
            //  Convert current value from string to int, then go an arbitrary, feel-good amount of volume points in a given direction (thus *3 on delta).
            var newVolume = parseInt(this.volumeSlider.val(), 10) + delta * 3;
            this.volumeSlider.val(newVolume).trigger('change');
        },

        toggleMute: function () {
            var isMuted = player.get('muted');
            player.set('muted', !isMuted);
        },

        //  Show the volume slider control by expanding its wrapper whenever any of the volume controls are hovered.
        expand: function () {
            this.volumeSliderWrapper.addClass('expanded');
        },

        contract: function () {
            this.volumeSliderWrapper.removeClass('expanded');
        }

    });

    var volumeControl = new volumeControlView;
})

Am I doing too much in render? Anything look weird?

\$\endgroup\$
4
  • \$\begingroup\$ RequireJS, correct? \$\endgroup\$ – Johntron Jun 5 '13 at 19:53
  • \$\begingroup\$ Hmm? Yes, the example uses RequireJS as indicated in the tags, but that's a pretty simple wrapper. :) \$\endgroup\$ – Sean Anderson Jun 5 '13 at 21:44
  • \$\begingroup\$ I just wanted to make sure. So why are you defining player as a dependency of the volumeControlView class rather than a parameter to the constructor? I'm not saying this is wrong, I've just never seen this. I've always seen the module define a class and export/return the class for other code to instantiate, passing in the appropriate parameters (e.g. player). \$\endgroup\$ – Johntron Jun 6 '13 at 14:36
  • \$\begingroup\$ I could be wrong, but this looks to be very similar to a singleton. \$\endgroup\$ – Johntron Jun 6 '13 at 14:38
1
\$\begingroup\$

I think this is fine for something as simple as a volume control; however, there are some limitations to at least be aware of:

  1. Since RequireJS invokes the module, it would be problematic to construct player dynamically.
  2. There's no good way of creating more than one instance of your view – probably not a problem.
  3. The View is tightly bound to a specific DOM structure. This means it will require extra code to make your View responsive. e.g. a small volume control for mouses (desktop) and a big one for fingers (mobile).

Here are some potential solutions:

  1. Likely, player just has a single default state, but if you ever want to construct this object yourself, you should consider return-/exporting the volumeControlView definition from your module, rather than returning an instance of it.
  2. A simple solution here is to simply return the result of Backbone.View.extend.
  3. Use a template. In the future, you can use additional templates to support other platforms. e.g. start with a desktop template, later on create a mobile template, and choose the template dynamically at runtime based on the environment.

I would define your module like so (uses RequireJS text plugin):

define(['Backbone', 'underscore', 'text!templates/volume-bar.html'], function (Backbone, _, volume_bar) {
    'use strict';

    return Backbone.View.extend({
        template: _.template(volume_bar),

        // ...

    });
});

This lets you instantiate the view like so:

require(['models/Player', 'views/VolumeBar', function(PlayerModel, VolumeBarView) {
    'use strict';

    var player = new PlayerModel({...});
    var volume = new VolumeBarView({
        model: player
    });
    volume.render().$el.appendTo('#player');
});

Doing so would change how you bind your events and render your HTML, so I'm leaving it at this just to give you the general idea.

\$\endgroup\$
2
  • \$\begingroup\$ Heya. Could you elaborate on your first point a bit more, please? I don't understand what 'construct a player dynamically' means in this context. For my purposes, the player module is hosted in a background page and, as a business rule, must exist and be ready before this module is loaded. This volumeControl has no need to interact with other objects. Its only goal is to send "i was interacted upon" events to the player such that it can respond to the UI being interacted upon. I agree that a template would be nice -- this is a Google Chrome Extension, but porting is an option. \$\endgroup\$ – Sean Anderson Jun 7 '13 at 6:11
  • \$\begingroup\$ If you define your view to require player, you're demanding that RequireJS load the module for you. This means you have to instantiate player in the same place you define it. This is fine so long as player doesn't require anything outside of it's scope to instantiate; however, should the player's construction ever rely on some external factor (e.g. current user, connection speed, browser features, etc.), you'll have to inject these dependencies into the player module rather than implementing them somewhere more appropriate like in a parent. Honestly, I don't think this will be a problem. \$\endgroup\$ – Johntron Jun 11 '13 at 1:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.