CanJS - Recipes

canjs

Pretty Date:

<div id="todo"></div>

<!-- PUT ANY TEMPLATES YOU NEED HERE -->
<script id="app-template" type="text/mustache">
    <h1><b>{{ message }}</b> 
        <i>created {{ prettyDate createdAt }}</i></h1>
</script>
(function() {
    // create a observe that has a property with the date right now
    var data = new can.Map({
        message: "Hello World",
        createdAt: new Date()
    })

    var now = can.compute( new Date() )

    // update that property every second        
    setTimeout(function(){
        now( new Date() );
        setTimeout(arguments.callee, 1000);
    },1000)

    // create a prettyDate helper
    var dateHelper = function(date){
        // compare the date passed in with 'now'
        var timeElapsed = ( now() - date() ) / 1000;
        if(timeElapsed < 1.2){
            return "a second ago"
        } else if (timeElapsed < 10 ) {
            return "a couple seconds ago"
        } else if (timeElapsed < 20 ) { 
            return "some seconds ago"
        } else if (timeElapsed < 30 ){
            return "a few seconds ago"
        }  else if (timeElapsed < 40 ) {
            return "a half min ago"
        } else {
            return Math.round(timeElapsed/60)+" min ago"
        }
    }

     var frag = can.view("app-template", data, {prettyDate: dateHelper});
    $("#todo").html(frag);    

})();

See http://canjs.com/guides/CreateALiveTimestamp.html

Update Text in the Page:

<script type="text/mustache" id="app-template">
    <h1>{{message}}</h1>
    <button id="change-value">Change message to "Goodbye, World"</button>
</script>

<!-- CanJS needs a place to put your application -->
<div id="my-app"></div>
// create an observable can.Map from the data
var data = new can.Map({message: "Hello World!"});

var frag = can.view("app-template", data);

//Load the DocumentFragment in the page
$("#my-app").html( frag );

$("#change-value").click(function(){
    data.attr("message", "Goodbye World!")
})

See http://canjs.com/guides/UpdateText.html

Pass message to the template:

<script type="text/mustache" id="app-template">
    <h1>{{message}}</h1>
</script>

<!-- CanJS needs a place to put your application -->
<div id="my-app"></div>
// Give message a value
var data = {message: "Hello World!"};

// Pass the id of the template and the data, containing our message to can.view
var frag = can.view("app-template", data);

//Load the DocumentFragment in the page
$("#my-app").html( frag );

Show A List in A Template:

