Ascheriit

about me

A little something about me.

twitter

found on

[javascriptMVC] JMVC導覽 - $.Controller

- - posted in javascriptMVC | Comments

$.Controller – jQuery plugin factory

JavaScriptMVC’s controllers are many things. They are a jQuery plugin factory. They can be used as a traditional view, making pagination widgets and grid controls. Or, they can be used as a traditional controller, initializing and controllers and hooking them up to models. Mostly, controller’s are a really great way of organizing your application’s code.

Controllers provide a number of handy features such as:

  • jQuery plugin creation
  • automatic binding
  • default options
  • automatic determinism

But controller’s most important feature is not obvious to any but the most hard-core JS ninjas. The following code creates a tooltip like widget that displays itself until the document is clicked.

$.fn.tooltip = function(){
    var el = this[0];

    $(document).click(function(ev){
        if(ev.target !== el){
            $(el).remove()
        }
    })

    $(el).show();
    return this;
 })

To use it, you’d add the element to be displayed to the page, and then call tooltip on it like:

$("<div class='tooltip'>Some Info</div>")
        .appendTo(document.body)
        .tooltip()

But, this code has a problem. Can you spot it? Here’s a hint. What if your application is long lived and lots of these tooltip elements are created?

The problem is this code leaks memory! Every tooltip element, and any tooltip child elements, are kept in memory forever. This is because the click handler is not removed from the document and has a closure reference to the element.

This is a frighteningly easy mistake to make. jQuery removes all event handlers from elements that are removed from the page so developers often don’t have to worry about unbinding event handlers. But in this case, we bound to something outside the widget’s element, the document, and did not unbind the event handler.

But within a Model-View-Controller architecture, Controllers listen to the View and Views listen to the Model. You are constantly listening to events outside the widget’s element. For example, the nextPrev widget from the $.Model section listens to updates in the paginate model:

paginate.bind('updated.attr', function(){
    self.find('.prev')[this.canPrev() ? 'addClass' : 'removeClass']('enabled')
    self.find('.next')[this.canNext() ? 'addClass' : 'removeClass']('enabled');
})

But, it doesn’t unbind from paginate! Forgetting to remove event handlers is potentially a source of errors. However, both the tooltip and nextPrev would not error. Instead both will silently kill an application’s performance. Fortunately, $.Controller makes this easy and organized. We can write tooltip like:

$.Controller('Tooltip',{
    init: function(){
        this.element.show()
    },
    "{document} click": function(el, ev){
        if(ev.target !== this.element[0]){
            this.element.remove()
        }
    }
})

When the document is clicked and the element is removed from the DOM, $.Controller will automatically unbind the document click handler.

$.Controller can do the same thing for the nextPrev widget binding to the the paginate model:

$.Controller('Nextprev',{
    ".next click" : function(){
        var paginate = this.options.paginate;
        paginate.attr('offset', paginate.offset+paginate.limit);
    },
    ".prev click" : function(){
        var paginate = this.options.paginate;
        paginate.attr('offset', paginate.offset-paginate.limit );
    },
    "{paginate} updated.attr" : function(ev, paginate){
        this.find('.prev')[paginate.canPrev() ? 'addClass' : 'removeClass']('enabled')
        this.find('.next')[paginate.canNext() ? 'addClass' : 'removeClass']('enabled');
    }
})

// create a nextprev control
$('#pagebuttons').nextprev({ paginate: new Paginate() })

If the element #pagebuttons is removed from the page, the Nextprev controller instance will automatically unbind from the paginate model.

Now that your appetite for error free code is properly whetted, the following details how $.Controller works.

Overview

$.Controller inherits from $.Class. To create a Controller class, call $.Controller( NAME, classProperties, instanceProperties ) with the name of your controller, static methods, and instance methods. The following is the start of a reusable list widget:

$.Controller("List", {
    defaults : {}
},{
    init : function(){  },
        "li click" : function(){  }
})

When a controller class is created, it creates a jQuery helper method of a similar name. The helper method is primarily use to create new instances of controller on elements in the page. The helper method name is the controller’s name underscored, with any periods replaced with underscores. For example, the helper for $.Controller(‘App.FooBar’) is $(el).app_foo_bar().

Controller Instantiation

To create a controller instance, you can call new Controller(element, options) with a HTMLElment or jQuery-wrapped element and an optional options object to configure the controller. For example:

new List($('ul#tasks'), {model : Task});

You can also use the jQuery helper method to create a List controller instance on the #tasks element like:

$('ul#tasks').list({model : Task})

When a controller is created, it calls the controller’s prototype init method with:

  • this.element set to the jQuery-wrapped HTML element
  • this.options set to the options passed to the controller merged with the class’s defaults object.

The following updates the List controller to request tasks from the model and render them with an optional template passed to the list:

$.Controller("List", {
    defaults : {
        template: "items.ejs"
    }
},{
    init : function(){
        this.element.html( this.options.template, this.options.model.findAll() ); 
    },
    "li click" : function(){  }
})

