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.
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!