Pages

Sunday, August 12, 2012

How to change the LoadMask text and target

How to change the default presence validation message

The default model "presence" validation message is "must be present", which is kinda ugly.

Here's how to change it to something better:


// set this near the beginning of your program:

Ext.data.validations.presenceMessage = "Required";

Saturday, August 11, 2012

Using ExtJS and JQuery on the same page

This widget shows an ExtJS Panel and Image component, along with the JQuery DeepLiquid JCrop widget, which allows you to select a crop area on an image.

Try it out by dragging your mouse over the image. You can also move the "crop" around.

 

Thursday, July 26, 2012

A function to concatenate field values in a store

Below is a function to concatenate the values in a field in a store. This is similar to Store.sum();

Ext.data.Store.override({
    
    concat: function(field, grouped, separator){
        
        if(typeof separator === 'undefined'){
            separator = ', ';
        }
        
        if (grouped && this.isGrouped()) {
            return this.aggregate(this.getConcat, this, true, [field, separator]);
        } else {
            return this.getConcat(this.data.items, field, separator);
        }    
    },
    
    getConcat: function(records, field, separator){
        var result = [];
        var i = 0;
        var len = records.length;
        
        for(; i < len; ++i){
            result.push(records[i].get(field));
        }
        return result.join(separator);
    }
});  
    
For example, if the store has a field 'email', with data 'a@b.ca' and 'b@c.com', then store.concat('email') would return 'a@b.ca, b@c.com'

Wednesday, July 18, 2012

How to capture the HTMLEditor's onpaste event

The key to capturing the onpaste event for the HTMLEditor component is to wait for its initialize event to fire and then add the onpaste handler to its document body:


{
xtype: 'htmleditor',
listeners: {
  initialize: function(cmp) {

    Ext.EventManager.addListener(

      cmp.iframeEl.dom.contentWindow.document.body

      , 'paste'

      , function(e, elem){
          console.log(arguments);
      });

    }
  }
}

Monday, July 16, 2012

How to simulate AJAX requests in JSFiddle

  1. set the URL as /echo/json/  [you need that last slash]
  2. if you are doing a read action (such as in Model.load) you need to set the proxy.actionMethods.read to "POST"
  3. set "json" as an extraParam and pass the json string that you want echoed back
  4. you can optionally set a delay in extraParams

Here is a little function to help you simulate an AJAX request for a proxy:
function sim(proxy, data, delay){

  if(typeof data !== 'string'){
    data = Ext.JSON.encode(data);
  }

  proxy.url = '/echo/json/';
  proxy.actionMethods.read = "POST";
  proxy.extraParams.json = data;
  proxy.extraParams.delay= typeof delay == 'undefined' ? 0 : delay;
}

//usage:

sim(MyModel.proxy, {name:"neil"}, 2);

MyModel.load(1, {
  callback: function(record, operation){ console.log( record.get('name'); ) }
});

Example:

//jsfiddle.net/el_chief/9ksWE/



/* this is a function to simulate ajax requests in jsfiddle

This is the Sencha Touch 2.01 compatible version
 */
function sim(proxy, data, delay) {
    if (typeof data !== 'string') {
        data = Ext.JSON.encode(data);
    }

    proxy.setUrl('/echo/json/');
    proxy.setExtraParams({
        json:data,
        delay: typeof delay === 'undefined' ? 0 : delay
    });
    proxy.setActionMethods({read:'POST'});
}

Sunday, July 15, 2012

This blog, in French

The smart guys at Developpez.com (a very popular French dev portal) asked to translate and post some of my blog articles on their site. Here it is:

http://neilmcguigan.developpez.com/


Thursday, July 5, 2012

ExtJS Trouble-shooting

Uncaught Error: The following classes are not declared even if their files have been loaded: 'My.folder.File'. Please check the source code of their corresponding files for possible typos: 'app/folder/File.js ext-all-dev.js:9655
Ext.apply.onFileLoaded ext-all-dev.js:9655
(anonymous function) ext-all-dev.js:2953
Ext.apply.injectScriptElement.onLoadFn