We can now configure Lists to render tasks with a template we provide. How flexible!

$('#tasks').list({model: Task, template: "tasks.ejs"});
$('#users').list({model: User, template: "users.ejs"})

If we don’t provide a template, List will default to using items.ejs.

Event Binding

As mentioned in $.Controller’s introduction, it’s most powerful feature is it’s ability to bind and unbind event handlers.

When a controller is created, it looks for action methods. Action methods are methods that look like event handlers. For example, “li click”. These actions are bound using jQuery.bind or jQuery.delegate. When the controller is destroyed, by removing the controller’s element from the page or calling destroy on the controller, these events are unbound, preventing memory leaks.

The following are examples of actions with descriptions of what the listen for:

  • “li click” – clicks on or within li elements within the controller element.
  • “mousemove” – mousemoves within the controller element.
  • “{window} click” – clicks on or within the window.

Action functions get called back with the jQuery-wrapped element or object that the event happened on and the event. For example:

"li click": function( el, ev ) {
    assertEqual(el[0].nodeName, "li" )
        assertEqual(ev.type, "click")
}

Templated Actions

$.Controller supports templated actions. Templated actions can be used to bind to other objects, customize the event type, or customize the selector.

Controller replaces the parts of your actions that look like {OPTION} with a value in the controller’s options or the window.

The following is a skeleton of a menu that lets you customize the menu to show sub-menus on different events:

$.Controller("Menu",{
    "li {openEvent}" : function(){
    // show subchildren
    }
});

//create a menu that shows children on click
$("#clickMenu").menu({openEvent: 'click'});

//create a menu that shows children on mouseenter
$("#hoverMenu").menu({openEvent: 'mouseenter'});

We could enhance the menu further to allow customization of the menu element tag:

$.Controller("Menu",{
    defaults : {menuTag : "li"}
},{
    "{menuTag} {openEvent}" : function(){
    // show subchildren
    }
});

$("#divMenu").menu({menuTag : "div"})

Templated actions let you bind to elements or objects outside the controller’s element. For example, the Task model from the $.Model section produces a “created” event when a new Task is created. We can make our list widget listen to tasks being created and automatically add these tasks to the list like:

$.Controller("List", {
    defaults : {
        template: "items.ejs"
    }
},{
    init : function(){
        this.element.html( this.options.template, this.options.model.findAll() ); 
    },
    "{Task} created" : function(Task, ev, newTask){
        this.element.append(this.options.template, [newTask])
    }
})

The “{Task} create” gets called with the Task model, the created event, and the newly created Task. The function uses the template to render a list of tasks (in this case there is only one) and add the resulting html to the element.

But, it’s much better to make List work with any model. Instead of hard coding tasks, we’ll make controller take a model as an option:

$.Controller("List", {
    defaults : {
        template: "items.ejs",
        model: null
    }
},{
    init : function(){
        this.element.html( this.options.template, this.options.model.findAll() ); 
    },
    "{model} created" : function(Model, ev, newItem){
        this.element.append(this.options.template, [newItem])
    }
})
// create a list of tasks
$('#tasks').list({model: Task, template: "tasks.ejs"});

Putting it all together – an abstract CRUD list.

Now we will enhance the list to not only add items when they are created, but update them and remove them when they are destroyed. To do this, we start by listening to updated and destroyed:

"{model} updated" : function(Model, ev, updatedItem){
    // find and update the LI for updatedItem
},
"{model} destroyed" : function(Model, ev, destroyedItem){
    // find and remove the LI for destroyedItem
}

You’ll notice here we have a problem. Somehow, we need to find the element that represents particular model instance. To do this, we need to label the element as belonging to the model instance. Fortunately, $.Model and $.View make labeling an element with an instance and finding that element very easy.

To label the element with a model instance within an EJS view, you simply write the model instance to the element. The following might be tasks.ejs

<% for(var i =0 ; i < this.length; i++){ %>
<% var task = this[i]; %>
    <li <%= task %> > <%= task.name %> </li>
<% } %>

tasks.ejs iterates through a list of tasks. For each task, it creates an li element with the task’s name. But, it also adds the task to the element’s jQuery data with: <li <%= task %> >.

To later get that element given a model instance, you can call modelInstance.elements([CONTEXT]). This returns the jQuery-wrapped elements the represent the model instance.

Putting it together, list becomes:

$.Controller("List", {
    defaults : {
        template: "items.ejs",
        model: null
    }
},{
    init : function(){
        this.element.html( this.options.template, this.options.model.findAll() ); 
    },
    "{model} created" : function(Model, ev, newItem){
        this.element.append(this.options.template, [newItem])
    },
    "{model} updated" : function(Model, ev, updatedItem){
        updatedItem.elements(this.element)
        .replaceWith(this.options.template, [updatedItem])
    },
    "{model} destroyed" : function(Model, ev, destroyedItem){
        destroyedItem.elements(this.element)
         .remove()
    }
});

