A Better ExtJS Grid and Symfony – Part 1
A few months ago I wrote about ExtJS grid search and its integration with the symfony framework. Well, I’ve had some time to do some more development, and have been working on ExtJS integration with symfony for about a month now, and I’ve learned a few new things.
First off I’ve moved from symfony version 1.2 to the 1.4 version. That means I’ve switched ORMs from Propel to Doctrine. This debate parallels the debate between Mac and PC – both offer the same thing, both do things in their own unique way, and both can provide a lot of power. However, I’m making decision based on the fact that I think Doctrine will have continued steady development (and documentation) as it is now the “default” ORM of symfony. That being said, lets move into some code.
ExtJS + Symfony
Most of us who have worked with ExtJS probably know that the new 3.x version of ExtJS brought about the release of the Ext.Direct methods, which basically allow easy (and somewhat standardized) communication between the client and the server. One invaluable plugin that I’ve utilized in symfony is the dsExtDirectPlugin. This plugin allows you to take your symfony actions – add a few comments – that are then parsed into yaml file, and interpreted by an extdirect controller. It also generates then necessary api.js file all ready for you to call from your Ext components. I won’t go into how to set this up in your project (hint: if using 1.3/1.4 you’ll have to install the plugin from source, as the repository isn’t updated with the latest symfony version – but as far as I can tell – the plugin works as expected in 1.3/1.4 out of the box), and here’s the discussion surrounding it on the ExtJS Forums.
Building up Symfony
Alright lets say we have a need to display a grid of media items from the defined schema.yml file below. Each media item has a speaker, and several categories:
Media:
actAs:
Timestampable:
updated:
disabled: true
Sluggable:
fields: [title]
canUpdate: true
SoftDelete:
columns:
title:
type: string(255)
notnull: true
speaker_id:
type: integer
notnull: true
sku: string(255)
best_seller: bit(1)
relations:
Speaker:
foreignAlias: Media
Categories:
foreignAlias: Media
class: Category
refClass: MediaCategory
Speaker:
actAs:
Timestampable:
updated:
disabled: true
Sluggable:
fields: [name]
canUpdate: true
columns:
name:
type: string(255)
notnull: true
description:
type: string()
notnull: true
Category:
actAs:
Sluggable:
fields: [name]
canUpdate: true
NestedSet:
hasManyRoots: true
rootColumnName: root_id
columns:
name:
type: string(255)
notnull: true
MediaCategory:
columns:
media_id:
type: integer
primary: true
category_id:
type: integer
primary: true
relations:
Media:
foreignAlias: MediaCategories
Category:
foreignAlias: MediaCategories
Heres some sample data to help populate the database – copy this into your fixtures.yml file:
Speaker:
Speaker_1:
id: 1
name: John Doe
description: 'John Doe has many good media items avaliable.'
Speaker_2:
id: 2
name: Jane Somebody
description: 'Jane is just starting out as a speaker, but has a promissing career in media.'
Speaker_3:
id: 3
name: C. Moore Butz
description: 'A description of this speaker is not avaliable.'
Media:
Media_1:
id: 1
title: 'An Awesome CD'
sku: 'JP_1_1'
best_seller: '1'
speaker_id: 1
Media_2:
id: 2
title: 'Media about Media'
sku: 'JP_1_2'
best_seller: '1'
speaker_id: 2
Media_3:
id: 3
title: 'Another Great Media Title'
sku: 'JP_1_3'
best_seller: '1'
speaker_id: 1
Media_4:
id: 4
title: 'Another Great Media Title - Part 2'
sku: 'JP_2_1'
best_seller: '1'
speaker_id: 1
Media_5:
id: 5
title: 'Running out of names already'
sku: 'JP_2_2'
best_seller: '0'
speaker_id: 3
Media_6:
id: 6
title: 'Sitting Under The Bleachers'
sku: 'JP_2_3'
best_seller: '1'
speaker_id: 3
Media_7:
id: 7
title: 'The Areas of My Expertise'
sku: 'JP_2_4'
best_seller: '1'
speaker_id: 1
Media_8:
id: 8
title: 'Some form of CD Title'
sku: 'JP_3_1'
best_seller: '0'
speaker_id: 2
Category:
Category_1:
id: 1
name: Self Help
Category_2:
id: 2
name: Social Media
Category_3:
id: 3
name: Do It Yourself
Category_4:
id: 4
name: Audiobooks
MediaCategory:
MediaCategory_1:
media_id: 4
category_id: 1
MediaCategory_2:
media_id: 1
category_id: 2
MediaCategory_3:
media_id: 2
category_id: 3
MediaCategory_4:
media_id: 3
category_id: 4
MediaCategory_5:
media_id: 5
category_id: 2
MediaCategory_6:
media_id: 6
category_id: 3
MediaCategory_7:
media_id: 7
category_id: 1
MediaCategory_8:
media_id: 8
category_id: 3
MediaCategory_9:
media_id: 3
category_id: 1
MediaCategory_10:
media_id: 8
category_id: 1
Alright – now lets build our model/forms/db by running [cci_bash]symfony doctrine:build –all –and-load –no-confirmation[/cci_bash]. This of course will destroy all the data in your defined database, so make sure this is a fresh app, or you don’t value what data you’ve got this app connected too.
Creating the Grid
Lets start by creating a page for us to view the grid on. Run the following in your cli or terminal:
symfony generate:app main symfony generate:module main media
Next lets create a .js file that will build and display our grid:
Ext.ns('Media.main');
Ext.BLANK_IMAGE_URL = '/js/ext-3.0.0/resources/images/default/s.gif';
// Register provider
Ext.app.JP_API.enableBuffer = 0;
Ext.Direct.addProvider(Ext.app.JP_API);
Ext.onReady(function() {
Ext.QuickTips.init();
var view = new Ext.Viewport({
items:[{
id: 'content-panel',
region: 'center', // this is what makes this panel into a region within the containing layout
margins: '0 0 0 3',
layout: 'card',
border: false,
split:true,
autoScroll:true,
activeItem: 0,
items: [
{xtype:"media_grid", id:"mediaGrid",hideMode:'offsets'}
]
}]
});
});
/********************
/ A function to turn returned mysql bit() chars into '1' or '0'
*********************/
function ord (string) {
// Returns the codepoint value of a character
//
// version: 909.322
// discuss at: http://phpjs.org/functions/ord
// + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// + bugfixed by: Onno Marsman
// + improved by: Brett Zamir (http://brett-zamir.me)
// * example 1: ord('K');
// * returns 1: 75
// * example 2: ord('\uD800\uDC00'); // surrogate pair to create a single Unicode character
// * returns 2: 65536
var str = string + '';
var code = str.charCodeAt(0);
if (0xD800 <= code && code <= 0xDBFF) { // High surrogate (could change last hex to 0xDB7F to treat high private surrogates as single characters)
var hi = code;
if (str.length === 1) {
return code; // This is just a high surrogate with no following low surrogate, so we return its value;
// we could also throw an error as it is not a complete character, but someone may want to know
}
var low = str.charCodeAt(1);
if (!low) {
}
return ((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
}
if (0xDC00 <= code && code <= 0xDFFF) { // Low surrogate
return code; // This is just a low surrogate with no preceding high surrogate, so we return its value;
// we could also throw an error as it is not a complete character, but someone may want to know
}
return code;
}
And now the actual grid code itself:
Ext.ns('Media.main');
Media.main.MediaGrid = Ext.extend(Ext.grid.GridPanel , {
initComponent: function() {
this.expander = new Ext.grid.RowExpander({
tpl: new Ext.XTemplate(
'<h2>Media Categories</h2>',
'<tpl if="Categories">',
'<tpl for="Categories">',
'{name}<br />',
'</tpl>',
'</tpl>')
});
this.store = new Ext.data.DirectStore({
storeId:'mediaList',
directFn: jpActions.media.getMediaList,
paramsAsHash:true,
fields:['id','title','best_seller','sku',{name:'speaker',mapping:'Speaker.name'},'Categories','created_at'],
idProperty:'id',
root:'media',
totalProperty:'total_count',
sortInfo:{
field:'title',
direction:'ASC'
},
remoteSort:true,
paramOrder: ['sort','dir','start','limit','fields','query'],
paramNames:{
start : 'start', // The parameter name which specifies the start row
limit : 'limit', // The parameter name which specifies number of rows to return
sort : 'sort', // The parameter name which specifies the column to sort on
dir : 'dir', // The parameter name which specifies the sort direction
fields: 'fields',
query: 'query'
},
listeners: {
load:function(){
//console.info('load',this,arguments);
}
}
});
this.store.setDefaultSort("title", "ASC");
this.rowActions = new Ext.ux.grid.RowActions({
actions:[{
iconCls:'icon-best-seller',
qtip:'Media is a Best Seller',
style:'margin:0 0 0 3px'
}]
});
this.rowActions.on('action', this.onRowAction, this);
/* Ext 3.0 Feature
this.editor = new Ext.ux.RowEditor({
saveText: 'Update'
});
*/
this.pagingBar = new Ext.PagingToolbar({
pageSize: 150,
store: this.store,
displayInfo: true,
dispalyMsg: 'Displaying {0} - {1} of {2}',
emptyMsg: 'No Media Found'
});
this.sm = new Ext.grid.CheckboxSelectionModel();
Ext.apply(this, {
loadMask:true,
columns:[this.sm,this.expander,{
id:'title',
header:'Media Title',
sortable:true,
width:80,
dataIndex:'title'
},{
id:'speaker_id',
header:'Speaker',
sortable:true,
width:80,
dataIndex:'speaker'
},{
id:'sku',
header:'SKU',
sortable:true,
width:120,
dataIndex:'sku'
},{
id:'best_seller',
header:'Best Seller',
sortable:true,
width:80,
dataIndex:'best_seller'
},{
id:'created_at',
header:'Created At',
sortable:true,
width:80,
dataIndex:'created_at'
},
this.rowActions
],
sm:this.sm,
stripeRows:true,
title:"Jeff Pipas - Doctrine Media Grid",
viewConfig:{forceFit:true},
plugins:[new Ext.ux.grid.Search({
iconCls:'icon-zoom',
minChars:3,
width:240,
autoFocus:true
}),this.rowActions,this.expander],
bbar:this.pagingBar
});
this.on({
beforeshow:{
scope:this,
single:true,
fn:function(){
this.store.load({params:{sort:'title',dir:'ASC',start:0,limit:150}});
}
}
});
Media.main.MediaGrid.superclass.initComponent.apply(this, arguments);
},
onRowAction:function(grid, record, action, row, col) {
}
});
Ext.reg("media_grid", Media.main.MediaGrid);
So I’m assuming you’re pretty familiar with what I’m doing here. I’m basically creating a small application. The first file (I’ve named it app.js) sets up my Ext.Direct api calls – as well as initializes my grid using my defined xtype, which I built in the second file (named media_grid.js). The grid is pretty straight forward. I’ve got an expander row to show all the media’s categories, and a few columns to display the data, along with a paging bar, and the search box.
To Be Continued
Thats all for now – this should at least get you started – we’ve got to add our ExtJS library files, and a few plugin files and define them in our view.yml config next. Then, the fun begins. Stay tuned!
I want to quote your post in my blog. It can?
And you et an account on Twitter?
You can quote it yes, as long as you link back to the original post.
Greetings from Poland
I have problem with this tutorial. It’s seems not work.
grid component isn’t display in my project.
I installed sfplugin using pear channel in views.yml i added path to javascripts: [app.js, media_grid.js, ext/adapter/ext/ext-base-debug.js, ext/ext-all-debug.js], like you said. Also generate database using doctirne. Still doesn’t have any effects.
Please help me solve my problem.
I’m using Symfony 1.4.1, doctrine 1.2
@Wojciech I’ve updated some of the .js files here – they had some typos. See if that fixes the problem. Else, there might be something wrong with the way you’ve got dsExtDirectPlugin installed. Hope the updated code fixes it – cheers!
Hi Jeff,
I followed your post and I wonder if you already have part 2?
Hi,thanks for your post.
I tried your code but I get the following error:
uncaught exception: Ext.data.DataProxy: DataProxy attempted to execute an API-action but found an undefined url / function. Please review your proxy url / api-configuration.
May depend on what? Thanks!
Poldotz
Any chance of you wrapping this up? I’m curious to see the rest of the pieces as a Symfony noob.