Ajax design patterns

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

User avatar
inghamn
Forum Contributor
Posts: 174
Joined: Mon Apr 16, 2007 10:33 am
Location: Bloomington, IN, USA

Re: Ajax design patterns

Post by inghamn »

Just recently something clicked for me with AJAX applications. It was what the term "application" is referring to in the first place. All along I've been doing web applications, which had a ton of screens each at a URL.

A recent conference proceeding made the point for me that, with AJAX stuff, what you're really doing is providing a complete, task-oriented rich "application" at each URL of your website.

So all along, these JavaScript folks have been thinking of applications as just one URL. (Like Yahoo home pages or such.) PHP's job in all this is to deliver the source code for these applications to the browser. And the source code is mix of HTML, CSS, and JavaScript.

PHP also has the job of delivering the data (through web services) needed by these applications.
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: Ajax design patterns

Post by VladSun »

arborint wrote:
VladSun wrote:My web-apps are fully AJAX featured. That is, except for the main page HTML View I have no more HTML code. Indeed, I don't have any server side Views - everything is sent to the user as JSON data (if you consider an JSON formatter helper a View ... I have one View ;) ). That is, the View part is always client side - no templates, no code duplication (except for the validation, which I think can't be done in other way).
So you have one piece of Javascript code that you load with the main page and that builds the page from JSON data for every page in your site?
Hm ... yes!
But it's more like a desktop application rather than a collection of web pages ...
A typical web application of mine looks like this:
http://www.extjs.com/deploy/dev/example ... sktop.html
There are 10 types of people in this world, those who understand binary and those who don't
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: Ajax design patterns

Post by Christopher »

So following ingham's "every URL is an application" idea ... when do you send a new Javascript? Do you have multiple "main pages"? Or a huge do-everything main Javascript app? Or small focused websites?
(#10850)
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: Ajax design patterns

Post by VladSun »

One single main page. It loads the core JS libraries and the main Application class. Then it fetches the list of modules available to the current user's role and loads their "meta data". If a module is going to be executed an AJAX call fetches its "real" class and it is run.
There is even a dependency register (in every class declaration), so JS files needed by the class to be loaded are preloaded first.

Also, I won't consider my web applications "small focused" web sites. One of them is a fully featured small ISP management system for back and front office. While one may call it "huge do-everything main Javascript", I think it's modular enough and it works flawlessly.

PS: do you remember this topic : viewtopic.php?f=19&t=93937 :)
There are 10 types of people in this world, those who understand binary and those who don't
josh
DevNet Master
Posts: 4872
Joined: Wed Feb 11, 2004 3:23 pm
Location: Palm beach, Florida

Re: Ajax design patterns

Post by josh »

So it is 1 class or or not? You said 1 main application class but what is a module meta data? Is all your code abstracted and your modules are simply listings of fields to render in a grid? and things like that?
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: Ajax design patterns

Post by VladSun »