// create a list of tasks
$('#tasks').list({model: Task, template: "tasks.ejs"});

It’s almost frighteningly easy to create abstract, reusable, memory safe widgets with JavaScriptMVC.

Determinism

Controllers provide automatic determinism for your widgets. This means you can look at a controller and know where in the DOM they operate, and vice versa.

First, when a controller is created, it adds its underscored name as a class name on the parent element.

<div id='historytab' class='history_tabs'></div>

You can look through the DOM, see a class name, and go find the corresponding controller.

Second, the controller saves a reference to the parent element in this.element. On the other side, the element saves a reference to the controller instance in jQuery.data.

$("#foo").data('controllers')

A helper method called controller (or controllers) using the jQuery.data reference to quickly look up controller instance on any element.

$("#foo").controller() // returns first controller found
$("#foo").controllers() // returns an array of all controllers on this element

Finally, actions are self labeling, meaning if you look at a method called “.foo click”, there is no ambiguity about what is going on in that method.

Responding to Actions

If you name an event with the pattern “selector action”, controllers will set these methods up as event handlers with event delegation. Even better, these event handlers will automatically be removed when the controller is destroyed.

".todo mouseover" : function( el, ev ) {}

The el passed as the first argument is the target of the event, and ev is the jQuery event. Each handler is called with “this” set to the controller instance, which you can use to save state.

Removing Controllers

Part of the magic of controllers is their automatic removal and cleanup.  Controllers bind to the special destroy event, which is triggered whenever an element is removed via jQuery.  So if you remove an element that contains a controller with el.remove() or a similar method, the controller will remove itself also.  All events bound in the controller will automatically clean themselves up.

Defaults

Controllers can be given a set of default options.  Users creating a controller pass in a set of options, which will overwrite the defaults if provided.

In this example, a default message is provided, but can is overridden in the second example by "hi".

$.Controller("Message", {
    defaults : {
        message : "Hello World"
    }
},{
    init : function(){
        this.element.text(this.options.message);
    }
})

$("#el1").message(); //writes "Hello World"
$("#el12").message({message: "hi"}); //writes "hi"

Parameterized Actions

Controllers provide the ability to set either the selector or action of any event via a customizable option. This makes controllers potentially very flexible. You can create more general purpose event handlers and instantiate them for different situations.

The following listens to li click for the controller on #clickMe, and “div mouseenter” for the controller on #touchMe.

$.Controller("Hello", {
    defaults: {item: “li”, helloEvent: “click”}
}, {
    “{item} {helloEvent}" : function(el, ev){
        alert('hello')  el // li, div
    }
})

$("#clickMe").hello({item: “li”, helloEvent : "click"});
$("#touchMe").hello({item: “div”, helloEvent : "mouseenter"});

Pub / Sub

JavaScriptMVC applications often use OpenAjax event publish and subscribe as a good way to globally notify other application components of some interesting event. The jquery/controller/subscribe method lets you subscribe to (or publish) OpenAjax.hub messages:

$.Controller("Listener",{
    "something.updated subscribe" : function(called, data){}
})

// called elsewhere
this.publish("some.event", data);

Special Events

Controllers provide support for many types of special events. Any event that is added to jQuery.event.special and supports bubbling can be listened for in the same way as a DOM event like click.

$.Controller("MyHistory",{
    "history.pagename subscribe" : function(called, data){
        //called when hash = #pagename
    }
})

Drag, drop, hover, and history and some of the more widely used controller events. These events will be discussed later.

controller is created, it adds its underscored name as a class name on the parent element.

<div id='historytab' class='history_tabs'></div>

You can look through the DOM, see a class name, and go find the corresponding controller.

Second, the controller saves a reference to the parent element in this.element. On the other side, the element saves a reference to the controller instance in jQuery.data.

$("#foo").data('controllers')

A helper method called controller (or controllers) using the jQuery.data reference to quickly look up controller instance on any element.

$("#foo").controller() // returns first controller found
$("#foo").controllers() // returns an array of all controllers on this element

Finally, actions are self labeling, meaning if you look at a method called “.foo click”, there is no ambiguity about what is going on in that method.

[jQuery]UI.layout*SlickGrid - 使SlickGrid在layout改變大小時能自動filter高度

- - posted in jquery | Comments

目前只有一種作法,利用publish/subscribe告訴SlickGrid要去追parent的高度

1
2
3
4
5
6
7
8
9
10
11
12
13
$.subscribe("units/set_grid_height", function (new_height) {
  grid_opts.height = new_height;
  resize();
});

// ...

// grid = the jQuery element that represents the SlickGrid
// slick = the instantiated slickgrid
function resize() {
  grid.css('height', grid_opts.height);
  slick.resizeCanvas();
}
1
2
3
4
5
6
7
8
9
layout = $('body').layout({
  center: {
      onresize: function (name, el, state, opts, layout_name) {
          $.publish("units/set_grid_height", [state.innerHeight]);
      }
  }
});

