Pages

Wednesday, December 14, 2011

ExtJS Models Need Some Work

  1. They really need getters. Convert is not a getter. It is a setter that returns a value. Convert is also called before the data is loaded, and before associations are created, meaning you have to do lots of checks for those. Sencha, please add getters, that can still auto-load on forms and grids
  2. They really need validation scenarios. These are easy to do. Sencha, just copy Yii's validation scenarios.
  3. Please add some events to Models, such as datachanged. I don't want to have to add a model to store just to get notified when it changes. 

RadioGroups are F-in Useless

This post was valid during ExtJS 4.0 beta, but RadioGroups are fine now. Thanks

Sunday, December 4, 2011

Wednesday, November 30, 2011

Using remote filter, sort, and groups in Stores, with PHP code

Was working with stores today, and wrote some Yii PHP code to support their features. Here is what I wanted to do:

1. support loading a single model via Model.load()
2. support combobox autocomplete
3. support Store remote paging, and filtering, sorting, and grouping by multiple properties
4. all using the same URL
5. avoid SQL injection

You need to be careful with custom filtering, sorting, and grouping on your server, as it is easy to allow SQL injection.

Here is what the requests and responses look like. Code is at the bottom.

1. Model.load() requests the model by id...

Request:

index?id=3

JSON Response:

{
  "success":true,
  "data":{model data...}
}

or, if it can't find the model:
{
  "success":false,
  "errors":"Not Found"
}
We don't need the total count, as we are not using a Store

2. combobox autocomplete requests the models via the query parameter, as well as start and limit for paging

Request:

index?query=McGu&page=1&start=0&limit=25

Response:
{
  "success":true,
  "total":101,
  "data":[{model data}, {model data}, ...]
}

I am not sure why there is a page parameter along with start and limit. I chose to ignore the page parameter.

The total property shows how many records match the query, while you might send back only 25 if using paging. Note that the data property is now an array

Query is sort of like filter, though filter is intended to be on a particular property, and query could be on multiple properties, and is more free form

(Responses below are omitted as they are the same format as the above)

To turn on remote filter, sort, and grouping on a store, define your store like this:

Ext.define('My.store.Contacts', {
  extend:'Ext.data.Store',
  model:'My.model.Contact',
  remoteSort:true,
  remoteFilter:true,
  remoteGroup:true
});

3. Store paging uses the page parameter, and the start and limit parameters.

Request:

index?page=1&start=0&limit=25

3a. Store filtering uses the filter parameter and looks like this:

Request:

index?filter=[{"property":"first_name","value":"Neil"},{"property":"city","value":"Vancouver"}, ...]

as you can see, the filter parameter is a JSON array

3b. Store grouping looks like this:

index?group=[{"property":"country","direction":"ASC"},{"property":"province","direction":"ASC"}, ...]

You can group by multiple fields, or by one or none.

3c. Store sorting has two modes. simpleSortMode looks like this:

index?sort=email&dir=DESC

where dir is the direction of the sort

and, uh, not simpleSortMode, looks like this:

index?sort=[{"property":"email","direction":"DESC"},{"property":"last_name","direction":"ASC"}, ...]

So, you can sort by multiple fields in different directions if you want

You can change the sort mode by setting store.getProxy().simpleSortMode = true | false;

You can of course use filtering, sorting, grouping, and paging all at the same time

These are the function calls to perform the above:

myStore.filter('city', 'vancouver');
myStore.sort('last_name');   
myStore.group('country');

Here is the Yii PHP code to handle all of these requests and return database data to the client:

class ContactsController extends CController {

  public function actionIndex($id = null, $query = null, $filter = null, $sort = null, $dir = 'ASC', $start = 0, $limit = 25, $group = null) {

    /* the json property that contains the model(s): */
    $root = 'contacts';
    
    /* if request id, send back a single model: */
    if($id !== null){
      $model = $this->loadModel($id);
      header('Content-type:application/json');
      echo CJSON::encode(array(
   'success'=>true,
   $root=>$model
      ));
      exit();
    }
    
    $criteria = new CDbCriteria(array(
  'limit' => $limit,
  'offset' => $start
     ));
    
    /* let the user search first and last name via a combobox */
    if ($query !== null) {
      $criteria->condition = 'first_name LIKE :query OR last_name LIKE :query';

      /* use PDO parameters to avoid sql injection: */
      $criteria->params[':query'] = "$query%";
    }    

    /*safely apply group by parameters (only allow group property names that are on the model)*/
    $grouper = new ExtGrouper('Contact');
    $grouper->applyGroups($criteria);
    
    /*safely apply filters*/
    $filter = new ExtFilter('Contact');
    $filter->applyFilters($criteria);
    
    /*safely apply order by parameters*/
    $sorter = new ExtSorter('Contact');
    $sorter->applyOrder($criteria);

    header('Content-type:application/json');
    echo CJSON::encode(array(
 'contacts' => Contact::model()->findAll($criteria),
 'total' => Contact::model()->count($criteria), /* this query does a count(*) */
 'success' => true
    ));
    exit();
  }

  private function loadModel($id) {
    $model = Contact::model()->findByPk((int) $id);
    if ($model === null) {
      header('Content-type:application/json');
      echo CJSON::encode(array(
   'success' => false,
   'errors' => '404 Not Found'
      ));
      exit();
    } else {
      return $model;
    }
  }
}

Here is the code for ExtSorter. It ignores any sort->property that is not on the database model, which is safer. The other classes are very similar.

class ExtSorter {

  public $modelClass;
  public $sortVar = 'sort';