This means that you required the file correctly, but that it's defined name did not match the file name. For example, if you require My.folder.File but define it like this:

Ext.define('My.folder.File2', {...});

more coming soon...

Monday, June 25, 2012

RowEditing Plugin: validateedit event

The rowediting plugin fires a validateedit event after its Update button is clicked.

It is fired before the record is populated with the form data.

Here is how to handle the validateedit event if you want to validate a model and show its validation errors:

The editor.editor.form reference is to a BasicForm and not a Form Panel.

validateedit: function(editor, e, eOpts){
    var newModel = e.record.copy(); //copy the old model
    newModel.set(e.newValues); //set the values from the editing plugin form

    var errors = newModel.validate(); //validate the new data
    if(!errors.isValid()){
      editor.editor.form.markInvalid(errors); //the double "editor" is correct
      return false; //prevent the editing plugin from closing
    }
}

ExtJS Faster Loading

Here's how to make your app appear to load faster. It's a trick used in iOS programming:

1. take a screenshot of your application, in its initial but loaded state *
2. blur or mask this image somewhat
3. load this image first, and set it as the HTML background image
4. load ExtJS and your application
5. start your application
6. the app will start and show its interface, and the viewport will cover the HTML background

To make sure an image loads first, put this script at the top:

(new Image()).src = 'path to your startup image';

To set it as the background image, add this style to your index page:

html {
  background-image: url('path to your startup image');
  background-size: 100%;
}

You should also, of course, use Sencha SDK Tools to compress and minimize your files.

* remove any user-specific or time-sensitive info

Saturday, June 16, 2012

Polymorphic JSON: Changing the model type based on the response

I am using the Party Model, and needed to create an Individual or Organization model depending on the JSON response. Had to override Ext.data.reader.Reader to do it. Ideally, Sencha would fire a "creating model" event or hook that would let us pick the model at Read time. The system looks like this:

PARTY
id
name
getText() - returns name

ORGANIZATION : PARTY
doingBusinessAs
getText() - returns doingBusinessAs

INDIVIDUAL : PARTY
middleName
firstName
birthday
getText() override - returns full name

Handles overridden fields and associations properly. You should use the name property on belongsTo associations if you want to override it.


Here is the override code:

Ext.data.reader.Reader.override({
    extractData:function(root){


        var me = this,
            records = [],
            Model   = me.model,
            length  = root.length,
            convertedValues, node, record, i;


        if (!root.length && Ext.isObject(root)) {
            root = [root];
            length = 1;
        }


        for (i = 0; i < length; i++) {
            node = root[i];

        /* OVERRIDE */
        if(me.types){
        var typeCode = node[me.typeProperty || 'type'];
        var type = me.types[typeCode];
        me.model = Ext.ClassManager.get(type); //the model reference is used in other functions
        Model = me.model;
        me.buildExtractors(true); //need to rebuild the field extractors
        }
        /* END OVERRIDE */


        record = new Model(undefined, me.getId(node), node, convertedValues = {});

            record.phantom = false;

            me.convertRecordData(convertedValues, node, record);

            records.push(record);


            if (me.implicitIncludes) {
                me.readAssociated(record, node);
            }
        }


        return records;
    }
});

//Here is how you would use it:

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


    requires:[
    'My.model.Individual',
    'My.model.Organization'
    ],


    fields:[
    'id',
    'type',
    'name',
    {
    name:'text',
    mapping:'name'
    }
    ],


    proxy:{
    type:'ajax',
    url:'parties',
    reader: {
        root:'parties',
        types:{
        'i':'My.model.Individual',
        'o':'My.model.Organization'
        }
    }
    }
});

// the json:

{
success: true,
parties:[
{ id:4, type:'i', name:'chief', firstName:'el', middleName:'_', birthday:'May 1' },
{ id:99, type:'o', name:'Sencha, Inc.', doingBusinessAs:'Sencha' }
]
}


Your Parties store would then contain an Individual object and an Organization object.

Peace.

Friday, May 25, 2012