$.publish("units/set_grid_height", [layout.state.center.innerHeight]);

[Jquery] jQuery Layout X jQuery UI Tabs的應用

- - posted in jquery | Comments

jQuery layout是一套幫助RD排版的工具, 現在遇到一個需求是在某個layout出來的版面中塞jQuery UI TABS, 會有scrollbar顯示的問題。

在jQuery layout官方範例有特別作一個tab結合的範例,看看它怎麼處理這個問題: 原始網址

jsfiddle

source

jquery.layout-latest.js
1
2
3
4
5
6
322             ,   contentSelector:        ".ui-layout-content" // INNER div/element to auto-size so only it scrolls, not the entire pane!                      
323         ,   contentIgnoreSelector:  ".ui-layout-ignore" // element(s) to 'ignore' when measuring 'content'
324         ,   findNestedContent:      false       // true = $P.find(contentSelector), false = $P.children(contentSelector)
325         //  GENERIC ROOT-CLASSES - for auto-generated classNames
326         ,   paneClass:              "ui-layout-pane"    // border-Pane - default: 'ui-layout-pane'
327         ,   resizerClass:           "ui-layout-resizer" // Resizer Bar      - default: 'ui-layout-resizer'

也就是,利用contentSelectorfindNestedContent告訴layout plugin我要把contentSelector的內容視作layout-pane的本體, 在改變大小時候應該去改變它而不是整個layout-pane。

[jQuery] $.fixture介紹:模擬ajax Request Callback

- - posted in jquery | Comments

$.fixture可以攔截ajax請求並且模擬可能的回應,回應可以從一個檔案(json格式)或某個callback函式。 這對你在開發js而不想依賴後端回應的時候很有用。

Fixture的型態

Fixtrue有兩種常見的使用方式,第一個是使用靜態檔案回應ajax請求。 下面這個敘述會攔截對tasks.json的請求,將它導向fixtures/tasks.json這個檔案的內容:

$.fixture("/tasks.json","fixtures/tasks.json");

另一個選擇是使用函式動態產生回應資料。下面的敘述攔截對tasks/ID.json的更新請求,並且回應被更新的資料(事實上沒有更新到伺服端):

$.fixture("PUT /tasks/{id}.json", function(original, settings, headers){
    return { updatedAt : new Date().getTime() }
})

我們將fixture分為兩種型態: * 靜態fixture – 用檔案內容回應。 * 動態fixture – 用函式產生的資料回應。

這兩種不同型態各有一些方法去使用它們。

靜態Fixture

靜態fixture使用一個替代URL來回應ajax請求,如下:

// 使用相對位置
$.fixture("tasks/1", "fixtures/task1.json");

// 使用絕對位置
$.fixture("tasks/1", "//fixtures/task1.json");

動態Fixtures

動態fixture實際上就是一個函式。它可以拿到你對ajax請求給予的參數並且回給你假的回應。

例如,下面會回應一個成功的請求,資料則是JSON格式。

$.fixture("/foobar.json", function(orig, settings, headers){
    return [200, "success", {json: {foo: "bar" } }, {} ]
})

fixture函數的signature如下

function( originalOptions, options, headers ) {
    return [ status, statusText, responses, responseHeaders ]
}

呼叫參數的意義如下:

  • originalOptions – are the options provided to the ajax method, unmodified, and thus, without defaults from ajaxSettings
  • options – are the request options
  • headers – a map of key/value request headers

and the fixture function returns an array as arguments for ajaxTransport’s completeCallback with:

  • status – is the HTTP status code of the response.
  • statusText – the status text of the response
  • responses – a map of dataType/value that contains the responses for each data format supported
  • headers – response headers

However, $.fixture handles the common case where you want a successful response with JSON data. The previous can be written like:

$.fixture("/foobar.json", function(orig, settings, headers){
    return {foo: "bar" };
})

If you want to return an array of data, wrap your array in another array:

$.fixture("/tasks.json", function(orig, settings, headers){
    return [ [ "first","second","third"] ];
})

$.fixture works closesly with jQuery’s ajaxTransport system. Understanding it is the key to creating advanced fixtures.

Templated Urls

Often, you want a dynamic fixture to handle urls for multiple resources (for example a REST url scheme). $.fixture’s templated urls allow you to match urls with a wildcard.

The following example simulates services that get and update 100 todos.

// create todos
var todos = {};
for(var i = 0; i < 100; i++) {
    todos[i] = {
        id: i,
        name: "Todo "+i
    }
}
$.fixture("GET /todos/{id}", function(orig){
    // return the JSON data
    // notice that id is pulled from the url and added to data
    return todos[orig.data.id]
})
$.fixture("PUT /todos/{id}", function(orig){
    // update the todo's data
    $.extend( todos[orig.data.id], orig.data );

    // return data
    return {};
})