  public function __construct($modelClass) {
    $this->modelClass = $modelClass;
  }

  public function applyOrder($criteria) {
    $order = $this->getOrderBy();
    if (!empty($order)) {
      if (!empty($criteria->order))
 $criteria->order.=', ';
      $criteria->order.=$order;
    }
  }

  public function getOrderBy() {
    $attributeNames = CActiveRecord::model($this->modelClass)->attributeNames();
    if (isset($_GET[$this->sortVar])) {
      $sorters = json_decode($_GET[$this->sortVar], false);
      if($sorters === null){
 $sort = $_GET[$this->sortVar];
 $dir = isset($_GET['dir'])?$_GET['dir']:'ASC';
 $sorters = json_decode("[{\"property\":\"$sort\", \"direction\":\"$dir\"}]", false);
      }
      $sb = array();
      foreach ($sorters as $sorter) {
 if (in_array(strtolower($sorter->property), $attributeNames) && in_array(strtolower($sorter->direction), array('asc', 'desc'))) {
   $sb[] = "$sorter->property $sorter->direction";
 }
      }
      return implode(', ', $sb);
    }
    else
      return '';
  }

}

Saturday, November 19, 2011

Getters on Models

The convert method of a model field is called when a value is set, not read, which is kinda bass-ackwards.

It is also called when a model is constructed, and before any associations are loaded, meaning you shouldn't be using it as a pseudo-getter, which I've seen recommended elsewhere.

Here's how to make a virtual field on a model:

Ext.define('My.model.Contact', {
  extend:'Ext.data.Model',

  getfull_name: function(){
    return this.get('first_name') + ' ' + this.get('last_name');
  },

  fields:[
    'first_name',
    'last_name'
  ],

  /* override Model.get */
  get: function(field){
    if(this['get'+field]) return this['get'+field](); //if get<field> exists, call it
    else return this.callParent(arguments); //otherwise, use regular get
  }
});

Usage:

var contact = Ext.create('My.model.Contact');
contact.set('first_name');
contact.set('last_name');

console.log(contact.get('full_name'));.

Friday, November 18, 2011

You should be using the Supervising Controller pattern with Ext JS 4

You should be using the Supervising Controller pattern with Ext JS 4. Here's why:


  • pure MVC is wrong, as the controller is not allowed to communicate with the view. 
  • Passive View is wrong, as you can then not take advantage of data binding (think stores in comboboxes and grids)
  • it is easily testable
  • Martin Fowler recommends it
  • Microsoft recommends it in their Application Architecture guides

Sunday, November 13, 2011

Using nested data for many-to-many relationships

An interesting thing to note about using nested data is that Ext will automatically give your child models a foreign key to the parent model.

Say, for example, that you have a Post model (blog post) and a Category model. The relationship is many-many. In your database, you'd have a Post table, and Category table, and a PostCategory junction table. You probably shouldn't expose the junction to the client, as that is a database concept, and you should be abstracting that out.

Instead, you would return a post and its list of categories in json format (see below).

You can get, add, or remove categories from the post.categories() store (automatically created due to the hasMany relationship). I'm still working on saving the posts categories back, should get back on here tomorrow.

The Post model:

Ext.define('app.model.Post', {
  extend:'Ext.data.Model',
  fields:[
    {name:'id', type:'int'}, 
    {name:'title'},
    {name:'body'}
  ],
  proxy:{
    type:'ajax',
    url:'data/posts.json',
    reader:{
      type:'json',
      root:'posts'
    }
  },
  hasMany:[
    {
      name:'categories',
      model:'app.model.Category'
    }
  ]
})

The Category model:
Ext.define('app.model.Category', {
  extend:'Ext.data.Model',
  fields:[
    {name:'id', type:'int'},
    {name:'name', type:'string'}
  ]
});

The posts.json data:
{
  "success":true,
  "posts":[
    {
      "id":1,
      "title":"hi",
      "body":"there",
      "categories":[
 {
   "id":1,
   "name":"hardware"
 },
 {
   "id":3,
   "name":"software"
 }
      ]
    },
    {
      "id":2,
      "title":"so",
      "body":"long",
      "categories":[
 {
   "id":1,
   "name":"hardware"
 },
 {
   "id":2,
   "name":"porn"
 }
      ]
    }
  ]
}

The application:

Ext.Loader.setConfig({
    enabled: true
});

Ext.application({
    name: 'app',
    autoCreateViewport:false,
    enableQuickTips:false,
    requires:[
      'app.model.Post',
      'app.model.Category'
    ],

    launch: function() {
      app.model.Post.load(1, {
 success: function(record, op){
   console.log(record.categories())
 }
      })
    }
});

Friday, November 4, 2011

viewing the example extjs mvc applications

for some reason, sencha minimized the code for the example mvc applications.

to be able to read them, you can use http://jsbeautifier.org/

just copy the classes.js content from ext/examples/app/feed-viewer/classes.js to your clipboard, paste it into jsbeautifier, and hit beautify. you can then copy and paste the code back to the classes file

Tuesday, November 1, 2011

ExtJS Code Completion / Intellisense in Netbeans

Here's how to get ExtJS code completion / intellisense working in Netbeans (6.9.1):

  1. you need to reference extjs scripts locally, and not from a remote server
  2. use the ext-all-debug-w-comments.js version
  3. in netbeans, on the menu, go to tools -> options -> miscellaneous -> javascript tab -> and set the targeted browsers to the highest version numbers and set javascript version to 1.8
  4. reference ext-all-debug-w-comments.js in your index file
<script src="ext/ext-all-debug-w-comments.js"></script>


Ext JS Netbeans intellisense