Using the Presenter Pattern with CanJS for Greater Reusability

Posted on December 12, 2013

Let's say you have a template and a data model that you use everywhere but you need to present it in a bazillion different ways. Do you need to make a bazillion different templates? Probably not! Do you need to make a bazillion different components? Well... maybe. But if you do they should be as simple as possible.

What you need is the Presenter Pattern!

In this JS Fiddle I use the same template and the same data model to create two nearly identical components but with different presentation strategies. You still get the goodness of live binding (as demonstrated with the interval) and you get a lot of code re-use, too.

Here's the code:


(function () {
    var VideoWithTimePresenter = {
        videoTitle: function (video) {
            return [video.attr("title"), " ",
                "(", this.secondsToTime(video.attr("duration")), ")"].join("");
        },

        videoUrl: function (video) {
            return ["/video/", video.attr("code")].join("");
        },

        secondsToTime: function (secs) {
            secs = parseInt(secs, 10);
            var hours = Math.floor(secs / (60 * 60));

            var divisor_for_minutes = secs % (60 * 60);
            var minutes = Math.floor(divisor_for_minutes / 60);

            var divisor_for_seconds = divisor_for_minutes % 60;
            var seconds = Math.ceil(divisor_for_seconds);

            var pad = function (n) {
                return ("0" + n).slice(-2);
            };

            var obj = {
                "h": pad(hours),
                    "m": pad(minutes),
                    "s": pad(seconds)
            };

            var arr = obj["h"] === "00" ? [] : [obj["h"]];
            return arr.concat([obj["m"], obj["s"]]).join(":");
        }
    };
    
    var VideoBackwardsPresenter = {
        videoTitle: function (video) {
            return video.attr("title").split("").reverse().join("");                
        },

        videoUrl: function (video) {
            return "#";
        }
    };    

    var Video = can.Model.extend({
        findAll: "GET /videos"
    }, {});

    can.fixture("GET /videos", function () {
        return [{
            id: 1,
            code: "santa_run_over_by_raindeer",
            title: "Santa Has Been Run Over By a Raindeer",
            duration: 340
        }, {
            id: 2,
            code: "raindeer_slays_santa",
            title: "Local Raindeer Slays Santa",
            duration: 122
        }];
    });

    can.Component.extend({
        tag: "video-list-with-time",
        template: can.view("video_list_template"),
        scope: VideoWithTimePresenter
    });
    
    can.Component.extend({
        tag: "video-list-backwards",
        template: can.view("video_list_template"),
        scope: VideoBackwardsPresenter
    });

    Video.findAll({}, function (videos) {
        $("#app").html(can.view("app_template", {
            videos: videos
        }));

        setInterval(function() {
            videos[0].attr("title", videos[0].title += "... and again");
            videos[0].attr("duration", videos[0].duration += 20);
        }, 1500);
    });
})();

Template:


<div id="app"></div>

<script type="text/mustache" id="app_template">
<video-list-with-time></video-list-with-time>

<video-list-backwards></video-list-backwards>
</script>

<script type="text/mustache" id="video_list_template">
Videos:
<table>
<tbody>
{{ #each videos }}
<tr>
<td class="title">
<a href="{{ videoUrl }}">
{{ videoTitle }}
</a>
</td>
</tr>
{{ /each }}
</tbody>
</table>
</script>
comments powered by Disqus