Notice that data found in templated urls (ex: {id}) is added to the original data object.

Simulating Errors

The following simulates an unauthorized request to /foo.

$.fixture("/foo", function(){
    return [401,"{type: 'unauthorized'}"]
});

This could be received by the following Ajax request:

$.ajax({
    url: '/foo',
    error : function(jqXhr, status, statusText){
        // status === 'error'
        // statusText === "{type: 'unauthorized'}"
    }
})

Turning off Fixtures

You can remove a fixture by passing null for the fixture option:

// add a fixture
$.fixture("GET todos.json","//fixtures/todos.json");

// remove the fixture
$.fixture("GET todos.json", null)

You can also set jQuery.fixture.on to false:

$.fixture.on = false;

Make

$.fixture.make makes a CRUD service layer that handles sorting, grouping, filtering and more.

Testing Performance

Dynamic fixtures are awesome for performance testing. Want to see what 10000 files does to your app’s performance? Make a fixture that returns 10000 items.

What to see what the app feels like when a request takes 5 seconds to return? Set jQuery.fixture.delay to 5000.’

Working Notes

- - | Comments

Form.Encoder spec

  1. input轉成ejs(/views/Naxx.Controllers.Form.Input.ejs)

[javascriptMVC] <翻譯+心得> Jmvc導覽(2) - $.Model

- - posted in javascriptmvc | Comments

Model

javascriptMVC的model和相關連plugin提供許多組織model資料的工具如驗證(validation),關聯(association),列表(list)還有其他更多。但核心功能專注在service encapsulation(服務包裝),型態轉換(type converstion),還有事件關聯(event)上。

可以這麼說:沒有跟伺服器溝通的必要就不需用到model。
相反地請將所有跟伺服器資料交換的動作交給model用一致的介面來處理。

屬性和觀察可能性 – Attributes and Observables

model層一個絕對的重要性在於存取物件資料的屬性(property), 和監聽model個體的改變。這就是觀察者模式而且正是MVC的重要環節 – view監聽model的改變。

幸運的是,JavascriptMVC使得任何資料都可以容易的被觀察(obsevable)。一個好的例子是翻頁機制。一個頁面通常有許多的翻譯控制項,例如,一個控制項提供前一頁另一個提供後一頁。另一個控制項可能提供顯示這一頁的部份資料(例如顯示1-20項)。所有的頁面控制項會需要同樣的資料:

  • offset – 要顯示的第一個項目的位置
  • limit – 總共要顯示多少項目
  • count – 全部項目的數量

We can model this data with JavaScriptMVC’s $.Model like:

var paginate = new $.Model({
    offset: 0,
    limit: 20,
    count: 200
});

The paginate variable is now observable. We can pass it to pagination controls that can read from, write to, and listen for property changes. You can read properties like normal or using the model.attr(NAME) method:

assertEqual( paginate.offset, 0 );
assertEqual( paginate.attr('limit') , 20 );

If we clicked the next button, we need to increment the offset. Change property values with model.attr(NAME, VALUE). The following moves the offset to the next page:

paginate.attr('offset',20);  

When paginate’s state is changed by one control, the other controls need to be notified. You can bind to a specific attribute change with model.bind(ATTR, success( ev, newVal ) ) and update the control:

paginate.bind('offset', function(ev, newVal){
    $('#details').text( 'Showing items ' + (newVal+1 )+ '-' + this.count )
})

You can also listen to any attribute change by binding to the ‘updated.attr’ event:

paginate.bind('updated.attr', function(ev, newVal){
    $('#details').text( 'Showing items ' + (newVal+1 )+ '-' + this.count )
})

The following is a next-previous jQuery plugin that accepts paginate data:

$.fn.nextPrev = function(paginate){
    this.delegate('.next','click', function(){
        var nextOffset = paginate.offset+paginate.limit;
        if( nextOffset < paginate.count){
            paginate.attr('offset', nextOffset );
        }
    })
    this.delegate('.prev','click', function(){
        var nextOffset = paginate.offset-paginate.limit;
        if( 0 < paginate.offset ){
            paginate.attr('offset', Math.max(0, nextOffset) );
        }
    });
    var self = this;
    paginate.bind('updated.attr', function(){
        var next = self.find('.next'),
        prev = self.find('.prev');
        if( this.offset == 0 ){
            prev.removeClass('enabled');
        } else { 
            prev.removeClass('disabled');
        }
        if( this.offset > this.count - this.limit ){
            next.removeClass('enabled');
        } else { 
            next.removeClass('disabled');
        }
    })
};

There are a few problems with this plugin. First, if the control is removed from the page, it is not unbinding itself from paginate. We’ll address this when we discuss controllers.

Second, the logic protecting a negative offset or offset above the total count is done in the plugin. This logic should be done in the model. To fix this problem, we’ll need to add additional constraints to limit what values limit, offset, and count can be. We’ll need to create a pagination class.

