Sistema com Login em Extjs 4 - MVC

by Hirashiki 29. August 2011 19:25

Acho que todos que usam a nova versão do ExtJs estão atrás de uma solução para implementar um sistema de login utilizando o MVC.

A maioria das soluções que eu vi não me agradaram muito. Apesar de serer totalmente funcionais, sempre em algum momento era necessário um "refresh" na página toda ou um redirecionamento para uma nova página.

Pensando neste assunto, e baseado em algumas informações dos tópicos Interface de Login no ExtJs4 e Chamar função de outro controlador criei um esquema para realizar o controle de sessão de uma aplicação e o controle das views sem refresh na página.

Este é um post onde é necessário um certo conhecimento sobre a nova estrutura do ExtJs versão 4 e sua arquitetura MVC. Para quem não

conhece ou deseja se aprofundar antes, recomendo ler MVC Architecture e Construindo um aplicativo com Ext 4 – Parte 1.

Então vamos lá!

Estrutura de Diretórios e arquivos

Esta é uma estrutura básica dos projetos que eu utilizo. Você pode pegar toda essa pasta e jogar em uma "htdocs" da vida que irá funcionar normalmente.

Criando a aplicação (app.js)

O arquivo app.js será a classe de inicialização do sistema:

 

/**
 *
 * Modelo de Login usando MCV
 * Desenvolvido por Ricardo Hirashiki
 * Publicado em: http://www.sitedoricardo.com.br
 * Data: Ago/2011
 *
 */
 
Ext.Loader.setConfig({
  enabled : true,
  paths   : {SampleApp:'app'}
});

        
Ext.application({
  name               : 'SampleApp',
  autoCreateViewport : true,
  paths              :    {'Ext.ux': 'app/ux'},
  requires           : ['Ext.ux.Initialization'],
  appFolder          : 'app',
  controllers        : ['Viewport'],
  enableRouter       : true,
  routes: {
    '/'           : 'viewport#index'        ,
    'login'       : 'authentication#index',
    'home'        : 'home#index'
  },
  launch: function() {
    var hideMask = function () {
      Ext.get('loading').remove();
      Ext.fly('loading-mask').animate({
        opacity : 0,
        remove  : true
      });
    };
    Ext.defer(hideMask, 250);
  }
});

 

 

Não existe a criação de nenhuma viewport, pois há a propriedade autoCreateViewport na linha 18 como true. Com esta propriedade, o ExtJs irá carregar o arquivo em view/Viewport.js.

Na linha 19 é mapeada a pasta para os plugins "Ext.ux" através da propriedade paths. Através do método requires eu informo a primeira classe dependente para este projeto: Ext.ux.Initialization declarada no arquivo Initialization.js (o Código se encontra mais abaixo do post).

Nas linhas 23 a 29 eu configuro os parâmetros utilizados pela classe Ext.ux.Router (Router.js). Definindo 3 "roteamentos":

  1. Ao acessar a "raiz" da aplicação ("/"), executo o método index do controller viewport;
  2. Ao acessar o alias "login", executo o método index do controller authentication;
  3. Ao acessar o alias "home", executo o método index do controller home.


Fora isso, eu crio uma função para esconder algumas tags DIV utilizadas no carregamento da aplicação (linha 31 a 38).

View login (Login.js e CapsWarningTooltip.js)

As views para a tela de login são 2: uma para a construção da janela (Login.js) e outra para uma para uma tooltip de alerta quando o Caps Lock estiver ativado (CapsWarningTooltip.js).

Login.js

 

/**
 *
 * Modelo de Login usando MCV
 * Desenvolvido por Ricardo Hirashiki
 * Publicado em: http://www.sitedoricardo.com.br
 * Data: Ago/2011
 *
 * Baseado na extensao criada por Wemerson Januario
 * http://code.google.com/p/login-window/
 *
 */
 
Ext.define('SampleApp.view.authentication.Login', {
  extend      : 'Ext.window.Window',
  alias       : 'widget.authenticationlogin',
  layout      : 'fit',
  bodyStyle   : 'padding:10px;',
  title       : 'Autenticação',
  id          : 'authentication-login',
  autoShow    : true,
  labelAlign  : 'left',
  closable    : false,
  draggable   : false,
  constrain   : true,
  resizable   : false,

  initComponent: function() {
    this.items = [
      {
        xtype          : 'form',
        baseCls        : 'x-plain',
        border         : false,
        bodyStyle      : "padding: 10px;",
        waitMsgTarget  : true,
        labelAlign     : "left",
        items: [
          {
            xtype            : 'textfield',
            name             : 'l',
            id               : 'l',
            fieldLabel       : 'Usuário',
            allowBlank       : false,
            blankText        : 'Usuário Obrigatório',
            msgTarget        : 'side',
            selectOnFocus    : true,
            enableKeyEvents  : true
          },{
            xtype            : 'textfield',
            inputType        : 'password', 
            name             : 's',
            id               : 's',
            fieldLabel       : 'Senha',
            allowBlank       : false,
            blankText        : 'Senha Obrigatória',
            msgTarget        : 'side',
            selectOnFocus    : true,
            enableKeyEvents  : true
          }
        ]
      }
    ];
    this.buttons = [
      {
        xtype  : 'label',
        style  : {color:'#ff0000'} ,
        id     : 'msgField',
        width  :200
      },{
        text: '<b>Entrar</b>',
        action: 'trylogin'
      }
    ];
    this.callParent(arguments);
  }
});

 

