Sistema Demo utilizando ExtJs 4.1 - Parte 01

by Hirashiki 15. May 2012 00:08

Fala Pessoal....

Faz um tempo que escrevi alguma coisa (decente) no site....
Depois que publiquei o artigo Sistema de Login em Extjs 4 - MVC comecei a receber um monte de e-mail de elogios, dúvidas, questionamentos.... Se todas estas pessoas se deram ao trabalho de mandar uma mensagem para mim, quer dizer que de alguma forma meu post foi útil....

Bom... 90% dos e-mails que eu recebo vem com uma simples pergunta: "Como é que eu implemento sua solução utilizando xxxx? (PHP, java, asp, etc..)"

Percebi que apesar do Sencha se tornar mais popular, as pessoas ainda tem as mesmas dúvidas que eu tive quando iniciei com o framework: Nós vemos os exemplos de forms, grids, painéis e um monte de funcionalidades, mas não conseguimos "unir" toda essa informação para formar uma coisa mais concreta.... um Sistema..!!!!

Pensando nisso, vou tentar criar uma série de posts... e explicar como organizar "todos estes exemplos" para formar um sistema... Isto será uma espécie de laboratório: Não tenho nada pronto com antecedência e estou fazendo a aplicação aos poucos, exatamente como está descrito aqui... não sei se vai dar certo, mas vamos à luta!!

Nota:

  • Fica por sua conta e risco colocar o código aqui em um sistema que vai para produção. O conteúdo apresentado, apesar de ter a idéia de um sistema final, tem muito a ser melhorado.
  • Eu queria fazer uma apresentação do novo tema Neptune, mas como a versão 4.1 do ExtJs está com alguns bugs para o firefox tive que disponibilizar também nos temas "gray" e "access".

Ferramentas

ExtJs 4.1

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

Até o momento que eu estou escrevendo (maio/2012) a última versão do ExtJs é a 4.1;

Sencha SDK Tools beta

http://www.sencha.com/products/sdk-tools/

Ferramenta para "deploy" da aplicação;

Servidor WAMP