Extending Model

JavaScriptMVC’s model inherits from $.Class. Thus, you create a model class by inheriting from $.Model(NAME, [STATIC,] PROTOTYPE):

$.Model('Paginate',{
    staticProperty: 'foo'
},{
    prototypeProperty: 'bar'
})

There are a few ways to make the Paginate model more useful. First, by adding setter methods, we can limit what values count and offset can be set to.

Setters

Settter methods are model prototype methods that are named setNAME. They get called with the val passed to model.attr(NAME, val) and a success and error callback. Typically, the method should return the value that should be set on the model instance or call error with an error message. Success is used for asynchronous setters.

The following paginate model uses setters to prevent negative counts the offset from exceeding the count by adding setCount and setOffset instance methods.

$.Model('Paginate',{
    setCount : function(newCount, success, error){
        return newCount < 0 ? 0 : newCount;
    },
    setOffset : function(newOffset, success, error){
        return newOffset < 0 ? 0 : Math.min(newOffset, !isNaN(this.count - 1) ? this.count : Infinity )
    }
})

Now the nextPrev plugin can set offset with reckless abandon:

this.delegate('.next','click', function(){
    paginate.attr('offset', paginate.offset+paginate.limit);
})
this.delegate('.prev','click', function(){
    paginate.attr('offset', paginate.offset-paginate.limit );
});

Defaults

We can add default values to Paginate instances by setting the static defaults property. When a new paginate instance is created, if no value is provided, it initializes with the default value.

$.Model('Paginate',{
    defaults : {
        count: Infinity,
        offset: 0,
        limit: 100
    }
},{
    setCount : function(newCount, success, error){ ... },
    setOffset : function(newOffset, success, error){ ... }
})

var paginate = new Paginate({count: 500});
assertEqual(paginate.limit, 100);
assertEqual(paginate.count, 500);

This is getting sexy, but the Paginate model can make it even easier to move to the next and previous page and know if it’s possible by adding helper methods.

Helper methods

Helper methods are prototype methods that help set or get useful data on model instances. The following, completed, Paginate model includes a next and prev method that will move to the next and previous page if possible. It also provides a canNext and canPrev method that returns if the instance can move to the next page or not.

$.Model('Paginate',{
    defaults : {
        count: Infinity,
        offset: 0,
        limit: 100
    }
},{
    setCount : function( newCount ){
        return Math.max(0, newCount  );
    },
    setOffset : function( newOffset ){
        return Math.max( 0 , Math.min(newOffset, this.count ) )
    },
    next : function(){
        this.attr('offset', this.offset+this.limit);
    },
    prev : function(){
        this.attr('offset', this.offset - this.limit )
    },
    canNext : function(){
        return this.offset > this.count - this.limit
    },
    canPrev : function(){
        return this.offset > 0
    }
})

Thus, our jQuery widget becomes much more refined:

$.fn.nextPrev = function(paginate){
    this.delegate('.next','click', function(){
        paginate.attr('offset', paginate.offset+paginate.limit);
    })
    this.delegate('.prev','click', function(){
        paginate.attr('offset', paginate.offset-paginate.limit );
    });
    var self = this;
    paginate.bind('updated.attr', function(){
        self.find('.prev')[paginate.canPrev() ? 'addClass' : 'removeClass']('enabled')
        self.find('.next')[paginate.canNext() ? 'addClass' : 'removeClass']('enabled');
    })
};

Service Encapsulation

We’ve just seen how $.Model is useful for modeling client side state. However, for most applications, the critical data is on the server, not on the client. The client needs to create, retrieve, update and delete (CRUD) data on the server. Maintaining the duality of data on the client and server is tricky business. $.Model is used to simplify this problem.

$.Model is extremely flexible. It can be made to work with all sorts of services types and data types. This book covers only how $.Model works with the most common and popular type of service and data type: Representational State Transfer (REST) and JSON.

A REST service uses urls and the HTTP verbs POST, GET, PUT, DELETE to create, retrieve, update, and delete data respectively. For example, a tasks service that allowed you to create, retrieve, update and delete tasks might look like:

ACTIONVERBURLBODYRESPONSE
Create a taskPOST/tasksname=do the dishes
{
    "id"       : 2,
        "name"     : "do the dishes",
        "acl"      : "rw" ,
        "createdAt": 1303173531164 // April 18 2011
}
Get a taskGET/task/2
{
    "id"       : 2,
        "name"     : "do the dishes",
        "acl"      : "rw" ,
        "createdAt": 1303173531164 // April 18 2011
}
Get tasksGET/tasks
[{
    "id"       : 1,
        "name"     : "take out trash",
        "acl"      : "r",
        "createdAt": 1303000731164 // April 16 2011
},
{
    "id"       : 2,
    "name"     : "do the dishes",
    "acl"      : "rw" ,
    "createdAt": 1303173531164 // April 18 2011
}]
Update a taskPUT/task/2name=take out recycling
{
    "id"       : 2,
    "name"     : "take out recycling",
    "acl"      : "rw" ,
    "createdAt": 1303173531164 // April 18 2011
}
Delete a taskDELETE/task/2
{}