Rules for HasOne and BelongsTo Associations in ExtJS

  1. Put the proxy in the model, unless you have a very good reason not to [1]
  2. Always use fully qualified model name
  3. Always set the getterName
  4. Always set the setterName
  5. Always set the associationKey, if the foreign object is returned in the same response as this object
  6. Always set the foreignKey, if you want to load the foreign object at will
  7. Consider changing the instanceName to something shorter
  8. The getter behaves differently depending on whether the foreign object is loaded or not. If it's loaded, the foreign object is returned. Otherwise, you need to pass in a callback to get it.
  9. You should set the name property if you plan to override this association.
  10. You do not need a belongsTo relationship for a hasMany to work
  11. Set the primaryKey property if the id field of the parent model is not "id"
  12. Sometimes you need to use uses or requires for the belongsTo association. Watch out for circular references though.
  13. Calling setter() function does not seem to set the instance. Set object.belongsToInstance = obj  if calling the setter().

Ext.define('Assoc.model.PhoneNumber', {
    extend:'Ext.data.Model',

    fields:[
        'number',
        'contact_id'
    ],

    belongsTo:[
    {
      name:'contact',
      instanceName:'contact',
      model:'Assoc.model.Contact',
      getterName:'getContact',
      setterName:'setContact',
      associationKey:'contacts',
      foreignKey:'contact_id'
    }
    ],

    proxy:{
        type:'ajax',
        url:'assoc/data/phone-numbers.json',
        reader:{
            type:'json',
             root:'phoneNumbers'
        }
    }
});

/*
 * Assuming Contact model uses an AJAX proxy with url 'contacts', and its id field is "id", 
 * the below function call will make an http request like this:
 * /contacts?id=88
 */

var pn = new Assoc.model.PhoneNumber( { contact_id:88 } );

pn.getContact( function(contact, operation){ 
  console.log('tried to load contact. this.contact is now set to the contact');
} );

/* you can call phoneNumber.setContact(contact). This will set contact_id on the phone number, BUT it won't set the contact instance on phonenumber, which is likely a bug: */

var contact = new Assoc.model.Contact({id:77});

var phoneNumber = new Assoc.model.PhoneNumber();

phoneNumber.setContact(contact);

console.log(phoneNumber.get('contact_id')) //77

console.log(phoneNumber.contact) //undefined

phoneNumber.contact = contact; //you need to do this if you want to use that reference in the future.






[1] The store will inherit its model's proxy, and you can always override it if necessary

Rules for HasMany Associations in ExtJS

  1. Always put your Proxies in your Models, not your Stores, unless you have a very good reason not to *
  2. Always require your child models if using them in hasMany relationships. ** 
  3. Always use foreignKey if you want to load the children at will
  4. Always use associationKey if you return the children in the same response as the parent
  5. You can use both foreignKey and associationKey if you like
  6. Always name your hasMany relationships
  7. Always use fully qualified model names in your hasMany relationship
  8. Consider giving the reader root a meaningful name (other than "data")
  9. The child model does not need a belongsTo relationship for the hasMany to work

Example:
Ext.define('Assoc.model.Contact', {

    extend:'Ext.data.Model',

    requires:[
        'Assoc.model.PhoneNumber'          /* rule 2 */
    ],

    fields:[
        'name'                             /* id field is inherited from Ext.data.Model */
    ],

    hasMany:[
    {
        foreignKey: 'contact_id',          /* rule 3, 5 */
        associationKey: 'phoneNumbers',    /* rule 4, 5 */
        name: 'phoneNumbers',              /* rule 6 */
        model: 'Assoc.model.PhoneNumber'   /* rule 7 */
}
    ],

    proxy:{                                /* rule 1 */
        type: 'ajax',
        url: 'assoc/data/contacts.json',
        
        reader: {
            type: 'json',
            root: 'contacts'               /* rule 8 */
        }
    }
});

// example usage:

var c = new Assoc.model.Contact({id:99});

/*
 * assuming that PhoneNumber.proxy is AJAX and has a url set to 'phonenumbers', the below function would make an http request like this:
 * /phonenumbers?page=1&start=0&limit=25&filter=[{"property":"contact_id","value":"99"}]
 * ie, it would make a request for all phone numbers, but ask the server to filter them by the contact_id of the Contact, which is 99 in this case
 */