CapsWarningTooltip.js

 

/**
 *
 * Modelo de Login usando MCV
 * Desenvolvido por Ricardo Hirashiki
 * Publicado em: http://www.sitedoricardo.com.br
 * Data: Ago/2011
 *
 * Baseado na extensao criada por Wemerson Januario
 * http://code.google.com/p/login-window/
 *
 */
 
Ext.define('SampleApp.view.authentication.CapsWarningTooltip', {
  extend       : 'Ext.tip.QuickTip',
  alias        : 'widget.capswarningtooltip',
  target       : 'authentication-login',
  id           : 'toolcaps',
  anchor       : 'left',
  anchorOffset : 60,
  width        : 305,
  dismissDelay : 0,
  autoHide     : false,
  disabled     : false,
  title        : '<b>Caps Lock está ativada</b>',
  html         : '<div>Se Caps lock estiver ativado, isso pode fazer com que você</div>' +
                 '<div>digite a senha incorretamente.</div><br/>' +
                 '<div>Você deve pressionar a tecla Caps lock para desativá-la</div>' +
                 '<div>antes de digitar a senha.</div>' 
});

 

View Home (Home.js)

A view Home é somente um painel com um título. A idéia é que este seja o painel de entrada da aplicação.

 

Ext.define('SampleApp.view.home.Home', {
  extend        : 'Ext.panel.Panel',
  alias         : 'widget.homehome',
  deferredRender: false,
  layout:'fit',
  title:'painel Home'
});

 

 

View Layout (Appview.js)

Aqui está o segredo para o sistema de autenticação funcionar. Na documentação do ExtJs, uma viewport é:

  • Rendenizada diretamente no corpo do HTML (document.body());
  • Automaticamente assume o tamanho do browser e acompanha o redimensionamento da janela
  • Única para a página.

Por ser um viewport, muita gente tenta colocar o seu layout como 'border' e aplicar as regiões de acordo com o layout do seu sistema (region:'west' para menu lateral, region:'north' para os logos e menus, etc...). Ocorre que uma viewport uma vez rendenizada não pode ser alterada, ou seja, se você rendenizou com uma região central e uma lateral, você não pode adicionar dinamicamente um painel para o topo e outro para a parte de baixo, por exemplo.

Mesmo se você chamar o método Close() e/ou destruir uma Viewport, não é possivel criar uma nova.

Para contornar este problema, é necessário criar um painel com layout:'border' (Appview.js) para "simular" uma viewport. Deste modo, eu posso criar e destruir a nossa viewport "simulada" dentro da viewport verdadeira:

 

Ext.define('SampleApp.view.layout.Appview', {
  extend        : 'Ext.panel.Panel',
  alias         : 'widget.layoutappview',
  layout : 'border',
  id : 'layoutappview',
  items:[
  / {xtype:'layoutheader'},
    {xtype:'layoutfooter'},
    {xtype:'layoutleft'},
    {xtype:'layoutright'},
    {xtype:'layoutmiddle'}
  ]
      
});

 

No exemplo, cada elemento do Appview.js é um painel com a propriedade region. O painel central (Middle.js) ficaria assim:

 

Ext.define('SampleApp.view.layout.Middle', {
  extend        : 'Ext.panel.Panel',
  alias         : 'widget.layoutmiddle',
  id:'layoutmiddle',
  deferredRender: false,
  region:'center',
  layout: 'card',
	baseCls: 'x-plain'
});

 

O código das outras classes você pode obter fazendo o download do projeto no final do post.

Controller Authentication (Authentication.js)

Este é o controller das funcionalidades do formulário de login. Além de vincular uma ação no botão "Entrar", adicionei mais 2 funcionalidades: a primeira é reconhecer o "Enter" como um click no botão e a segunda é verificar se o CapsLock está acionado e, dependendo do caso, exibir uma mensagem de aviso (CapsWarningTooltip.js):

 

