Tutorial: Combo Boxes Aninhados com ExtJS, Spring MVC 3 e Hibernate 3.5

13/10/2010 | By | 13 Comments

Este é um tutorial passo a passo de como implementar combo boxes aninhados usando ExtJS (no lado cliente) e Spring MVC 3 e Hibernate 3.5 (no lado servidor).

Vou usar o exemplo clássico de comboboxs: estados e cidades. neste exemplo, vou usar os estados e cidades do Brasil.

extjs linked comboboxes spring loiane02 Tutorial: Combo Boxes Aninhados com ExtJS, Spring MVC 3 e Hibernate 3.5

Qual é o objetivo final? Quando o usuário selecionar um estado no primeiro combo box, a aplicação irá carregar o segundo combo box com as cidades que pertencem ao estado selecionado – sem recarregar a página.

No ExtJS, existem duas maneiras de implentar.

A primeira é carregar o conteúdo dos dois combo boxes, e quando o usuário selecionar um estados, a aplicação irá filtrar os dados do combo box de cidades para mostrar apenas as cidades que pertencem ao estado selecionado.

A segunda forma é carregar apenas as informações necessárias para popular o combo box dos estados. Quando o usuário selecionar um estado, a aplicação irá fazer uma requisição para carregar as informações das cidades do estado escolhido.

Qual é a melhor maneira? Depende da quantidade de dados que será necessário buscar no banco de dados. Por exemplo: você tem um combo box que lista todos os países do mundo. E o segundo combo box representa todas as cidades do mundo (ou cidades de cada país). Neste caso, o cenário número 2 é a melhor opção, porque no cenário 1 seria necessário carregar todas as cidades de uma só vez. Imagina a quantidade enorme de dados que iria carregar do banco de dados? É necessário analisar.

Ok. Vamos ao código fonte. Vou mostrar como implementar ambos os cenários.

Mas primeiro, vou mostrar como o projeto está organizado:

extjs linked comboboxes spring loiane01 Tutorial: Combo Boxes Aninhados com ExtJS, Spring MVC 3 e Hibernate 3.5

Vamos dar uma olhada no código Java.

BaseDAO:

Contém o hibernate template usado por CityDAO e StateDAO.

package com.loiane.dao;

import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.stereotype.Repository;

@Repository
public abstract class BaseDAO {

	private HibernateTemplate hibernateTemplate;

	public HibernateTemplate getHibernateTemplate() {
		return hibernateTemplate;
	}

	@Autowired
	public void setSessionFactory(SessionFactory sessionFactory) {
		hibernateTemplate = new HibernateTemplate(sessionFactory);
	}
}

CityDAO:

Contém dois métodos: um para carregar todas as cidades do banco (usado no cenário #1), e outro método para carregar todas as cidades que pertencem a um determinado estado (usado no cenário #2).

package com.loiane.dao;

import java.util.List;

import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Restrictions;
import org.springframework.stereotype.Repository;

import com.loiane.model.City;

@Repository
public class CityDAO extends BaseDAO{

	public List<City> getCityListByState(int stateId) {

		DetachedCriteria criteria = DetachedCriteria.forClass(City.class);
		criteria.add(Restrictions.eq("stateId", stateId));

		return this.getHibernateTemplate().findByCriteria(criteria);

	}

	public List<City> getCityList() {

		DetachedCriteria criteria = DetachedCriteria.forClass(City.class);

		return this.getHibernateTemplate().findByCriteria(criteria);

	}
}

StateDAO:

Contém apenas um método para carregar todos os estados do banco.

package com.loiane.dao;

import java.util.List;

import org.hibernate.criterion.DetachedCriteria;
import org.springframework.stereotype.Repository;

import com.loiane.model.State;

@Repository
public class StateDAO extends BaseDAO{

	public List<State> getStateList() {

		DetachedCriteria criteria = DetachedCriteria.forClass(State.class);

		return this.getHibernateTemplate().findByCriteria(criteria);

	}
}

City:

Representa o POJO Cidade/City; representa a tabela Cidade/City.

package com.loiane.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import org.codehaus.jackson.annotate.JsonAutoDetect;

@JsonAutoDetect
@Entity
@Table(name="CITY")
public class City {

	private int id;
	private int stateId;
	private String name;

	//getters and setters
}

State:

Representa o POJO Estado/State; represeta a cidade Estado/State.

package com.loiane.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import org.codehaus.jackson.annotate.JsonAutoDetect;

@JsonAutoDetect
@Entity
@Table(name="STATE")
public class State {

	private int id;
	private int countryId;
	private String code;
	private String name;

	//getters and setters
}

CityService:

Contém dois métodos: um para carregar todas as cidades do banco (usado no cenário #1), e outro método para carregar todas as cidades que pertencem a um determinado estado (usado no cenário #2).

Faz apenas chamada para a classe CityDAO.

package com.loiane.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.loiane.dao.CityDAO;
import com.loiane.model.City;

@Service
public class CityService {

	private CityDAO cityDAO;

	public List<City> getCityListByState(int stateId) {
		return cityDAO.getCityListByState(stateId);
	}

	public List<City> getCityList() {
		return cityDAO.getCityList();
	}

	@Autowired
	public void setCityDAO(CityDAO cityDAO) {
		this.cityDAO = cityDAO;
	}
}

StateService:

Contém apenas um método para carregar todos os estados do banco. Faz apenas uma chamada para a classe StateDAO.

package com.loiane.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.loiane.dao.StateDAO;
import com.loiane.model.State;

@Service
public class StateService {

	private StateDAO stateDAO;

	public List<State> getStateList() {
		return stateDAO.getStateList();
	}

	@Autowired
	public void setStateDAO(StateDAO stateDAO) {
		this.stateDAO = stateDAO;
	}
}

CityController:

Contém dois métodos: um para carregar todas as cidades do banco (usado no cenário #1), e outro método para carregar todas as cidades que pertencem a um determinado estado (usado no cenário #2). Faz apenas chamada para a classe CityService. Ambos os métodos retornam um objeto JSON no seguinte formato:

{"data":[
         {"stateId":1,"name":"Acrelândia","id":1},
         {"stateId":1,"name":"Assis Brasil","id":2},
         {"stateId":1,"name":"Brasiléia","id":3},
         {"stateId":1,"name":"Bujari","id":4},
         {"stateId":1,"name":"Capixaba","id":5},
         {"stateId":1,"name":"Cruzeiro do Sul","id":6},
         {"stateId":1,"name":"Epitaciolândia","id":7},
         {"stateId":1,"name":"Feijó","id":8},
         {"stateId":1,"name":"Jordão","id":9},
         {"stateId":1,"name":"Mâncio Lima","id":10},
]}

Classe:

package com.loiane.web;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.loiane.service.CityService;

@Controller
@RequestMapping(value="/city")
public class CityController {

	private CityService cityService;

	@RequestMapping(value="/getCitiesByState.action")
	public @ResponseBody Map<String,? extends Object> getCitiesByState(@RequestParam int stateId) throws Exception {

		Map<String,Object> modelMap = new HashMap<String,Object>(3);

		try{

			modelMap.put("data", cityService.getCityListByState(stateId));

			return modelMap;

		} catch (Exception e) {

			e.printStackTrace();

			modelMap.put("success", false);

			return modelMap;
		}
	}

	@RequestMapping(value="/getAllCities.action")
	public @ResponseBody Map<String,? extends Object> getAllCities() throws Exception {

		Map<String,Object> modelMap = new HashMap<String,Object>(3);

		try{

			modelMap.put("data", cityService.getCityList());

			return modelMap;

		} catch (Exception e) {

			e.printStackTrace();

			modelMap.put("success", false);

			return modelMap;
		}
	}

	@Autowired
	public void setCityService(CityService cityService) {
		this.cityService = cityService;
	}
}

StateController:

Contém apenas um método para carregar todos os estados do banco. Faz apenas uma chamada para a classe StateService. O método retorna um objeto JSON no seguinte formato:

{"data":[
         {"countryId":1,"name":"Acre","id":1,"code":"AC"},
         {"countryId":1,"name":"Alagoas","id":2,"code":"AL"},
         {"countryId":1,"name":"Amapá","id":3,"code":"AP"},
         {"countryId":1,"name":"Amazonas","id":4,"code":"AM"},
         {"countryId":1,"name":"Bahia","id":5,"code":"BA"},
         {"countryId":1,"name":"Ceará","id":6,"code":"CE"},
         {"countryId":1,"name":"Distrito Federal","id":7,"code":"DF"},
         {"countryId":1,"name":"Espírito Santo","id":8,"code":"ES"},
         {"countryId":1,"name":"Goiás","id":9,"code":"GO"},
         {"countryId":1,"name":"Maranhão","id":10,"code":"MA"},
         {"countryId":1,"name":"Mato Grosso","id":11,"code":"MT"},
         {"countryId":1,"name":"Mato Grosso do Sul","id":12,"code":"MS"},
         {"countryId":1,"name":"Minas Gerais","id":13,"code":"MG"},
         {"countryId":1,"name":"Pará","id":14,"code":"PA"},
]}

Classe:

package com.loiane.web;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.loiane.service.StateService;

@Controller
@RequestMapping(value="/state")
public class StateController {

	private StateService stateService;

	@RequestMapping(value="/view.action")
	public @ResponseBody Map<String,? extends Object> view() throws Exception {

		Map<String,Object> modelMap = new HashMap<String,Object>(3);

		try{

			modelMap.put("data", stateService.getStateList());

			return modelMap;

		} catch (Exception e) {

			e.printStackTrace();

			modelMap.put("success", false);

			return modelMap;
		}
	}

	@Autowired
	public void setStateService(StateService stateService) {
		this.stateService = stateService;
	}
}

Dentro da pasta WebContent temos:

  • ext-3.2.1 – contém todos os arquivos ExtJS;
  • js – contém todos os arquivos javascript que foram implementados para este exemplo. liked-comboboxes-local.js comtém o código fonte do combo box para o cenário #1; liked-comboboxes-remote.js contém o combo box para o cenário #2; linked-comboboxes.js contém um tab panel para exemplificar os dois cenários.

Vamos dar uma olhada no código ExtJS.

Cenário Numero 1:

Carregar todos os dados disponíveis do banco de dados para popular os dois combo boxes. Usa um filtro no combo box das cidades.

liked-comboboxes-local.js:

	var localForm = new Ext.FormPanel({
       width: 400
       ,height: 300
       ,style:'margin:16px'
       ,bodyStyle:'padding:10px'
       ,title:'Linked Combos - Local Filtering'
       ,defaults: {xtype:'combo'}
       ,items:[{
            fieldLabel:'Select State'
           ,displayField:'name'
           ,valueField:'id'
           ,store: new Ext.data.JsonStore({
	       		url: 'state/view.action',
	            remoteSort: false,
	            autoLoad:true,
	            idProperty: 'id',
	            root: 'data',
	            totalProperty: 'total',
	            fields: ['id','name']
	        })
           ,triggerAction:'all'
           ,mode:'local'
           ,listeners:{select:{fn:function(combo, value) {
               var comboCity = Ext.getCmp('combo-city-local');
               comboCity.clearValue();
               comboCity.store.filter('stateId', combo.getValue());
               }}
           }

       },{
            fieldLabel:'Select City'
           ,displayField:'name'
           ,valueField:'id'
           ,id:'combo-city-local'
           ,store: new Ext.data.JsonStore({
       		   url: 'city/getAllCities.action',
               remoteSort: false,
               autoLoad:true,
               idProperty: 'id',
               root: 'data',
               totalProperty: 'total',
               fields: ['id','stateId','name']
           })
           ,triggerAction:'all'
           ,mode:'local'
           ,lastQuery:''
       }]
   });

O combo box que representa os estados (state) é declarado nas linhas  9 a 28.

O combo box que representa das cidades (city) é declarado nas linhas 31 a 46.

Repare que ambos os combo boxes são carregados quando fazemos o load da página, como pode ser visto nas linhas 15 e 38 (autoload:true).

O combo box que representa os estados possui um select event listener que quando executado, filtra o combo box que representa das cidades baseado na seleção atual do estado. Pode ser visto nas linhas 23 a 28.

O combo box que representa as cidades possui um atributo lastQuery:”". Isso é para “enganar” o combo box quando é feito o load da página. Assim, o combo box pensa que já foi feito um filtro.

Scenario Number 2:

Carrega apenas os dados dos estados do banco de dados. Quando o usuário seleciona um estado, a aplicação irá buscar todas as cidades relacionadas a este estado no banco de dados – sem fazer refresh da página.

liked-comboboxes-remote.js:

var dataBaseForm = new Ext.FormPanel({
       width: 400
       ,height: 200
       ,style:'margin:16px'
       ,bodyStyle:'padding:10px'
       ,title:'Linked Combos - Database'
       ,defaults: {xtype:'combo'}
       ,items:[{
            fieldLabel:'Select State'
           ,displayField:'name'
           ,valueField:'id'
           ,store: new Ext.data.JsonStore({
	       		url: 'state/view.action',
	            remoteSort: false,
	            autoLoad:true,
	            idProperty: 'id',
	            root: 'data',
	            totalProperty: 'total',
	            fields: ['id','name']
	        })
           ,triggerAction:'all'
           ,mode:'local'
           ,listeners: {
               select: {
                   fn:function(combo, value) {
                       var comboCity = Ext.getCmp('combo-city');
                       //set and disable cities
                       comboCity.setDisabled(true);
                       comboCity.setValue('');
                       comboCity.store.removeAll();
                       //reload city store and enable city combobox
                       comboCity.store.reload({
                           params: { stateId: combo.getValue() }
                       });
                       comboCity.setDisabled(false);
       			  }
               }
       		}
       },{
            fieldLabel:'Select City'
           ,displayField:'name'
           ,valueField:'id'
           ,disabled:true
           ,id:'combo-city'
           ,store: new Ext.data.JsonStore({
       		   url: 'city/getCitiesByState.action',
               remoteSort: false,
               idProperty: 'id',
               root: 'data',
               totalProperty: 'total',
               fields: ['id','stateId','name']
           })
           ,triggerAction:'all'
           ,mode:'local'
           ,lastQuery:''
       }]
});

O combo box que representa os estados (state) é declarado nas linhas  9 a 38.

O combo box que representa das cidades (city) é declarado nas linhas 40 a 55.

Repare que apenas o combo box dos estados é carregado quando fazemos o load da página, como pode ser visto na linha 15 (autoload:true).

O combo box que representa os estados possui um select event listener que quando executado, carrega os dados para a store das cidades (passa stateId como parâmetro) baseado no estado selectionado. Pode ser vista nas linhas 24 a 38.

O combo box que representa as cidades possui um atributo lastQuery:”". Isso é para “enganar” o combo box quando é feito o load da página. Assim, o combo box pensa que já foi feito um filtro.

Se desejar, pode fazer o download do projeto completo no meu repositório GitHub: http://github.com/loiane/extjs-linked-combox

Usei Eclipse IDE + TomCat 7 para desenvolver este projeto de exemplo.

Referência: http://www.sencha.com/learn/Tutorial:Linked_Combos_Tutorial_for_Ext_2

Bons códigos! icon smile Tutorial: Combo Boxes Aninhados com ExtJS, Spring MVC 3 e Hibernate 3.5

Filed in: Ext JS 3 | Tags: , , , , , , , , , ,

Comments (13)

  1. Júnior

    Olá tudo bem? Tenho acompanhado (e muito) os materiais que você tem disponibilizado. Quero aprofundar bastante nessa tecnologia. Eu peguei um exemplo seu de grid então adaptei à minha necessidade, só que não estou conseguindo fazer consultas parametrizadas. Por exemplo: Em uma consulta poder recuperar a informação através de vários tipos de parâmetros diferentes, tais como: nome, data de nascimento, cidade, setor. O exemplo que vc passou do grid retorna os dados completos sem algum tipo de filtragem. Quando me deparei com controller, @Requestparam, store.load()….etc… fiquei sem saber como passar o parâmetro, verificar e retornar os dados à contento. Se tiver um exemplo assim ficaria grato. Postei aqui e não no exemplo do grid prq percebi que este post é recente. Agradeço antecipadamente a sua atenção.

  2. Rafael Reuber

    Por que na classe City você colocou o atributo stateId ao Invés de um objeto Estate?
    Se você usasse um objeto, não funcionaria?

    • Ei Rafael,
      Coloquei por causa do formato que o ExtJS espera. Até poderia carregar o objeto Estado junto, mas aí estaria enviando informações desnecessárias para o Ext JS, e isso ia fazer com que o volume de dados trocados entre cliente e servidor seja maior, sendo que não vai usar toda a informação.
      []‘s

  3. Rafael Reuber

    Isso que você falou, é verdade. Nesse caso, seriam informações desnecessárias. Quando passamos de frameworks java baseados em componentes, para programar em Javascript, temos que fazer essas considerações.

    Mas há casos em que temos que retornar os objetos com composições com atributos completos.

  4. Maycon Belfort

    Olá Loiane, teria como você fazer um tutorial deste mesmo tipo, porém utilizando o ExtJS 4? Peguei um exemplo que você postou em um forum, mas estou tendo dificuldades quanto a implementar comboboxs em um form. Seria muito útil.

    Estarei aguardando o seu tutorial, enquanto isso vou tentando implementar do jeito que entendi… srrsrs

    Aprecio seu trabalho, continue contribuindo para nossa comunidade.

    Obrigado!

  5. Oliver

    Olá Loiane,
    Desculpa se eu estiver falando bobagem..rsrs
    É possível integrar ExtJS com JSF?
    Se sim, poderia fazer um tutorial?

    Parabéns, sucesso e tudo de bom!

    • Olá Oliver,
      Sim, é possível integrar ExtJS com JSF, porém eu não vejo razão para isso já que o JSF já possui componentes nativos.
      Mas anotei a sugestão! :)
      []‘s

  6. Éverton Trindade

    Olá Loiane…

    Testando seu tutorial com ExtJS4 notei que o Store não possui o método reload.

    Nesse caso, qual método utilizar?

    Só consegui fazer carregar com o método load. Porém, ele só carrega na primeira seleção. A partir da segunda em diante ele carrega o select corretamente, e fica em estado de “carregando”… Assim, não consigo selecionar nenhuma opção do combobox, fica desabilitado.

    Você poderia me ajudar quanto ao problema?

    desde já agradeço

    • Olá Éverton, vc vai usar o método load mesmo, e passar o item selecionado no primeiro combobox através do extraParams.
      []‘s

  7. Éverton Trindade

    Boa noite, Loiane… Tentei realizar como mostra o código abaixo… porém sem sucesso… vc poderia me auxiliar?

    items: [{
    xtype: 'combobox',
    name: 'uf.codigo',
    displayField: 'sigla',
    valueField: 'codigo',
    store: ufsStore,
    flex: 1,
    triggerAction: 'all',
    queryMode: 'local',
    listeners:{
    select: {
    fn : function(combo, value) {
    var comboCidade = Ext.getCmp('cidadeCombo');
    comboCidade.clearValue();
    comboCidade.setDisabled(true);
    comboCidade.setValue('');
    comboCidade.store.removeAll();
    comboCidade.store.load({
    extraParams : {
    codigoUf: combo.getValue()
    }
    });
    comboCidade.setDisabled(false);
    }
    }
    }
    }, {
    xtype: 'combobox',
    name: 'cidade.codigo',
    id: 'cidadeCombo',
    fieldLabel: 'Cidade',
    displayField: 'nome',
    valueField: 'codigo',
    labelAlign: 'right',
    store: cidadesStore,
    triggerAction: 'all',
    queryMode: 'local',
    margins: '0 0 0 6',
    labelWidth: 50,
    flex: 6,
    lastQuery: ''
    }]

Leave a Reply

Trackback URL | RSS Feed for This Entry