c.phoneNumbers().load(); 

* The store will inherit its model's proxy, and you can always override it
** To make it easy, and avoid potential circular references, you can require them in app.js .

Saturday, May 19, 2012

ExtJS History is backwards

Ext JS's History navigation is backwards. I know why they did it that way (in the name of cross-browser compatibility), and most people, like balupton's history.js, get it wrong too.

In your app, you want to change the URL bar AFTER you navigate, not before.

Here's why:
  1. Your app should work without browser history manipulation, so you need some sort of navigation system in the first place
  2. You don't want to change the URL bar if the user doesn't want to leave the current form (because it's dirty)

Therefore, you need to try to navigate first, and only if it's successful, do you change the URL bar.

Unfortunately, Ext.History makes this impossible, as change() always fires after add(), no matter what you do (because there is a timer that polls the URL bar for changes every 50 ms).

HTML5 pushState and onpopstate do it correctly, in that calls to pushState() never fire onpopstate. onpopstate is only ever fired by the user, which is what you want.

Your navigation process should look something like this:

  • User is at "Tasks" screen, for example. URL is "#tasks"
  • User clicks the "Contacts" button or similar
  • Your app asks the current form if it is OK to navigate away (perhaps w a user confirmation dialog)
  • If not, do nothing. If ok, display the contact form. 
  • Update the URL bar to "#contacts", using pushState

  • URL bar is at "#contacts"
  • User hits the back button
  • URL bar is at "#tasks". onpopstate fires
  • App asks the Contacts form if it's ok to unload
  • If not OK, do nothing and change the URL bar back to "#contacts", usually with replaceState
  • If OK, load the tasks form

Tuesday, May 15, 2012

Disabling Chrome's Translate Function in your ExtJS Application

To disable Chrome's Google Translate function in your ExtJS app, add this to your <head>:

HTML 5:

<meta name="google" value="notranslate">

Wednesday, April 25, 2012

Ext Replace History

You can use Ext.History.add() to change the URL bar and add an item to the browser's history. For example, you could go from example.com#contacts to example.com#email

This doesn't reload the page, but you can listen for the change and load different forms in Ext, for example

Today, however, I needed to change the URL without adding an item to History, yet still fire the Ext.History.change event. To do that call:

window.location.replace('#somevalue');

I added a function to Ext.History:

Ext.History.replace = function(value){

    location.replace('#' + value);
};

You might want to use this to go from contacts/create to contacts/99 (say it autosaved), without the using having to hit back twice to get back to where they came from. 

Tuesday, April 17, 2012

Sencha Architect is out

http://www.sencha.com/products/architect

If you bought Ext Designer this year, you get Architect for free.

Formerly Sencha Designer 2, and before that Ext Designer. Registering my version now and will post some videos soon.

ExtJS 4.1 Docs are finally up

Looks like 4.1 is ready for prime-time

http://docs.sencha.com/ext-js/4-1/

Saturday, March 31, 2012

Things your server needs in order to not suck

On the database side, you need full-text search, you need transactions, check constraints, and you probably need real schemas.

Full-Text search allows you to find records by similarity, which is what most users expect these days (thanks Google). Some moron will tell ya, "oh you can use Lucene or Sphinx". Too bad they don't really talk to databases eh? Ya, polling your database for modified records every minute is a great idea. Upon getting Lucene to sync with MySQL you will end up like Mel Gibson in this scene:

http://www.youtube.com/watch?v=UpOqkz86_lg#t=135s

And you need transactions and check constraints if you remotely care about the integrity of your data. If you don't, please continue to use MyISAM.

You need schemas (real ones, not MySQL ones) if you want a multi-tenant system that doesn't suck. If you only have one tenant, this doesn't apply. You can still do it with MySQL, but it provides much butt-hurt.

The above requirements rules out MySQL. UPDATE: MySQL 5.6.4 supports full-text search on InnoDB! Holy shit. Still no check constraints or schemas though. Use PostgreSQL.

On the application server side, you need a comprehensive validation library, deeply-nested JSON support, support for table inheritance, and concurrency control in your ORM.

By deeply nested JSON support I mean being able to load JSON from a POST into your models and into your database in a couple lines of code, with validation. With hasOne, hasMany, and manyMany relationships. You should also be able to write 50 records or so, with relations of arbitrary nesting depth, very fast.

By support for table inheritance, I mean this: http://martinfowler.com/eaaCatalog/singleTableInheritance.html , and the other schemes. Ruby's ActiveRecord supports STI, and PHP's Doctrine supports single and multi. Yii's ActiveRecord sorta-kinda-almost supports STI, but requires some work.

Concurrency means that your UPDATE will fail if you try to update a record that someone has updated since you read it. Usually this means a query like this:

UPDATE mytable SET (field1='happy' and field2='monday') where id=59 and (field1='sad' and field2='sunday')

Unfortunately, this is not built in to Yii, which I have been using. Yii's validation library is actually really good, but it sucks at nested JSON and it doesn't do concurrency. It looks like Doctrine ORM does concurrency, but has no validation for some bizarre reason. I might have to switch to Ruby, or hell, even go back to ASP.Net

Tuesday, February 14, 2012

How to disable change / validation events when using loadRecord

When you use Basic.loadRecord() to load a record into a form, all of the form fields fire their change events, which you might not want. Here's a quick way to fix this:

This prevents change events from firing, but then re-enables them:

var basic = form.getForm();

basic.getFields().each(function(item, index, length){
  item.suspendCheckChange++;
});

basic.loadRecord(myRecord);

basic.getFields().each(function(item, index, length){
  item.suspendCheckChange--;

}); 

How to prompt the user to save a dirty form if leaving the page

How to prompt the user to save a dirty form if leaving the page:

  1. set the Basic Form's trackResetOnLoad to true
  2. handle the form's dirtychange event

something like this:

function my_onbeforeunload(e){
  message = 'You have unsaved changes. Are you sure you want to leave this page?'; 
  e = e || window.event;
  if(e){
    e.returnValue = message;
  }
  return message;
}

form.on('dirtychange', function(basic, dirty, eOpts){
  window.onbeforeunload = dirty ? my_onbeforeunload : null; 
});

Tuesday, January 31, 2012

Sencha Designer 2 Beta is out

I am a big fan of Ext Designer 1.2. It saves a ton of time designing a UI. Doesn't help much with MVC or writing code though.

It has been re-branded as Sencha Designer 2 (due to Sencha touch integration), and has built-in code editing and MVC! The beta is out now.

You can try it here:

http://www.sencha.com/forum/showthread.php?152941-Sencha-Designer-2-Beta-Download-links

Sencha didn't link to the docs anywhere, but here they are:

http://docs.sencha.com/designer/2-0/

Monday, January 16, 2012

Thursday, January 12, 2012

How to load a single record into a store

Usually a store loads a list of records, but if you need to load a single record, do this:
var id = 9; //an example record id

var contactsStore = Ext.StoreManager.get('contacts');

contactsStore.load({
  id: id, //set the id here
  scope:this,
  callback: function(records, operation, success){
    if(success){
      var contact = records[0];
      //do something with the contact record
    }
  }
});
this will send an async request to your server, using the store or its model's proxy, with the read api or its url, something like

/api/contacts/read?id=9

Friday, January 6, 2012

How to observe all the events on a view

Here's how to observe all of the events on a view:
var view = Ext.create('My.view.SomeView');

Ext.util.Observable.capture(view, function(){
  console.log(arguments);
});
This will show output in the console window whenever an event happens to the view, such as render. A useful debugging tool.

An easy way to get a reference to the viewport from any controller

Here's an easy way to get a reference to your viewport from any controller:

Ext.application({

  launch: function(){

    this.viewport = Ext.ComponentQuery.query('viewport')[0];


    this.centerRegion = this.viewport.down('[region=center]');

  }

});

then, inside say, a controller, you can call this:

this.application.viewport

to get a reference to the viewport.