josh wrote:So it is 1 class or or not?
Well, the Application class depends on several classes:
[js]Ext.dependencies ={    application :    [        'ex/app/CurrentUser',        'ex/app/CurrentSettings',        'desktop/AppWatch',        'desktop/StartMenu',        'desktop/TaskBar',        'desktop/Desktop',        'desktop/Module',        'desktop/MRTGModule',        'desktop/ReportModule',        'desktop/Shortcut',        'desktop/Notification',        'desktop/App',        'ex/Sound',        'ex/Image',        'ex/Printer'    ],.... [/js]
But there are no classes not related to the framework itself loaded. See what I meant by "meta data" below.
josh wrote:You said 1 main application class but what is a module meta data?
You know that JS classes are prototype based, so you can extend them and their instances at runtime. So ... this is a "meta-class" (see below its "real" class):

personnelview-launcher.js
[js](function (){    return (    {        translationContext  : 'ledger',        translationSource   : null,         dependencies        : null,         create  : function ()        {            Ext.namespace('Ext.Module');            Ext.Module.Ledger = Ext.extend(Ext.app.Module,            {                moduleId    : 'mod-ledger',                menuPath    : 'StartMenu/' + Ext.Translation.get('menu', 'ledger_menu'),                 launcher    :                {                    iconCls         : 'ledger-icon',                    shortcutIconCls : 'ledger-shortcut',                    text            : Ext.Translation.get('ledger', 'title'),                    tooltip         : Ext.Translation.get('ledger', 'module_description', true)                }            });             return new Ext.Module.Ledger();        }    })})();[/js]

josh wrote:Is all your code abstracted and your modules are simply listings of fields to render in a grid? and things like that?
I didn't get this one ... Are you talking about the JS "modules" or for PHP modules?

If you are talking about JS modules - well, they are not so thin:
[js](function (){    return (    {        translationContext  : 'ledger',        translationSource   : null,         dependencies        : Ext.dependencies.singleEditGrid,         create  : function ()        {            Ext.namespace('Ext.Module');            Ext.override(Ext.Module.Ledger,            {                init        : function ()                {                /*                **  ledger JsonStore extended                **                */                    var ledgerDataStore = new Ext.data.JsonStore(                    {                        proxy: new Ext.data.HttpProxy(                        {                            url     : '/ledger/personnelView',                            method  : 'POST'                        }),                         root        : 'response',                        totalProperty : 'totalCount',                         recordModel : Ext.data.Record.create(                        [                            {name: 'id',            type: 'int',    defaultValue: 0},                            {name: 'operation_id',  type: 'int',    defaultValue: ''},                            {name: 'value',         type: 'int',    defaultValue: ''},                            {name: 'notice',        type: 'string', defaultValue: ''}                        ]),                        fields      :                        [                            {name: 'id',            type: 'int'     },                            {name: 'time',          type: 'date',   dateFormat  : 'Y-m-d H:i:s' },                            {name: 'operation_id',  type: 'int'     },                            {name: 'operation_sign',type: 'int'},                            {name: 'value',         type: 'float'   },                            {name: 'notice',        type: 'string'  },                            {name: 'username',      type: 'string'  },                            {name: 'fullname',      type: 'string'  }                        ],                        sortInfo    : {field: 'time', direction: 'ASC'},                        remoteSort  : true,                        autoLoad    : true,                        listeners   :                        {                            loadexception :                            {                                fn      : function ()                                {                                    Ext.Msg.error(Ext.Translation.get('common', 'load_data_failure'));                                }                            }                        }                    });                    ledgerDataStore.setDefaultSort('time', 'asc');                     this.ledgerOperationDataStore = new Ext.data.JsonStore(                    {                        proxy: new Ext.data.HttpProxy(                        {                            url     : '/ledgeroperation/load',                            method  : 'POST'                        }),                        root        : 'response',                         fields      :                        [                            {name: 'id',            type: 'int'},                            {name: 'sign',          type: 'int'},                            {name: 'visible',       type: 'int'},                            {name: 'notice',        type: 'string'}                        ],                        sortInfo    : {field: 'notice', direction: 'ASC'},                        remoteSort  : false,                        autoLoad    : true,                        listeners   :                        {                            loadexception :                            {                                fn      : function ()                                {                                    Ext.Msg.error(Ext.Translation.get('common', 'load_data_failure'));                                }                            },                            load :                            {                                fn      : function ()                                {                                    ledgerDataStore.fireEvent('datachanged', ledgerDataStore);                                }                            }                        }                    });                     this.ledgerSignedOperationDataStore = new Ext.data.JsonStore(                    {                        proxy: new Ext.data.HttpProxy(                        {                            url     : '/ledgeroperation/load',                            method  : 'POST'                        }),                        root        : 'response',                         fields      :                        [                            {name: 'id',            type: 'int'},                            {name: 'sign',          type: 'int'},                            {name: 'visible',       type: 'int'},                            {name: 'notice',        type: 'string'}                        ],                        sortInfo    : {field: 'notice', direction: 'ASC'},                        remoteSort  : false,                        autoLoad    : true,                        listeners   :                        {                            loadexception :                            {                                fn      : function ()                                {                                    Ext.Msg.error(Ext.Translation.get('common', 'load_data_failure'));                                }                            },                            load :                            {                                scope   : this,                                fn      : function ()                                {                                    var sign_income = Ext.Translation.get('ledger', 'sign_income');                                    var sign_outgo = Ext.Translation.get('ledger', 'sign_outgo');                                     this.ledgerSignedOperationDataStore.each(function (record)                                    {                                        if (record.get('sign') == 1)                                            record.set('notice', sign_income + ' - ' + record.get('notice'));                                        else                                            record.set('notice', sign_outgo + ' - ' + record.get('notice'));                                    });                                     this.ledgerSignedOperationDataStore.fireEvent('datachanged', this.ledgerSignedOperationDataStore);                                }                            }                        }                    });                     this.totalsDataStore = new Ext.data.JsonStore(                    {                        proxy: new Ext.data.HttpProxy(                        {                            url     : '/ledger/currentCash',                            method  : 'POST'                        }),                        root        : 'response',                         fields      :                        [                            {name: 'id',        type: 'int'},                            {name: 'profit',    type: 'int'},                            {name: 'income',    type: 'int'},                            {name: 'outgo',     type: 'int'}                        ],                        sortInfo    : {field: 'id', direction: 'ASC'},                        remoteSort  : false,                        autoLoad    : true,                        listeners   :                        {                            loadexception :                            {                                fn      : function ()                                {                                    Ext.Msg.error(Ext.Translation.get('common', 'load_data_failure'));                                }                            }                        }                    });                     var totalsColumnModel = new Ext.grid.ColumnModel(                    {                        columns:                        [                            {                                id          : 'profit',                                dataIndex   : 'profit',                                header      : Ext.Translation.get('ledger', 'totals_profit_header_text'),                                renderer    : function (val, meta)                                {                                    if (val.toFixed)                                    {                                        if (val < 0)                                            meta.attr = 'style="color:red;"';                                        return val.toFixed(2);                                    }                                    else                                        return val;                                }                            },                            {                                dataIndex   : 'income',                                header      : Ext.Translation.get('ledger', 'totals_income_header_text'),                                renderer    : function (val)                                {                                    if (val.toFixed)                                        return val.toFixed(2);                                    else                                        return val;                                }                            },                            {                                dataIndex   : 'outgo',                                header      : Ext.Translation.get('ledger', 'totals_outgo_header_text'),                                renderer    : function (val)                                {                                    if (val.toFixed)                                        return val.toFixed(2);                                    else                                        return val;                                }                            }                        ]                    });                    totalsColumnModel.defaultSortable = false;                     var totalsGrid = new Ext.grid.GridPanel(                    {                        store           : this.totalsDataStore,                        colModel        : totalsColumnModel,                        loadMask        : true,                         enableColLock   : true,                        enableColumnHide: false,                        enableHdMenu    : false,                         viewConfig  :                        {                            forceFit    : true                        },                         autoExpandColumn: 'profit'                    });                 /*                **  Column model                **                */                    var expander = new Ext.grid.RowExpander(                    {                        tpl : new Ext.Template(                            '<pre class="expander">{notice}</pre>'                        )                    });                     var cm = new Ext.grid.ColumnModel(                    {                        columns:                        [                            expander,                            {                                dataIndex   : 'time',                                header      : Ext.Translation.get('ledger', 'time_header_text'),                                renderer    : function (val)                                {                                    if (val.format)                                        return val.format('Y-m-d H:i:s');                                    else                                        return val;                                }                            },                            {                                dataIndex   : 'username',                                header      : Ext.Translation.get('ledger', 'username_header_text'),                                renderer    : function (val, meta, rec)                                {                                    return rec.get('fullname') + ' (' + val + ')';                                }                            },                            {                                dataIndex   : 'operation_sign',                                header      : Ext.Translation.get('ledger', 'operation_sign_header_text'),                                renderer    : function (val, meta)                                {                                    if (val > 0)                                    {                                        meta.attr = 'style="color:green"';                                        return Ext.Translation.get('ledger', 'sign_income');                                    }                                    else                                    {                                        meta.attr = 'style="color:red"';                                        return Ext.Translation.get('ledger', 'sign_outgo');                                    }                                }                            },                            {                                dataIndex   : 'operation_id',                                header      : Ext.Translation.get('ledger', 'operation_header_text'),                                id          : 'operation_id',                                store       : this.ledgerOperationDataStore,                                displayField: 'notice',                                valueField  : 'id'                            },                            {                                dataIndex   : 'value',                                header      : Ext.Translation.get('ledger', 'value_header_text'),                                renderer    : function (val)                                {                                    if (val.toFixed)                                        return val.toFixed(2);                                    else                                        return val;                                }                            }                        ]                    });                    cm.defaultSortable = true;                 /*                **  Grid                **                */                    var grid = new Ext.ex.grid.SingleEditGridPanel(                    {                        scope           : this,                        baseUrl         : 'ledger',                         toolButtons     :                            + Ext.ex.grid.SingleEditGridPanel.constant.hasAddButton,                         store           : ledgerDataStore,                        colModel        : cm,                         enableColLock   : false,                         plugins         :                        [                            new Ext.ex.grid.ComboField(),                            expander                        ],                         autoExpandColumn: 'notice',                                                getEditWindowConfig : function ()                        {                            return (                            {                                title : Ext.Translation.get('ledger', 'ledger_properties_window_title')                            });                        },                         getEditFormConfig   : function (record, self)                        {                            return (                            {                                items :                                [                                    {                                        name        : 'operation_id',                                        xtype       : 'combo',                                        fieldLabel  : Ext.Translation.get('ledger', 'operation_header_text'),                                        mode        : 'local',                                        autoWidth   : true,                                        store       : self.ledgerSignedOperationDataStore,                                        editable    : false,                                        allowBlank  : false,                                        triggerAction: 'all',                                        displayField: 'notice',                                        valueField  : 'id',                                        listClass   : 'x-combo-list-small'                                    },                                    {                                        name        : 'notice',                                        xtype       : 'textarea',                                        maxLength   : 500,                                        fieldLabel  : Ext.Translation.get('ledger', 'notice_header_text'),                                        allowBlank  : false                                    },                                    {                                        name        : 'value',                                        vtype       : 'money',                                        minValue    : 0.01,                                        xtype       : 'numberfield',                                        fieldLabel  : Ext.Translation.get('ledger', 'value_header_text'),                                        allowBlank  : false                                    }                                ]                            });                        }                    });                     this.winConfig =                    {                        items   :                        {                            xtype   : 'panel',                            layout  : 'border',                            items   :                            [                                {                                    xtype   : 'panel',                                    layout  : 'fit',                                    items   : grid,                                    region  : 'center'                                },                                {                                    xtype   : 'panel',                                    layout  : 'fit',                                    items   : totalsGrid,                                    height  : 70,                                    region  : 'south'                                }                            ]                        },                        title   : Ext.Translation.get('ledger', 'title')                    };                }            });        }    })})(); [/js]

While most of the back office modules are grid based, front office modules are much more complex:

[js]...                    this.tabPanel = new Ext.TabPanel(                    {                        resizeTabs      : true,                        minTabWidth     : 115,                        tabWidth        : 135,                        autoWidth       : true,                        enableTabScroll : true,                        activeTab       : 0,                        defaults        :                        {                            autoScroll  : true,                            labelWidth  : 120                        },                        bbar            :                        [                            this.resetButton                        ],                        items           :                        [                            this.personalDataForm,                            this.internetDataForm                        ]                    });                     this.searchPanel    = new Ext.Panel(                    {                        region      : 'west',                        width       : 320,                        layout      : 'fit',                        items       : this.tabPanel                    });                     this.resultsPanel   = new Ext.Panel(                    {                        region      : 'center',                        width       : 400,                        items       : this.userGrid                    });                     this.container = new Ext.Panel(                    {                        layout  : 'border',                        frame   : true,                        defaults        :                        {                            layout  : 'fit'                        },                        items   :                        [                            this.searchPanel,                            this.resultsPanel                        ]                    });                     this.winConfig =                    {                        items   : this.container,                        title   : Ext.Translation.get('payment', 'title')                    };                }[/js]
It's just the visual layout ...
Last edited by VladSun on Fri Dec 17, 2010 4:19 am, edited 1 time in total.
There are 10 types of people in this world, those who understand binary and those who don't
alex.barylski
DevNet Evangelist
Posts: 6267
Joined: Tue Dec 21, 2004 5:00 pm
Location: Winnipeg