Ext.define('SampleApp.controller.Authentication', {
  extend: 'Ext.app.Controller',
  views: ['authentication.Login', 'authentication.CapsWarningTooltip'],
  init: function() {
    console.log('Authentication.init()');
    this.control({
      'authenticationlogin button[action=trylogin]': {
        click: this.tryLogin
      },
      'authenticationlogin #l': {
        keypress: this.verifyCapsLock,
        keyup   : this.verifyEnter
      },
      'authenticationlogin #s': {
        keypress: this.verifyCapsLock,
        keyup   : this.verifyEnter
      }
    });
  },
  index:function(){
  	
    var middle = Ext.getCmp('viewport_default');
    if (middle){
      middle.removeAll();
    }
    Ext.widget('authenticationlogin');
  },
  tryLogin: function(button){
    var loginWin    = button.up('window');
    var loginForm   = loginWin.down('form');
    if (loginForm.getForm().isValid()){
      var values = loginForm.getValues();
      var ok;
      loginForm.submit({
        url : 'data/trylogin.json'          //Simula OK
        ,method : 'POST'
        ,scope:this
        ,success: function(form, action){
             loginWin.close();
             Ext.ux.Router.redirect('');
        }
        ,failure: function(form, action)
        {
          var lblField = Ext.ComponentQuery.query('authenticationlogin #msgField')[0];
          if (lblField ){
            switch (action.failureType) {
              case Ext.form.action.Action.CLIENT_INVALID:
                lblField.setText("Campos inválidos", false);
                break;
              case Ext.form.action.Action.CONNECT_FAILURE:
                lblField.setText("Falha ao conectar no servidor", false);
                break;
              case Ext.form.action.Action.SERVER_INVALID:
                lblField.setText(action.result.msg||"Usuário e/ou senha inválido", false);
            }
          }
        }
      });
      
    }
  },
  factoryCapsWarningToolTip: function(){
    /*
     * Cria a view do tooltip
     */
    if (!this._capswarningtooltip){
      this._capswarningtooltip = Ext.widget('capswarningtooltip');
    }
    return this._capswarningtooltip;
  },
  verifyCapsLock: function(text, e) {
    var charCode = e.getCharCode();
    if(
      (e.shiftKey && charCode >= 97 && charCode <= 122) ||
      (!e.shiftKey && charCode >= 65 && charCode <= 90)
    ){
      this.factoryCapsWarningToolTip().show();
      return false;
    } else {
      this.factoryCapsWarningToolTip().hide();
    }  
  },
  verifyEnter: function(txt,e){
    if(e.getKey() === e.ENTER){
      e.stopEvent();
      var btn = Ext.ComponentQuery.query('authenticationlogin button[action=trylogin]')[0];
      if (btn){
        this.tryLogin(btn);
      }
    }
  }
});

 

Eu não implementei estas funções dentro da view Login (Login.js) pois quis aproveitar uma das facilidades do modelo MVC. Se algum dia eu não quiser mais esta funcionalidade, basicamente eu devo inibir as ações da view alterando os parâmetros do this.control no método init.

O mesmo vale para adicionar novas funcionalidades. Se algum dia eu quiser um teclado virtual, por exemplo, eu crio a view e eu vinculo com a view de login através de seu controller. Laughing

Controller Viewport (Viewport.js)

O código do controller Viewport.js está listado abaixo:

 

Ext.define('SampleApp.controller.Viewport', {
  extend: 'Ext.app.Controller',
  views: ['layout.Header','layout.Footer','layout.Left','layout.Right','layout.Middle', 'layout.Appview','home.Home'],
  defaultItem:{
        id:'viewport_default',
        region:'center',
        layout:'fit',
        border:0,
        html: 
        "<p align='center'>" +
        "    <img src='images/logo-sencha-sm.png' alt='Logo' />" +
        "</p>"
        },
  init: function() {
    this.control({
       'viewport': {
	   render: this.onViewportRendered
      }
    });
  },
  index:function(){
  	this.render('home.Home');
  },
  onViewportRendered: function(p) {
    console.log("Viewport - onViewportRendered");
    p.add(this.defaultItem);
    this.index();
  }
});

 

Aqui, sempre que eu rendenizo a viewport, o método index é chamado. Este método por sua vez, chama o método this.render, passando como parâmetro a primeira tela do sistema ('home.Home'). Este método é descrito em outro arquivo.....


Classe Ext.ux.Initialization (Initialization.js)

A Classe Ext.ux.Initialization serve para configurar a classe do Controller. Através do método render, o sistema sempre verificará se o usuário está logado ao acessar uma view. A outra função é configurar os listeners da classe Ext.ux.Router.

 

/**
 *
 * Modelo de Login usando MCV
 * Desenvolvido por Ricardo Hirashiki
 * Publicado em: http://www.sitedoricardo.com.br
 * Data: Ago/2011
 *
 * Baseado nos exemplos disponibilizados em
 * https://github.com/brunotavares/Ext.ux.Router
 * 
 */
 
