Tutorial: Combo Boxes Aninhados com ExtJS, Spring MVC 3 e Hibernate 3.5
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.
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:
Vamos dar uma olhada no código Java.
BaseDAO:
Contém o hibernate template usado por CityDAO e StateDAO.
[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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);
}
}
[/code]
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).
[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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);
}
}
[/code]
StateDAO:
Contém apenas um método para carregar todos os estados do banco.
[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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);
}
}
[/code]
City:
Representa o POJO Cidade/City; representa a tabela Cidade/City.
[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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
}
[/code]
State:
Representa o POJO Estado/State; represeta a cidade Estado/State.
[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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
}
[/code]
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.
[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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;
}
}
[/code]
StateService:
Contém apenas um método para carregar todos os estados do banco. Faz apenas uma chamada para a classe StateDAO.
[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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;
}
}
[/code]
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:
[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
{"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},
]}
[/code]
Classe:
[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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;
}
}
[/code]
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:
[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
{"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"},
]}
[/code]
Classe:
[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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;
}
}
[/code]
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:
[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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:''
}]
});
[/code]
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:
[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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:''
}]
});
[/code]
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! :)