Sunday, August 12, 2012
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:
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
- set the URL as /echo/json/ [you need that last slash]
- if you are doing a read action (such as in Model.load) you need to set the proxy.actionMethods.read to "POST"
- set "json" as an extraParam and pass the json string that you want echoed back
- 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/
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.
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:
To set it as the background image, add this style to your index page:
You should also, of course, use Sencha SDK Tools to compress and minimize your files.
* remove any user-specific or time-sensitive info
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:
Your Parties store would then contain an Individual object and an Organization object.
Peace.
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
- Put the proxy in the model, unless you have a very good reason not to [1]
- Always use fully qualified model name
- Always set the getterName
- Always set the setterName
- Always set the associationKey, if the foreign object is returned in the same response as this object
- Always set the foreignKey, if you want to load the foreign object at will
- Consider changing the instanceName to something shorter
- 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.
- You should set the name property if you plan to override this association.
- You do not need a belongsTo relationship for a hasMany to work
- Set the primaryKey property if the id field of the parent model is not "id"
- Sometimes you need to use uses or requires for the belongsTo association. Watch out for circular references though.
- 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
- Always put your Proxies in your Models, not your Stores, unless you have a very good reason not to *
- Always require your child models if using them in hasMany relationships. **
- Always use foreignKey if you want to load the children at will
- Always use associationKey if you return the children in the same response as the parent
- You can use both foreignKey and associationKey if you like
- Always name your hasMany relationships
- Always use fully qualified model names in your hasMany relationship
- Consider giving the reader root a meaningful name (other than "data")
- 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 .
Monday, May 21, 2012
Javascript SOLID principles
This guy seems to know what he's talking about:
http://aspiringcraftsman.com/series/solid-javascript/
http://aspiringcraftsman.com/series/solid-javascript/
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.
Therefore, you need to try to navigate first, and only if it's successful, do you change the URL bar.
In your app, you want to change the URL bar AFTER you navigate, not before.
Here's why:
- Your app should work without browser history manipulation, so you need some sort of navigation system in the first place
- 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">
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.
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.
Tuesday, April 3, 2012
Looks like Chrome 18 breaks Sencha ExtJS
Using Chrome 18, try clicking the reload button twice here:
http://dev.sencha.com/deploy/ext-4.0.7-gpl/examples/charts/Line.html
Many of the buttons in my app started disappearing in Chrome 18 only...hopefully will be fixed in 19!
http://dev.sencha.com/deploy/ext-4.0.7-gpl/examples/charts/Line.html
Many of the buttons in my app started disappearing in Chrome 18 only...hopefully will be fixed in 19!
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
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.
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:
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:
something like this:
- set the Basic Form's trackResetOnLoad to true
- 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/
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
An ExtJS MVC application with dynamically loaded views and controllers
Here's a slightly more complex MVC application, that allows for dynamic loading of controllers and views:
here is the jsfiddle code
here is the jsfiddle code
How to simulate ExtJS AJAX requests in JSFiddle
Here's how to simulate AJAX requests using ExtJS and JSFiddle, using the SimManager user extension
Please note that this user extension only supports GETs for the moment. It's not mine, but I will try to update it to support POSTs too.
here is the code
Please note that this user extension only supports GETs for the moment. It's not mine, but I will try to update it to support POSTs too.
here is the code
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:
to get a reference to the viewport.
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.
Subscribe to:
Posts (Atom)