Ext.define('Ext.ux.Initialization', {requires: ['Ext.ux.Router']},
  (function()
  {
	  console.log('Ext.ux.Initialization - begin');
  /* 
   * Override Ext.app.Controller to provide render capability. I believe each application
   * will handle rendering task different (some will render into a viewport, some in tabs, etc...), 
   * so I didn't put this role into Ext.ux.Route responsability.
   */
  Ext.override(Ext.app.Controller, {
      render: function(view)
      {
      	if (!view){
      		view="home.Home";
        }
        Ext.Ajax.request({
          url : 'data/islogged.json',   //Simula ERRO
          method: 'POST',
          scope:this,
          success: function ( result, request ) { 
            var retorno = Ext.decode(result.responseText);
            if (retorno.success)
            {
              var viewport_main = Ext.getCmp('viewport_default');
              var viewport_layoutappview = Ext.getCmp('layoutappview');
              if (!viewport_layoutappview){
                viewport_main.removeAll();
                viewport_main.add(Ext.widget('layoutappview'));
                viewport_main.doLayout();
              }
              var middle = Ext.getCmp('layoutmiddle');
              if (middle){
                middle.removeAll();
                console.log(middle);
                //load view
                if (Ext.isString(view)) {
                  view = this.getView(view);
                }
                   
                //if it already exists, remove
                element = middle.child(view.xtype);
                if(element){
                  middle.remove(element);
                }
                middle.setActive(true,middle.add(view));
                middle.doLayout();                
             }
            }else{
              Ext.ux.Router.redirect('login');
            }
          },
          failure: function ( result, request) { 
            console.log(result);
            console.log(request);
            switch (result.failureType) {
              case Ext.form.action.Action.CLIENT_INVALID:
                Ext.MessageBox.alert('Erro', "Campos inválidos"); 
                break;
              case Ext.form.action.Action.CONNECT_FAILURE:
                Ext.MessageBox.alert('Erro', "Falha ao conectar no servidor"); 
                break;
              case Ext.form.action.Action.SERVER_INVALID:
                this.onAuthenticationFail(sender);
            }
          } 
        });
    }
  });
  
  /* 
   * Ext.ux.Router provides some events for better controlling
   * dispatch flow
   */
  Ext.ux.Router.on({
      routemissed: function(uri)
      {
          Ext.Msg.show({
              title:'Error 404',
              msg: 'Route not found: ' + uri,
              buttons: Ext.Msg.OK,
              icon: Ext.Msg.ERROR
          });
      },
      beforedispatch: function(uri, match, params)
      {
          console.log('beforedispatch ' + uri);
      },
      dispatch: function(uri, match, params, controller)
      {
          console.log('dispatch ' + uri);
          //TIP: you could automize rendering task here, inside dispatch event
      }
  });
  console.log('Ext.ux.Initialization - end');
  })
);

Customização

A única customização necessária foi uma pequena alteração na Classe Ext.ux.Router (Router.js). Internamente, esta classe faz uma chamada ao método getController. De acordo com a documentação do Sencha, este método retorna uma instância do controller e, se necessário, cria o controller. Acontece que os controllers adicionados deste modo não chamam o método init. Então, adicionei uma linha chamando o método:

 

/**
         * Receives an URI Token, parses it, and find a respective route, firing 'dispatch' event. 
         * If no route is found, fires 'routemissed' event.
         * @param {String} uri URI token (e.g. 'home/index', 'users/1/edit')
         */
        processURI: function(uri)
        {
            var controller,
                uriObject = pub.decomposeURI(uri),
                match = this.findMatch(uriObject),
                params = Ext.apply({}, uriObject.params);
            
            if(match === false){
                pub.fireEvent('routemissed', uri);
                return false;
            }
            
            Ext.iterate(match.sections, function(section, i)
            {
                if(regColumn.test(section))
                {
                    section = section.replace(regColumn, '');
                    params[section] = uriObject.sections[i];
                }
            });
            
            Ext.iterate(params, function(key, value)
            {
                params[key] = parseValue(value);
            });
            
            if(pub.fireEvent('beforedispatch', uri, match, params) === false){
                return;
            }
            
            controller  = app.getController(match.controller);
            controller['init'].call(controller, params);
            controller[match.action].call(controller, params);
            pub.fireEvent('dispatch', uri, match, params, controller);
        }

 

 

Execução e Testes

Para ver todas as possibilidades que o exemplo cobre, tente mudar os retornos das chamadas de islogged.json e trylogin.json.

Bom estudo!!

Até logo!

ExtJS_login_MVC.zip (17,75 kb)

Tags: ,

ExtJS

Anunciante

Calendário

<<  February 2012  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
2728291234
567891011

View posts in large calendar

RecentPosts