TODO: We can label the urls

The following connects to task services, letting us create, retrieve, update and delete tasks from the server:

$.Model("Task",{
    create  : "POST /tasks.json",
    findOne : "GET /tasks/{id}.json",
    findAll : "GET /tasks.json",
    update  : "PUT /tasks/{id}.json",
    destroy : "DELETE /tasks/{id}.json"
},{ });

The following table details how to use the task model to CRUD tasks.

rst change the attributes of a model instance with attr. Then call save().

Save takes the same arguments and returns the same deferred as the create task case.

ACTIONCODEDESCRIPTION
Create a task
new Task({ name: 'do the dishes'}).save( 
            success( task, data ), 
            error( jqXHR) 
            ) -> taskDeferred

To create an instance of a model on the server, first create an instance with new Model(attributes). Then call save().

Save checks if the task has an id. In this case it does not so save makes a create request with the task’s attributes. Save takes two parameters:

  • success - a function that gets called if the save is successful. Success gets called with the task instance and the data returned by the server.
  • error - a function that gets called if there is an error with the request. It gets called with jQuery’s wrapped XHR object.
Save returns a deferred that resolves to the created task.
Get a task
Task.findOne(params, 
        success( task ), 
        error( jqXHR) 
        ) -> taskDeferred
Retrieves a single task from the server. It takes three parameters:
  • params - data to pass to the server. Typically an id like: {id: 2}.
  • success - a function that gets called if the request is succesful. Success gets called with the task instance.
  • error - a function that gets called if there is an error with the request.
findOne returns a deferred that resolves to the task.
Get tasks
Destroy a task
task.destroy( 
            success( task, data ), 
            error( jqXHR) 
            ) -> taskDeferred

Destroys a task on the server. Destroy takes two parameters:

  • success - a function that gets called if the save is successful. Success gets called with the task instance and the data returned by the server.
  • error - a function that gets called if there is an error with the request.
Destroy returns a deferred that resolves to the destroyed task.
The <code>Task</code> model has essentially become a contract to our services!

Type Conversion

Did you notice how the server responded with createdAt values as numbers like 1303173531164. This number is actually April 18th, 2011. Instead of getting a number back from task.createdAt, it would be much more useful if it returns a JavaScript date created with new Date(1303173531164). We could do this with a setCreatedAt setter. But, if we have lots of date types, this will quickly get repetitive.

To make this easy, $.Model lets you define the type of an attribute and a converter function for those types. Set the type of attributes on the static attributes object and converter methods on the static convert object.

$.Model('Task',{
    attributes : {
        createdAt : 'date'
    },
    convert : {
        date : function(date){
        return typeof date == 'number' ? new Date(date) : date;
        }
    }
},{});

Task now converts createdAt to a Date type. To list the year of each task, write:

Task.findAll({}, function(tasks){
    $.each(tasks, function(){
        console.log( "Year = "+this.createdAt.fullYear() )
    })
});

CRUD Events

Model publishes events when an instance has been created, updated, or destroyed. You can listen to these events globally on the Model or on an individual model instance. Use MODEL.bind(EVENT, callback( ev, instance ) ) to listen for created, updated, or destroyed events.

Lets say we wanted to know when a task is created and add it to the page. After it’s been added to the page, we’ll listen for updates on that task to make sure we are showing its name correctly. We can do that like:

Task.bind('created', function(ev, task){
    var el = $('<li>').html(todo.name);
    el.appendTo($('#todos'));

    task.bind('updated', function(){
        el.html(this.name)
    }).bind('destroyed', function(){
        el.remove()
    })
})

[JavascriptMVC] <翻譯+心得> JMVC導覽(1) Introduction + $.Class

- - posted in javascriptmvc | Comments

前言

本文以https://gist.github.com/989117 為基礎翻譯後再加上個人使用上的心得。

介紹

JavascriptMVC (JMVC)是一個基於jQuery的開放原始碼js框架。 它擁有完整的前端框架解決方案,包含打包工具,測試,代碼依賴管理,文件化,以及附帶許多有用的jQuery外掛。

JavascriptMVC的每一個項目都可以被獨立使用而不需要依賴其他項目。只看Class, Model, View, Controller部分的大小壓縮過只有7k左右,而且這之中的任何一個都還是能被獨立使用。 JavasriptMVC的輕巧及強大的獨立性使得它能應付複雜的大型web專案。

這個導覽 只會 提到JavaScriptMVC的$.Class, $.Model, $.View, 和$.Controller. 下列是它們各自的意義:

  • $.Class – 基於js的類別化系統
  • $.Model – 傳統的model
  • $.View – 客戶端模版系統
  • $.Controller – jQuery元件工廠