Re: Ajax design patterns

Post by alex.barylski »

Hm ... yes!
But it's more like a desktop application rather than a collection of web pages ...
A typical web application of mine looks like this:
http://www.extjs.com/deploy/dev/example ... sktop.html
I`m right in the process of looking for an interesting web desktop interface for my own framework...that was so slick...I started crying :P

Thanks man :)
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: Ajax design patterns

Post by Christopher »

Vlad, you showed a lot of code that seems pretty Ext-centric. Is this stuff that could be done with any Javascript library? Or does Ext provide the functionality that makes what you are doing easy?
(#10850)
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: Ajax design patterns

Post by VladSun »

Yes, most of it is ExtJS specific ...

I do love ExtJS ;) and I always try to show its power to other developers - I think you've noticed it ;)
http://www.extjs.com/deploy/dev/docs/
There are 10 types of people in this world, those who understand binary and those who don't
User avatar
Eran
DevNet Master
Posts: 3549
Joined: Fri Jan 18, 2008 12:36 am
Location: Israel, ME

Re: Ajax design patterns

Post by Eran »

In my opinion, developing all the views in Javascript is very unportable. Often, I need to create low-FI versions of my web apps, for mobile consumption or other edge cases, in which case I'd need it to run similarly but without the Javascript. I use Javascript to create an enhanced interface for those with modern browsers, but keep the option to reuse view elements in other usecases.
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: Ajax design patterns

Post by VladSun »

PCSpectra wrote:
My web-apps are fully AJAX featured. That is, except for the main page HTML View I have no more HTML code. Indeed, I don't have any server side Views - everything is sent to the user as JSON data (if you consider an JSON formatter helper a View ... I have one View ). That is, the View part is always client side - no templates, no code duplication (except for the validation, which I think can't be done in other way).
So what happens when someone visits your site on a mobile device like a blackberry, which might not have JS enabled by default? If templates rendered via client side (using XSLT, or whatever) chances are a mobile will not render anything but simple HTML.

No to mention SEF/SEO. Unless your pages are purely XML data structures which are rendered into HTML (but I recall JSON being the term) how does that affect SE's???
Sorry, missed that one.

I develop desktop web applications which are run in an Intranet environment, strictly specified browser (usually Prism), etc.
So it's easy for me - no SEO, no cross-browser issues ;) Even almost no screen resolution issues ;)

Also, I've really never cared about mobile devices - people using such devices 1) are too rare in our country and 2) are simply not in our target group. And SEO may be achieved in several ways -e.g. by using the Google XML sitemap tool. Also, I do think search engines should follow the Internet evolution, not the opposite - I mean SE should use RIA-enabled spiders ;)
There are 10 types of people in this world, those who understand binary and those who don't
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: Ajax design patterns

Post by VladSun »

Back to the topic ;)
VladSun wrote:Just today, I've read a short article about MVC and AJAX - http://wiki.apidesign.org/wiki/MVC
To MVP Again
Obviously, people tend to repeat the same thing again and again, just with new technologies. Thus with the rise of Ajax, where the GUI is represented inside a browser we are again closer to MVP as the user deals with the view elements directly.
So, we have MVP here ;)
Any comments on this one?
There are 10 types of people in this world, those who understand binary and those who don't
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Re: Ajax design patterns

Post by matthijs »

Haven't followed the details exactly, but I do think that if you throw out URLs completely by using a full ajax approach for web apps, you loose a lot. Easy to remember or predictable urls, back-button functionality, good inexibility by search engines, etc
User avatar
Eran
DevNet Master
Posts: 3549
Joined: Fri Jan 18, 2008 12:36 am
Location: Israel, ME

Re: Ajax design patterns

Post by Eran »

I would say Vlad's case is not applicable to most of us since he develops desktop applications which run in a browser. They work over an intranet and specific browser only. Most of us develop web applications / websites that are served to the mass population and have to worry about cross-browser support, mobile support, various resolutions etc.
alex.barylski
DevNet Evangelist
Posts: 6267
Joined: Tue Dec 21, 2004 5:00 pm
Location: Winnipeg

Re: Ajax design patterns

Post by alex.barylski »

I wanna figure out a way so my users can switch between an AJAX interface like those supplied by that library and one which is statically displayed, such as HTML.

Have you looked at some of those screenies? Oh man...thats awesome :)
Post Reply