Aqui fica a critério da pessoa: WAMP, XAMPP, etc... A idéia é você ter um lugar para rodar o PHP e ter um banco de dados Mysql.
Particularmente eu utilizo um outro servidor para PHP: o USBWebServer (http://www.usbwebserver.net/en/download.php). Para mim este é o mais prático: não preciso instalar e posso rodar direto de um pen drive! Muito útil quando se quer fazer alguma apresentação em algum computador que não tem nada instalado.

IDE para programação do Javascript/HTML

Aqui também vai de acordo com o gosto. Algumas vezes prefiro programar no Notepad... *r*. Mas nas últimas vezes eu usei o Aptana (http://www.aptana.com/).

"GoGoGo" por a mão na massa?

Para aqueles mais sendentos por utilizar todas as ferramentas acima, vamos com calma......

Vamos começar bem aos poucos. Um dos erros de quem quer aprender ExtJs é querer baixar os arquivos, já colocar em um servidor e sair programando que nem uma "vaca louca"... Aí começa a dar pau... não gera a tela... o banco não conecta... dá erro de compilação...... Se a pessoa não resolve mandar tudo pro alto e desistir vai perder um bom tempo tentando resolver....

Para evitar isso, primeiro vamos começar com uma "casca", ou seja, o mínimo de uma aplicação para servir de base. Esqueça por enquanto a tela de login, aquele cadastro que você deseja fazer , aquele download do relatório em PDF....

A partir desta "casca" iremos evoluindo até chegar a alguma coisa parecida com um sistema. Após os devidos avisos.. :) vamos à prática!!!

Instalação

O meu servidor PHP não presa instalar... é só baixar do site e executar:

 

Depois disso é só acessar http://localhost:8080/

Pois é.. só isso!!!

Agora vamos colocar a pasta do ExtJs na pasta Root:


E agora, se você acessar http://localhost:8080/extjs-4.1.0/ terá:

Até agora nenhuma novidade.... Depois de instalar o Extjs no servidor, eu quero que você dê olhada no exemplo "Kitchensink" (http://localhost:8080/extjs-4.1.0/examples/kitchensink/). Este é um novo tema disponível para o Extjs.

Criando nossa "casca"

O "kithensink" será o modelo de nossa "casca". Para isto, crie um novo diretório no diretório root com a seguinte estrutura:

A pasta resources é uma cópia do exemplo kitchensink.

vou listar somente alguns arquivos aqui. Você poderá encontrar a solução completa mais abaixo no link para download:

App.js

 

/**
 *
 * Sistema Demo utilizando ExtJS
 * Desenvolvido por Ricardo Hirashiki
 * Publicado em: http://www.sitedoricardo.com.br
 * Data: Mai/2012
 *
 * Baseado no exemplo disponível no framework do sencha 4.1.0 "kitchensink"
 * http://http://dev.sencha.com/deploy/ext-4.1.0-gpl/examples/kitchensink/index.html
 *
 */
Ext.Loader.setConfig({
  enabled : true,
  paths   : {SistemaDemo:'app'}
});
var aplicacao=Ext.application({
    name: 'SistemaDemo',

    autoCreateViewport: true,

    controllers: [
        'Main'
    ]
});

 

Controller - BaseController.js

 

/**
 *
 * Sistema Demo utilizando ExtJS
 * Desenvolvido por Ricardo Hirashiki
 * Publicado em: http://www.sitedoricardo.com.br
 * Data: Mai/2012
 *
 * Baseado no exemplo disponível no framework do sencha 4.1.0 "kitchensink"
 * http://http://dev.sencha.com/deploy/ext-4.1.0-gpl/examples/kitchensink/index.html
 *
 */
Ext.define('SistemaDemo.controller.base.BaseController', {
    extend: 'Ext.app.Controller',
    firstTime:true,
	init:function(){
			console.log('init Base ' + this.getFirstTime());
	},
	getFirstTime: function(){
		return this.firstTime;
	},
	setFirstTime:function(value){
		this.firstTime=value;
	}
});

 

Controller - Main.js

 

/**
 *
 * Sistema Demo utilizando ExtJS
 * Desenvolvido por Ricardo Hirashiki
 * Publicado em: http://www.sitedoricardo.com.br
 * Data: Mai/2012
 *
 * Baseado no exemplo disponível no framework do sencha 4.1.0 "kitchensink"
 * http://http://dev.sencha.com/deploy/ext-4.1.0-gpl/examples/kitchensink/index.html
 *
 */
Ext.define('SistemaDemo.controller.Main', {
    extend: 'SistemaDemo.controller.base.BaseController',
    requires:[
    	'Ext.window.MessageBox',
    	'Ext.tip.*'
	],
    stores: [
        'Menus'//,
        //'Companies',
        //'States'
    ],
    views: [
        'Viewport',
        'layout.Header'
    ],
    refs: [
        {
            ref: 'mainPanel',
            selector: '#mainPanel'
        },
        {
            ref: 'menuList',
            selector: 'menuList'
        }
    ],

    init: function() {
    	console.log('init main ' +this.getFirstTime());
    	if(!this.getFirstTime()){
    		return;
    	}
        this.control({
            'viewport menuList': {
                'select': function(me, record, item, index, e) {
                    if (!record.isLeaf()) {
                        return;
                    }
                    this.setActiveExample(this.classNameFromRecord(record), record.get('text'));
                },
                afterrender: function(){
                    var me = this,
                        className, menuList, name, record;

                    setTimeout(function(){
                        className = location.hash.substring(1);
                        menuList = me.getMenuList();

                        if (className) {
                            name = className.replace('-', ' ');
                            record = menuList.view.store.find('text', name);     
                        } else {
							record = menuList.view.store.find('text', 'Home');
						}
                        menuList.view.select(record);
                    }, 0);
                }
            }
        });
		this.setFirstTime(false);
    },

    setActiveExample: function(className, title) {
    	var me = this;
        var mainPanel = this.getMainPanel(),
            path, mypanel, className;
        
        if (!title) {
            title = className.split('.').reverse()[0];
        }
        
        //remember the className so we can load up this example next time
        location.hash = title.toLowerCase().replace(' ', '-');

        //set the browser window title
        document.title = document.title.split(' - ')[0] + ' - ' + title;
        
        //Implementação 1 - Segurança
        if (className=='SistemaDemo.view.menuPrincipal.GroupedHeaderGridBloqueado'){
        	mypanel = Ext.create('SistemaDemo.view.base.GenericPanel');
        	mypanel.down('panel').setTitle('Permissão Negada: ' + title);
        	mypanel.down('panel').update('<p>Você não tem permissão para acessar esta função.</p>');
        		
        }else{
			this.searchForRequiredControllers(className);
        	mypanel = Ext.create(className);
        }
        
        //remove all items from the example panel and add new example

        mainPanel.removeAll();
        mainPanel.add(mypanel);
    },
    

    filePathFromRecord: function(record) {
        var parentNode = record.parentNode,
            path = record.get('text');
        while (parentNode && parentNode.get('text') != "Root") {
            path = parentNode.get('text') + '/' + Ext.String.capitalize(path);

            parentNode = parentNode.parentNode;
        }

        return this.formatPath(path);
    },

    classNameFromRecord: function(record) {
        var path = this.filePathFromRecord(record);

        path = 'SistemaDemo.view.' + path.replace('/', '.');
        return path;
    },

    formatPath: function(string) {
        var result = string.split(' ')[0].charAt(0).toLowerCase() + string.split(' ')[0].substr(1),
            paths = string.split(' '),
            ln = paths.length,
            i;

        for (i = 1; i < ln; i++) {
            result = result + Ext.String.capitalize(paths[i]);
        }

        return result;
    },
    
    searchForRequiredControllers: function (className){
    	var me = this;
		//Verifico se a classe foi Criada
		if (!Ext.ClassManager.isCreated(className)){
			//Crio a Classe
			Ext.syncRequire(className);
		}
		//Verifico a propriedade requiredcontrollers para verificar se há controllers ligados à esta view
		var requiredcontrollers = Ext.ClassManager.get(className).prototype.requiredcontrollers;
		console.log(requiredcontrollers);
		if(requiredcontrollers){
		       requiredcontrollers.forEach(function(element){
				//var requiredClassName = Ext.ClassManager.getName(element);
				var requiredClassName = element;
				//Verifica se a classe controller pertence à Aplicação
				var ret = requiredClassName.search('SistemaDemo.controller.');
				if (ret==0){
					console.log('esta e uma classe controller:' + requiredClassName);
					//Crio dinamicamente o controller
					var tmpController = me.getController(requiredClassName);
					//Se for a primeira execução, chamo o método init
					if (tmpController.getFirstTime()){
						tmpController.init();
					}
	
				}
			});
		}
    }
});

 

Controller - Register.js

 

/**
 *
 * Sistema Demo utilizando ExtJS
 * Desenvolvido por Ricardo Hirashiki
 * Publicado em: http://www.sitedoricardo.com.br
 * Data: Mai/2012
 *
 * Baseado no exemplo disponível no framework do sencha 4.1.0 "kitchensink"
 * http://http://dev.sencha.com/deploy/ext-4.1.0-gpl/examples/kitchensink/index.html
 *
 */
Ext.define('SistemaDemo.controller.Register', {
    extend: 'SistemaDemo.controller.base.BaseController',
    views:['form.Register'],
    init: function() {
    	console.log('init Register ' +this.getFirstTime());
    	if(!this.getFirstTime()){
    		return;
    	}
        this.control({
            'formregister button[action=register]': {
				click: this.onRegisterClick
            }
        });
        this.setFirstTime(false);
    },
	onRegisterClick:function(button){
		console.log('botao Register do form Register clicado');
	}
});

 

Store - Menus.js

 

/**
 *
 * Sistema Demo utilizando ExtJS
 * Desenvolvido por Ricardo Hirashiki
 * Publicado em: http://www.sitedoricardo.com.br
 * Data: Mai/2012
 *
 * Baseado no exemplo disponível no framework do sencha 4.1.0 "kitchensink"
 * http://http://dev.sencha.com/deploy/ext-4.1.0-gpl/examples/kitchensink/index.html
 *
 */
Ext.define('SistemaDemo.store.Menus', {
    extend: 'Ext.data.TreeStore',

    root: {
        expanded: true,
        children: [
            {
                text: 'Menu Principal',
                expanded: true,
                children: [
                    { leaf: true, text: 'Home'             },
                    { leaf: true, text: 'GroupedHeaderGrid' },
                    { leaf: true, text: 'GroupedHeaderGridBloqueado' }
                ]
            },
            {
                text: 'form',
                expanded: true,
                children: [
                    { leaf: true, text: 'Login'             },
                    { leaf: true, text: 'Contact' },
                    { leaf: true, text: 'Register' }
                ]
            }
        ]
    }
});

 

View - Register.js

 

/**
 *
 * Sistema Demo utilizando ExtJS
 * Desenvolvido por Ricardo Hirashiki
 * Publicado em: http://www.sitedoricardo.com.br
 * Data: Mai/2012
 *
 * Baseado no exemplo disponível no framework do sencha 4.1.0 "kitchensink"
 * http://http://dev.sencha.com/deploy/ext-4.1.0-gpl/examples/kitchensink/index.html
 *
 */
Ext.define('SistemaDemo.view.form.Register', {
    extend: 'SistemaDemo.view.base.BaseContainer',
    requiredcontrollers:['SistemaDemo.controller.Register'],
    requires: [
        'Ext.form.FieldSet',
        'Ext.form.Panel',
        'Ext.form.field.ComboBox',
        'Ext.form.field.Date',
        'Ext.form.field.Text',
        'SistemaDemo.store.States'
    ],
	alias:'widget.formregister',
    items: [{
        xtype: 'form',

        frame: true,
        title: 'Register',
        bodyPadding: 13,
        autoScroll:true,

        fieldDefaults: {
            labelAlign: 'right',
            labelWidth: 115,
            msgTarget: 'side'
        },

        items: [{
            xtype: 'fieldset',
            title: 'User Info',
            defaultType: 'textfield',
            defaults: {
                width: 300
            },
            items: [
                { allowBlank:false, fieldLabel: 'User ID', name: 'user', emptyText: 'user id' },
                { allowBlank:false, fieldLabel: 'Password', name: 'pass', emptyText: 'password', inputType: 'password' },
                { allowBlank:false, fieldLabel: 'Verify', name: 'pass', emptyText: 'password', inputType: 'password' }
            ]
        },
        {
            xtype: 'fieldset',
            title: 'Contact Information',
            defaultType: 'textfield',
            defaults: {
                width: 300
            },
            items: [{
                fieldLabel: 'First Name',
                emptyText: 'First Name',
                name: 'first'
            },
            {
                fieldLabel: 'Last Name',
                emptyText: 'Last Name',
                name: 'last'
            },
            {
                fieldLabel: 'Company',
                name: 'company'
            },
            {
                fieldLabel: 'Email',
                name: 'email',
                vtype: 'email'
            },
            {
                xtype: 'combobox',
                fieldLabel: 'State',
                name: 'state',
                store: Ext.create('SistemaDemo.store.States'),
                valueField: 'abbr',
                displayField: 'state',
                typeAhead: true,
                queryMode: 'local',
                emptyText: 'Select a state...'
            },
            {
                xtype: 'datefield',
                fieldLabel: 'Date of Birth',
                name: 'dob',
                allowBlank: false,
                maxValue: new Date()
            }]
        }],

        buttons: [{
            text: 'Register',
            disabled: true,
            formBind: true,
            action:'register'
        }]

    }]
});

 

View - Home.js

 

/**
 *
 * Sistema Demo utilizando ExtJS
 * Desenvolvido por Ricardo Hirashiki
 * Publicado em: http://www.sitedoricardo.com.br
 * Data: Mai/2012
 *
 * Baseado no exemplo disponível no framework do sencha 4.1.0 "kitchensink"
 * http://http://dev.sencha.com/deploy/ext-4.1.0-gpl/examples/kitchensink/index.html
 *
 */
Ext.define('SistemaDemo.view.menuPrincipal.Home', {
	extend: 'SistemaDemo.view.base.BaseContainer',
	items: [
		{
			xtype: 'panel',
			title:'teste panel',
			frame:true,
			layout: {
				type: 'hbox',
				align: 'center',
				pack: 'center'
			},
			defaults: {
				width: 200,
				height: 295
			},
			items: [
				{
					xtype: 'panel',
					html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed egestas gravida nibh, quis porttitor felis venenatis id. Nam sodales mollis quam eget venenatis. Aliquam metus lorem, tincidunt ut egestas imperdiet, convallis         lacinia tortor.'
				},
				{
					xtype: 'panel',
					title: 'Title',
					html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed egestas gravida nibh, quis porttitor felis venenatis id. Nam sodales mollis quam eget venenatis. Aliquam metus lorem, tincidunt ut egestas imperdiet, convallis         lacinia tortor.'
				},
				{
					xtype: 'panel',
					title: 'Header Icons',
					tools: [
						{type:'pin'},
						{type:'refresh'},
						{type:'search'},
						{type:'save'}
					],
					html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed egestas gravida nibh, quis porttitor felis venenatis id. Nam sodales mollis quam eget venenatis. Aliquam metus lorem, tincidunt ut egestas imperdiet, convallis         lacinia tortor.'
				},{
					xtype: 'panel',
					title: 'Collapsed Panel',
					collapsed: true,
					collapsible: true,
					//width: 640,
					bodyPadding: 10,
					html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed egestas gravida nibh, quis porttitor felis venenatis id. Nam sodales mollis quam eget venenatis. Aliquam metus lorem, tincidunt ut egestas imperdiet, convallis lacinia tortor.'
				}
			]
		}
	]
});

 

Viewport.js

 

/**
 *
 * Sistema Demo utilizando ExtJS
 * Desenvolvido por Ricardo Hirashiki
 * Publicado em: http://www.sitedoricardo.com.br
 * Data: Mai/2012
 *
 * Baseado no exemplo disponível no framework do sencha 4.1.0 "kitchensink"
 * http://http://dev.sencha.com/deploy/ext-4.1.0-gpl/examples/kitchensink/index.html
 *
 */
Ext.define('SistemaDemo.view.Viewport', {
	extend: 'Ext.container.Viewport',
	requires: [
		'Ext.layout.container.Border',
		'Ext.layout.container.HBox',
		'SistemaDemo.view.layout.List'
	],
	layout: 'border',
	items: [
    	{
			region: 'north',
			xtype : 'pageHeader'
		},
		{
			region: 'center',
			layout: {
				type : 'hbox',
				align: 'stretch'
			},
			items: [
				{
					width: 250,
					bodyPadding: 5,
					xtype: 'menuList'
				},
				{
					cls: 'x-example-panel',
					flex: 1,
					id   : 'mainPanel',
					layout: {
						type: 'fit',
						align: 'center',
						pack: 'center'
					},
					overflowY: 'auto',
            		bodyPadding: 0
				}
    		]
		},
		{
			xtype: 'pageHeader',
			region: 'south',
            height: 13
        }
    ]
});

 

Funcionamento

A applicação será iniciada através do arquivo app.js, onde será automaticamente carregado o Controller Main.js, além de criar a viewport (viewport.js).

A viewport será criada com uma região 'north' (onde contém o título da aplicação), 'west' (onde contém uma treeview que será nosso menu) e a região 'center' (onde será o painel principal).

Quando o controller Main.js é inicializado, ele cria 2  eventos para o viewport:

  • A seleção de um item da treeview;
  • um evento 'afterrender' da própria viewport.

Este exemplo funciona porque existe uma relação do título da aplicação com o nome da classe e com o conteúdo da treeview.
Após clicar em um item da treeview, a informação do record é enviada para a função "classNameFromRecord". Esta função combina as informações do item clicado para formar o nome da classe view a ser criada.

Logo em seguida, a função "setActiveExample" é executada. Esta é a função responsável por criar uma instância da classe que a função "classNameFromRecord" retornou. Em seguida esta instância criada é adicionada à nosso viewport e assim é exibida na tela.

Ao mesmo tempo que exibe na tela a classe criada, a url é alterada, concatenando no final o título da funcionalidade. Esta ação, junto com o evento 'afterrender' forma uma espécie de 'dispatcher' (ou 'Router') simplificado: quando você entra com a url http://localhost:8080/ExtJS_Sistema_Demo/#register o evento 'afterrender' 'descobre' que você está acessando o sistema e quer que ele vá direto para a view 'register'. Se você acessar outro menu, a url será alterada para ser acessada do mesmo modo!

Parece meio complicado, mas resumi umas 100 linhas de código em 2 parágrafos... :) Se você ver a aplicação rodando, fica mais fácil de entender!

Eu coloquei 2 coisas a mais neste controller em relação ao exemplo do kitchensink. Uma delas é um controle simples que servirá como um módulo de segurança: caso eu não tenha permissão de acessar uma view, será criada um painel padrão de permissão negada. Veja o exemplo clicando em 'GroupedHeaderGridBloqueado'. Por enquanto está fixo, mas a idéia é que seja dinâmico:

 

if (className=='SistemaDemo.view.menuPrincipal.GroupedHeaderGridBloqueado'){
            mypanel = Ext.create('SistemaDemo.view.base.BasePanel');
            mypanel.down('panel').setTitle('Permissão Negada: ' + title);
            mypanel.down('panel').update('<p>Você não tem permissão para acessar esta função.</p>');
               
        }else{
            this.searchForRequiredControllers(className);
            mypanel = Ext.create(className);
        }

 

A outra implementação é um pouco mais complicada.....

Inversão de Controle, Injeção de Dependência ou algo do tipo....

Uma coisa que eu acho "um porre" no ExtJs é eu ter que sempre referenciar o meu controller na minha aplicação para ser carregada. Na minha aplicação eu carrego um controller, que contém  um monte de view e store que é carregado por consequencia:

Se for pensar em uma aplicação em que cada controller será uma espécie de "plug-in" que complementará o sistema, eu não consigo ter a visão de qual controller eu devo carregar. Existem algumas soluções para isto (como o exemplo do ext.ux.router que eu utilizei no outro artigo). Porém dando uma olhada no exemplo do kithensink, descobri sem querer uma alternativa...

Até onde consegui depurar do framework, o Ext.Create e o Ext.Widget usam o mesmo método para criar um objeto (Ext.ClassManagerView.instantiate). A única diferença é que o Ext.Widget utiliza a propriedade 'alias', para achar o nome da classe e utilizar o "Ext.ClassManagerView.instantiate".

Eu já passo por parâmetro o nome da classe no método "setActiveExample" do controller "Main.js". Então, independente de onde estiver a view (se estiver na memória ou não) o Ext irá fazer o 'loader' da classe e instanciá-la!!!

Se é possível fazer isso, então eu consigo fazer uma "referência ao contrário" em relação ao controller e a view: Em vez de eu dizer para um controller quais são as views que ele deve carregar, eu informo para a view qual é o controller relacionado à ela...

Depois de quebrar um pouco a cabeça eu criei o método "searchForRequiredControllers". Após ocorrer o 'loader' da view, este método irá procurar se esta view tem algum controller relacionado. Se houver, ele irá carregar através do método 'getController' e se necessário chamar o método init().

Eu fiz alguns testes aqui e aparentemente não encontrei nenhum bug com esta implementação. Se descobrirem alguma coisa, me avisem!!

Por último, coloquei uma lógica simples para executar somente uma vez o método init().

Acho que já está complexo demais para um primeiro post!!

Com isto, já tenho o mínimo de uma aplicação. A partir deste ponto é só complementar com as telas do nosso sistema.

Bom estudo!!

Download

Disponibilizei o código no GitHub (https://github.com/Hirashiki/ExtJS_Sistema_Demo_pt1). É a primeira vez que eu faço isso, portanto, existe a grande probabilidade de eu ter feito alguma coisa errada. Qualquer problema me mandem uma mensagem que eu tento arrumar...

Referências

http://www.sencha.com/

Diagramas criados pelo Bizagi (https://www.bizagi.com/)

Tags:

ExtJS | Programação

Anunciante

Calendário

<<  May 2012  >>
MoTuWeThFrSaSu
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

View posts in large calendar

RecentPosts