JavaScriptMVC的命名取向不像傳統的 Model-View-Controller 設計模式. $.Controller被用來創造一個傳統的view,例如翻頁按鈕會列表,同時也是傳統的controller--作為傳統的model與傳統的view之間協調的角色。

安裝

方法一:官網下載

JavaScriptMVC can be used as a single download that includes the entire framework. But since this chapter covers only the MVC parts, go to the download builder, check Controller, Model, and View’s EJS templates and click download.

The download will come with minified and unminified versions of jQuery and the plugins you selected. Load these with script tags in your page:

<script type='text/javascript' src='jquery-1.6.1.js'></script>  
<script type='text/javascript' src='jquerymx-1.0.custom.js'></script> 

方法二:github

根據JavascriptMVC on github的README:

  1. 在自己帳號內開一個新git專案取名jmvc
  2. Fork 以下這些project到自己的github帳號內

    http://github.com/jupiterjs/steal
    http://github.com/jupiterjs/jquerymx http://github.com/jupiterjs/funcunit http://github.com/jupiterjs/documentjs

    可選的額外功能

    http://github.com/jupiterjs/mxui

  3. 在自己的電腦上取回剛剛fork的javascriptmvc

    git submodule add git@github.com:YOU/steal.git steal git submodule add git@github.com:YOU/jquerymx.git jquery git submodule add git@github.com:YOU/funcunit.git funcunit git submodule add git@github.com:YOU/documentjs.git documentjs

  4. 因為作者將主要功能都模組化的緣故,有四個子模組需要手動取回

  5. 可以開始玩了!

Class

JMVC的Controller和Model繼承自Class的helper – $.Class. 要創造一個類別,只要呼叫 $.Class(NAME, [classProperties, ] instanceProperties]).

$.Class("Animal",{
    breathe : function(){
        console.log('breathe'); 
    }
});

在上例中,Animal的instance都有breathe()這個method. 我們可以建立一個新的Animal instance而且呼叫它的breathe()像這樣:

var man = new Animal();
man.breathe();

如果你想要創造一個子類別, 只要把新類別名稱以及屬性用基礎類別呼叫即可:

Animal("Dog",{
    wag : function(){
        console.log('wag');
    }
})

var dog = new Dog;
dog.wag();
dog.breathe();

Instantiation – 物件化

當一個新的物件物體被創造時, 它首先會執行這個類別的init函式, 將建構子帶的參數傳給init:

$.Class('Person',{
    init : function(name){
        this.name = name;
    },
    speak : function(){
        return "I am "+this.name+".";
    }
});

var payal = new Person("Payal");
assertEqual( payal.speak() ,  'I am Payal.' );

呼叫基礎類別函式(base method)

this._super使用基礎類別的函式. 下面重寫(overwrite)了person類別的speak函式。

Person("ClassyPerson", {
    speak : function(){
        return "Salutations, "+this._super();
    }
});

var fancypants = new ClassyPerson("Mr. Fancy");
assertEquals( fancypants.speak() , 'Salutations, I am Mr. Fancy.')

代理 – proxies

Class的回呼函式會把this適當地設置(類似$.proxy)。下面建立一個clicky類別然後計算它被click多少次。

$.Class("Clicky",{
    init : function(){
        this.clickCount = 0;
    },
    clicked: function(){
        this.clickCount++;
    },
    listen: function(el){
        el.click( this.callback('clicked') );
    }
})

var clicky = new Clicky();
clicky.listen( $('#foo') );
clicky.listen( $('#bar') ) ;

靜態繼承 – static inheritance

Class可以定義靜態屬性跟類別。如下允許我們用Person.findOne(ID, success(person) )向伺服器端取得一個person個體。成功的話會回傳一個Person個體。

$.Class("Person",{
    findOne : function(id, success){
        $.get('/person/'+id, function(attrs){
            success( new Person( attrs ) );
        },'json')
    }
},{
    init : function(attrs){
        $.extend(this, attrs)
    },
    speak : function(){
        return "I am "+this.name+".";
    }
})

Person.findOne(5, function(person){
    assertEqual( person.speak(), "I am Payal." );
})

內觀 – Introspection

Class提供命名空間以及存取類別名稱以及命名空間的物件:

$.Class("Jupiter.Person");

Jupiter.Person.shortName; //-> 'Person'
Jupiter.Person.fullName;  //-> 'Jupiter.Person'
Jupiter.Person.namespace; //-> Jupiter

var person = new Jupiter.Person();

person.Class.shortName; //-> 'Person'

[Octopress] Tags測試

- - posted in octopress | Comments

Octopress支援的內嵌程式碼功能: – code block – jsfiddle

0 gist

title: “octopress-tags” date: 2012-02-01 11:34 comments: true

categories:

Octopress支援的內嵌程式碼功能: * code block * jsfiddle * gist

origin/master