<script type="text/mustache" id="app-template">
<button id="push">Add a new person to the list</button>
<button id="pop">Remove someone from the list</button>  
<ul>
{{#each people}}
    <li>
    {{lastname}}, {{firstname}}
    </li>
{{/each}}
</ul>  
</script>
<!-- CanJS needs a place to put your application -->
<div id="my-app"></div>
var people = new can.List([
        {firstname: "John", lastname: "Doe"},
        {firstname: "Emily", lastname: "Dickinson"}
])

// pass the observable list into the can.view
var frag = can.view("app-template", {people: people})
$("#my-app").html(frag);

$("#push").click(function(){
    people.push({firstname: "Paul", lastname: "Newman"})
})

$("#pop").click(function(){
    people.pop();
})

Show or Hide Elements:

<script type="text/mustache" id="app-template">
  <button id="toggle">Toggle</button>
  {{#if visible}}
    <h1>{{message}}</h1>
  {{/if}}
</script>

<!-- CanJS needs a place to put your application -->
<div id="my-app"></div>
// Give message a value
var data = new can.Map({
    message: "Hello World!",
    visible: true
});

// Pass the id of the template and the data, containing our message to can.view
var frag = can.view("app-template", data );

//Load the DocumentFragment in the page
$("#my-app").html( frag );

$("#toggle").click(function(){
  data.attr("visible", ! data.attr("visible") );
})

See http://canjs.com/guides/ShowAndHideElements.html

Handle User Interaction:

<script type="text/mustache" id="app-template">
<ul>
{{#each people}}
    <li {{data 'person'}}>
    {{lastname}}, {{firstname}}
    </li>
{{/each}}
</ul>  
</script>
<!-- CanJS needs a place to put your application -->
<div id="my-app"></div>
var people = [
    {firstname: "John", lastname: "Doe"},
    {firstname: "Emily", lastname: "Dickinson"},
    {firstname: "William", lastname: "Adams"},
    {firstname: "Stevie", lastname: "Nicks"},
    {firstname: "Bob", lastname: "Barker"}
];

var PeopleList = can.Control.extend({
    init: function( el, op ){
         this.options.people = new can.List(op.people);
         this.element.html( can.view('app-template', {
             people: this.options.people
        }));
    },
    'li click': function( li, event ) {
           var people = this.options.people;
           var person = li.data('person');
           var index = people.indexOf(person);
           people.splice(index, 1);
    }
});

new PeopleList('#my-app', {people: people});

See http://canjs.com/guides/HandleUserInteraction.html

Build Widgets/UI Elements:

<script type="text/mustache" id="app-template">
<tabs>
    <panel title="Fruit">Oranges, Apples, Pears</panel>
    <panel title="Vegetable">Carrot, Lettuce, Rutabega</panel>
    <panel title="Grains">Bread, Pasta, Rice</panel>
</tabs>
</script>
<!-- CanJS needs a place to put your application -->
<div id="my-app"></div>
var TabsViewModel = can.Map.extend({
    panels: [],
    active: null,
    addPanel: function( panel ){
        var panels = this.attr("panels");
        panels.push(panel);
        panel.attr("visible", false);
        //activate panel if it is the first one
        if ( panels.attr("length") === 1 ){
            this.activate( panel );
        }
    },
    removePanel: function( panel ){
        var panels = this.attr("panels");
        var index = panels.indexOf(panel);
        panels.splice(index, 1);
        //activate a new panel if panel being removed was the active panel
        if( this.attr("active") === panel ){
            panels.attr("length") ? this.activate(panels[0]) : this.attr("active", null)
        }
    },
    activate: function( panel ){
        var active = this.attr("active")
        if( active !== panel ){
            active && active.attr("visible", false);
            this.attr("active", panel.attr("visible", true));
        }
    }
})

can.Component.extend({
    tag: "tabs",
    scope: TabsViewModel,
    template: "<ul>\
    {{#each panels}}\
    <li can-click='activate'>{{title}}</li>\
    {{/each}}\
    </ul>\
    <content />"
});

can.Component.extend({
    tag: "panel",
    template: "{{#if visible}}<content />{{/if}}",
    scope: {
        title: "@"
    },
    events: {
        inserted: function() {
            this.element.parent().scope().addPanel( this.scope )
        },
        removed: function() {
            this.element.parent().scope().addPanel( this.scope )
        }
    }
});

var frag = can.view("app-template", {});
$("#my-app").html(frag);

Build an App with Remote Data:

<script type="text/mustache" id="app-template">
    <chat></chat>
</script>
<div id="my-app"></div>
<script src='http://agile-everglades-1937.herokuapp.com/socket.io/socket.io.js'></script>
var myServerUrl = "http://agile-everglades-1937.herokuapp.com";

// define our message model
var Message = can.Model({
    findAll : 'GET ' + myServerUrl + '/messages',
    create : function(attrs) {
        $.post(myServerUrl + '/messages', attrs)
        //keep '{Message} created' from firing twice
        return $.Deferred()
    }
},{});

can.Component.extend({
    tag: 'chat',
    template: '<ul id="messages">' +
                '{{#each messages}}' +
                '<li>{{body}}</li>' +
                '{{/each}}' +
              '</ul>' +
              '<form id="create-message" action="" can-submit="submitMessage">' +
                  '<input type="text" id="body" placeholder="type message here..." can-value="newMessage" />' +
              '</form>',
    scope: {    
        Message: Message,
        messages: new Message.List({}),
        submitMessage: function(el, ev, ev2){
            ev2.preventDefault();
            new Message({body: this.attr("newMessage")}).save();
            this.attr("newMessage", "");
        }
    },
    events: {
        '{Message} created': function(construct, ev, message){
            this.scope.attr('messages').push(message);
        }
    }
});

var frag = can.view('app-template', {});
$('#my-app').html(frag);

// connect using Socket.io
var socket = io.connect(myServerUrl)
// listen for 'message-created' event to create a Message instance
// Note: the 'created' event IS triggered on this model instance. 
socket.on('message-created', function(message){
    new Message(message).created();
})

TreeCombo:

// HTML:
<div id="treeCombo"></div>
<div id="selected"></div>

<script id="treeEJS" type="text/ejs">
<ul class='breadcrumb'>
    <li><%= title %></li>
    <% breadcrumb.each(function(item){ %>
        <li <%=(el)-> el.data('item',item) %>>
          <%= item.attr('title') %>
        </li>
    <%})%>
</ul>

<ul class='options'>
    <% selectableItems().each(function(item){ %>
        <li class='<%= selected.indexOf(item) >= 0 ? "checked":""%>'
            <%=(el)-> el.data('item',item) %> >
            <input type="checkbox"
                   <%= selected.indexOf(item) >= 0 ? "checked":""%>>

            <%= item.attr('title') %>

            <%if(item.children && item.children.length){ %>
                <button class="showChildren">→</button>
            <%}%>
        </li>
    <% }) %>
</ul>
</script>
<script id='selectedEJS' type='text/ejs'>
  You've selected:

  <ul>
    <% if(this.attr('length')){%>
      <% this.each(function(item){ %>
        <li><%=item.attr('title')%></li>
      <% }) %>
    <% } else { %>
      <li>Nothing</li>
    <% } %>
  </ul>
</script>

// JavaScript:
// the json data we will be using
var data = [
  { id: 1, title: "Midwest", children: [
    { id: 5, title: "Illinois", children: [
      { id: 23423, title: "Chicago"}, { id: 4563, title: "Springfield"},
      { id: 4564, title: "Naperville"} 
      ]
    },
    { id: 6, title: "Wisconsin", children: [
      { id: 232423, title: "Milwaulkee"}, { id: 45463, title: "Green Bay"},
      { id: 45464, title: "Madison"} 
      ]
    }]
  },
  { id: 2, title: "East Coast", children: [
    { id: 25, title: "New York", children: [
      { id: 3413, title: "New York"}, { id: 4613, title: "Rochester"},
      { id: 4516, title: "Syracuse"} 
      ]
    },
    { id: 6, title: "Pennsylvania", children: [
      { id: 2362423, title: "Philadelphia"}, { id: 454663, title: "Harrisburg"},
      { id: 454664, title: "Scranton"} 
      ]
    }]
  },
  { id: 3, title: "West Coast", children: [
    { id: 75, title: "California", children: [
      { id: 347813, title: "Los Angeles"}, { id: 463213, title: "Sunnyvale"},
      { id: 457516, title: "San Diego"} 
      ]
    },
    { id: 66, title: "Oregon", children: [
      { id: 23621, title: "Bend"}, { id: 1111, title: "Salem"},
      { id: 45461, title: "Eugene"} 
      ]
    }]
  },
  { id: 4, title: "South", children: [
    { id: 65, title: "Texas", children: [
      { id: 343413, title: "Austin"}, { id: 463413, title: "Dalas"},
      { id: 498516, title: "El Paso"} 
      ]
    },
    { id: 6, title: "Florida", children: [
      { id: 2360, title: "Orlando"}, { id: 4540, title: "Tampa"},
      { id: 4064, title: "Miami"} 
      ]
    }]
  }];

// The TreeCombo control
var TreeCombo = can.Control({
  init : function(){
    // contains items in the breadcrumb
    this.options.breadcrumb = new can.Observe.List([]);

    // items we should be displaying
    var selectableItems = can.compute(function(){

      // if there's an item in the breadcrumb
      if(this.options.breadcrumb.attr('length')){

        // return the last item's children
        return this.options.breadcrumb
           .attr(""+(this.options.breadcrumb.length-1))
           .attr('children');
      } else{

        // return the top list of items
        return this.options.items;
      }
    }, this);

    // render the template
    this.element.html( can.view('treeEJS',{
      breadcrumb: this.options.breadcrumb,
      selectableItems: selectableItems,
      title: this.options.title,
      selected: this.options.selected
    }));

  },
  ".showChildren click": function(el, ev){
    // add the item to the breadcrumb
    this.options.breadcrumb.push(el.closest('li').data('item'));
    // prevents selection
    ev.stopPropagation();
  },
  ".breadcrumb li click": function(el){
    var item = el.data('item');
    // if you clicked on a breadcrumb li with data
    if(item){
      // remove all breadcrumb items after it
      var index = this.options.breadcrumb.indexOf(item);
      this.options.breadcrumb.splice(index+1, 
                                     this.options.breadcrumb.length - index-1)
    } else {
      // clear the breadcrumb
      this.options.breadcrumb.replace([])
    }

  },
  ".options li click": function(el){
    // toggles an item's existance in the selected array
    var item = el.data('item'),
        index = this.options.selected.indexOf(item);
    if(index === -1 ){
      this.options.selected.push(item);
    } else {
      this.options.selected.splice(index, 1) 
    }
  }

});

var selected = new can.Observe.List();

new TreeCombo("#treeCombo", {
  items: new can.Observe.List(data),
  title: "Locations",
  selected: selected
});

$("#selected").html(can.view('selectedEJS', selected));

Show more:

// HTML:
<article>
    <h1 class="post-title">Weekly Widget 1 -&nbsp;TreeCombo</h1>
    <div class="author-time">18 January 2013 by justinbmeyer</div>
    <p>I, <a href="http://twitter.com/justinbmeyer">@justinbmeyer</a>, am 
        going to post a weekly widget made with CanJS. I hope to continue
        this for as long as I have widgets to write. If you 
        <strong>want to see something</strong>, please tweet it to 
        <a href="http://twitter.com/justinbmeyer">@canjs</a>. These posts
        are going to be quick and dirty. Eventually, I will put 
        these up on <a href="http://canjs.us/recipes.html">CanJS’s recipe page</a> with
        a full description.
    </p>
    <p>I’m starting with the <code>TreeCombo</code> below:</p>

    <iframe style="width: 100%; height: 300px" src="http://jsfiddle.net/sTLhX/35/embedded/result,html,js"         
           allowfullscreen="allowfullscreen" frameborder="0">JSFiddle</iframe>

    <h2 id="what-it-does">What it does</h2>

    <p>The tree combo allows you to select items within and
    navigate through a hierarchial collection
    of data. Click “→” to see child items. Click the
    breadcrumb navigation at the top to return to parent items.</p>

    <p>It also displays a list of the items the user has selected under

        “You’ve selected:”.</p>
</article>
<article>
    <h1 class="post-title">Weekly Widget 2 - 2-Way Mustache&nbsp;Helpers
    </h1>
    <div class="author-time">27 January 2013 by justinbmeyer</div>
    <p>This article shows how to make 2-way binding mustache helpers
        and use them in a basic form.</p>

    <p>By default, CanJS supports 1-way binding templates. 
        1-way binding means that if data changes,
        the template is automatically updated. For example,
        if <code>attendee.name</code> changes, the following input’s value is updated:
    </p>

    <pre><code class="xml"><span class="tag">&lt;<span class="title">input</span> <span
        class="attribute">type</span>=<span class="value">'text'</span> <span 
        class="attribute">value</span>=<span 
        class="value">"{{attendee.name}}"</span>/&gt;</span>
    </code></pre>

    <p>2-way binding means the reverse is also true. If the user
    types a new value, the <code>attendee.name</code> is automatically updated. 
    This article shows how roll your own
    2-way mustache helpers that can be used like:</p>

    <pre><code class="xml"><span class="tag">&lt;<span class="title">input</span> <span 
        class="attribute">type</span>=<span class="value">'text'</span> {{<span 
        class="attribute">value</span> <span
        class="attribute">attendee.name</span>}}/&gt;</span>
<span 
        class="tag">&lt;<span class="title">input</span> <span 
        class="attribute">value</span>=<span class="value">"yes"</span> <span 
        class="attribute">type</span>=<span class="value">"radio"</span> {{<span 
        class="attribute">checked</span> <span 
            class="attribute">attendee.attending</span>}}/&gt;</span>
        </code></pre>
</article>
<article>
    <h1 class="post-title">Weekly Widget 3 - Paginated&nbsp;Grid
    </h1>
    <div class="author-time">01 February 2013 by justinbmeyer</div>
    <p>This weekly widget was suggested 
    by <a href="https://twitter.com/Cherif_b/status/296765112386203649">@Cherif_b</a>.
    The article covers how to create a basic paginated
    grid. But even more important, it gives a glipse
    at how we organize large applications. </p>
    <h2 id="the-app">The app</h2>
    <p>The app allows you to paginate through a list of links:</p>
    <iframe style="width: 100%; height: 300px"     
    src="http://jsfiddle.net/SyEXx/4/embedded/result,html,js" 
    allowfullscreen="allowfullscreen"     
    frameborder="0">JSFiddle</iframe>
    <h2 id="what-it-does">What it does</h2>
    <p>You can go through each page by clicking the “Next” and “Prev” 
    links. Those buttons become gray when clicking on one would
    move to a page without data.</p>
    <p>The app also supports bookmarking of the current page and 
    forward/back button support.</p>
</article>

// JavaScript:
(function(){

    /**
     * $('.body').more({
     *   moreHTML: "<a href='javascript://' class='more'>...</a>",
     *   moreWidth: 50,
     *   lessHTML: " <a href='javascript://' class='less'>less</a>",
     *   lines: 2
     * })
     */
    $.fn.more = function(options){
        // setup defaults
            options = $.extend({
                lessHTML: " <a href='javascript://' class='less'>-</a>",
                moreHTML: " <a href='javascript://' class='more'>+</a>",
                moreWidth: 50,
                lines: 2
            },options||{});

        this.each(function(el){
            var $el = $(this);

            // save current HTML for later
            $el.data('originalHTML', $el.html())

            // the active range we will be moving around
            var range = $el.range(),
                // the end of the body's text for comparison
                end = range.clone().collapse(false).start("-1"),
                // the start of the body's text for comparison
                start = nextChar(range.collapse().end("+1"),end).clone(),
                // the current line's first character's coordinates
                prevRect = start.rect(),
                // how many lines we've come across
                lines = 0;

            // go until we reach the end of the body 
            while(range.compare("START_TO_START",end) != 0){
                range.end("+1").start("+1");

                var rect = range.rect();
                // if the charcter is on a new line
                if( rect && (rect.top -prevRect.top  > 4) ) {
                    lines++;
                    // quit on second line
                    if(lines == options.lines){
                        break;
                    }
                    prevStart = range.clone()
                    prevRect = rect;
                }
            }

            if(lines === options.lines){
                // backup to previous line
                range.end('-1').start('-1');
            }

            // get the last visible text element
            prevChar(range, start)

            var movedLeft = false,
                offset = $el.offset(),
                width = $el.width();

            // keep moving back until there is room for more
            while(range.compare("START_TO_START",start) != -1 ){
                if( range.rect(true).left <= (offset.left+width-options.moreWidth) ) {
                    break;
                }
                movedLeft = true;
                range.end("-1").start("-1")
            }
            // exit if we don't need to add more button
            if(!movedLeft && (lines < options.lines ) ) {
                return
            }

            var parent = range.start().container;
            // remove remaining text
            if( parent.nodeType === Node.TEXT_NODE ||
                 parent.nodeType === Node.CDATA_SECTION_NODE ) {
                 parent.nodeValue = parent.nodeValue.slice(0,range.start().offset+1)
            }
            var removeAfter =  parent;
            // remove everything after
            while(removeAfter !== this){
                var parentEl = removeAfter.parentNode,
                    childNodes = parentEl.childNodes,
                    index = $.inArray(removeAfter,childNodes );

                for(var i = parentEl.childNodes.length-1; i > index; i--){
                    parentEl.removeChild( childNodes[i] );
                }
                removeAfter = parentEl;
            }

            // add more after parent
            if( parent.nodeType === Node.TEXT_NODE ||
                 parent.nodeType === Node.CDATA_SECTION_NODE ) {
                 parent = parent.parentElement
            }
              $(parent).append(options.moreHTML);
              $el.data('shortenedHTML',$el.html())
                // show more / hide listeners
                .on("click","a.more",function(){
                    $el.html($el.data('originalHTML')+options.lessHTML)
                })
                .on("click","a.less",function(){
                    $el.html($el.data('shortenedHTML'))
                });
        })
    };

    // Moves the range until it hits something other than a space
    var nextChar = function(range, boundary){
        while(/[\s\n]/.test(range) && range.compare("END_TO_END",boundary) != 1){
            range.start("+1").end("+1")
        }
        return range;
    };
    // Moves the range until it hits something other than a space
    var prevChar = function(range, boundary){
        while(/[\s\n]/.test(range) && range.compare("START_TO_START",boundary) != -1){
            range.start("-1").end("-1")
        }
        return range;
    }

})();

$("article").more({lines: 5, moreWidth: 25})
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License