Movatterモバイル変換


[0]ホーム

URL:


Skip to content
Search Gists
Sign in Sign up

Instantly share code, notes, and snippets.

@jupiterjs
Forked frommoschel/JavaScriptMVC.md
CreatedMay 24, 2011 16:58
    Save jupiterjs/989117 to your computer and use it in GitHub Desktop.
    JavaScriptMVC Overview

    The following is a VERY rough draft of an article I am working on for Alex MacCaw's @maccman's Book. It is very rough, but even now a worthwhile read. Suggestions / comments are very welcome! Please help me :-)

    JavaScriptMVC (JMVC) is an open-source jQuery-based JavaScript framework. It is nearly a comprehensive (holistic) front-end development framework, packaging utilities for testing, dependency management, documentation, and a host of useful jQuery plugins.

    Yet every part of JavaScriptMVC can be used without every other part, making the library lightweight. Its Class, Model, View, and Controller combined are only 7k minified and compressed, yet even they can be used independently. JavaScriptMVC's independence lets you start small and scale to meet the challenges of the most complex applications on the web.

    This chapter coversonly JavaScriptMVC's$.Class, $.Model,$.View, and $.Controller. The following describes each component:

    • $.Class - JavaScript based class system
    • $.Model - traditional model layer
    • $.View - client side template system
    • $.Controller - jQuery widget factory

    JavaScriptMVC's naming conventions deviate slightly from the traditionalModel-View-Controller design pattern. $.Controller is used to create traditional view controls, like pagination buttons and list, as well as traditional controllers, which coordinate between the traditional views and models.

    Setup

    JavaScriptMVC can be used as a single download that includes the entire framework. But since this chapter covers only the MVC parts, go to thedownload 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>

    Class

    JMVC's Controller and Model inherit from its Class helper - $.Class. To create a class, call$.Class(NAME, [classProperties, ] instanceProperties]).

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

    In the example above, instances of Animal have abreathe() method. We can create a newAnimal instance and callbreathe() on it like:

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

    If you want to create a sub-class, simply call the the base class with the sub-class's name and properties:

    Animal("Dog",{  wag : function(){    console.log('wag');  }})var dog = new Dog;dog.wag();dog.breathe();

    Instantiation

    When a new class instance is created, it calls the class'sinit method with the arguments passed to the constructor function:

    $.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.' );

    Calling base methods

    Call base methods withthis._super. The following overwrites personto provide a more 'classy' greating:

    Person("ClassyPerson", {  speak : function(){    return "Salutations, "+this._super();  }});var fancypants = new ClassyPerson("Mr. Fancy");assertEquals( fancypants.speak() , 'Salutations, I am Mr. Fancy.')

    Proxies

    Class's callback method returns a function that has 'this' set appropriately (similar to$.proxy). The following creates a clicky class that counts how many times it was clicked:

    $.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 lets you define inheritable static properties and methods. The following allows us to retrieve a person instance from the server by callingPerson.findOne(ID, success(person) ). Success is called back with an instance of Person, which has thespeak method.

    $.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 provides namespacing and access to the name of the class and namespace object:

    $.Class("Jupiter.Person");Jupiter.Person.shortName; //-> 'Person'Jupiter.Person.fullName;  //-> 'Jupiter.Person'Jupiter.Person.namespace; //-> Jupitervar person = new Jupiter.Person();person.Class.shortName; //-> 'Person'

    Model example

    Putting it all together, we can make a basic ORM-style model layer. Just by inheriting from Model, we can request data from REST services and get it back wrapped in instances of the inheriting Model.

    $.Class("Model",{  findOne : function(id, success){    $.get('/'+this.fullName.toLowerCase()+'/'+id,       this.callback(function(attrs){         success( new this( attrs ) );      })    },'json')  }},{  init : function(attrs){    $.extend(this, attrs)  }})Model("Person",{  speak : function(){    return "I am "+this.name+".";  }});Person.findOne(5, function(person){  alert( person.speak() );});Model("Task")Task.findOne(7,function(task){  alert(task.name);})

    This is similar to how JavaScriptMVC's model layer works.

    Model

    JavaScriptMVC's model and its associated plugins provide lots of tools around organizing model data such as validations, associations, lists and more. But the core functionality is centered around service encapsulation, type conversion, and events.

    Attributes and Observables

    Of absolute importance to a model layer is the ability to get and set properties on the modeled data and listen for changes on a model instance. This is the Observer pattern and lies at the heart of the MVC approach - views listen to changes in the model.

    Fortunately, JavaScriptMVC makes it easy to make any data observable. A great example is pagination. It's very common that multiple pagination controls exist on the page. For example, one control might provide next and previous page buttons. Another control might detail the items the current page is viewing (ex "Showing items 1-20"). All pagination controls need the exact same data:

    • offset - the index of the first item to display
    • limit - the number of items to display
    • count - the total number of items

    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 themodel.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 withmodel.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 withmodel.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 namedsetNAME. They get called with the val passed tomodel.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 addingsetCount andsetOffset 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 staticdefaults 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 anext andprev method that will move to the next and previous page if possible. It also provides acanNext andcanPrev 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.

    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 withnew Model(attributes). Then callsave().

    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 thetask instance and thedata 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 thetask 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
    Task.findAll(params,   success( tasks ),   error( jqXHR) ) -> tasksDeferred
    Retrieves an array of tasks from the server. It takes three parameters:
    • params - data to pass to the server. Typically, it's an empty object ({}) or filters:{limit: 20, offset: 100}.
    • success - a function that gets called if the request is succesful. Success gets called with an array of task instances.
    • error - a function that gets called if there is an error with the request.
    findOne returns a deferred that resolves to an array of tasks.
    Update a task
    task.attr('name','take out recycling');task.save(   success( task, data ),   error( jqXHR) ) -> taskDeferred

    To update the server, first change the attributes of a model instance withattr. Then callsave().

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

    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 thetask instance and thedata 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.

    TheTask model has essentially become a contract to our services!

    Type Conversion

    Did you notice how the server responded with createdAt values as numbers like1303173531164. This number is actually April 18th, 2011. Instead of getting a number back fromtask.createdAt, it would be much more useful if it returns a JavaScript date created withnew Date(1303173531164). We could do this with asetCreatedAt 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 staticattributes object and converter methods on the staticconvert 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. UseMODEL.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()  })})

    $.View

    The content for$.View has been moved here

    $.Controller

    The content for$.Controller has been moved here.

    Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

    [8]ページ先頭

    ©2009-2025 Movatter.jp