Construindo uma select box com options condicionais utilizando cakephp e ajax
Ao acompanhar a lista de discussão CakePHP Tuga percebi que existe uma dúvida que sempre dá as caras por lá: preencher as opções de um select box baseado na escolha de um select box preenchido anteriormente. O maior exemplo disso é o famoso “select cidade estado” ou “combo box cidade estado”.
Essa dúvida retornou novamente, mas através de um e-mail enviado para o pessoal da empresa por um amigo de faculdade com a mesma dúvida. No e-mail ele ainda referenciava duas fontes [1] [2] em que havia tentado, mas não obtia sucesso. Talvez a dificuldade seja o idioma, então decidi fazer um exemplo e disponibilizar para a comunidade. Este exemplo utiliza a famosa combinação de seleção do estado e depois da cidade, sendo que o select box de cidades é preenchido a partir da escolha do estado.
Criei mais coisas que o necessário pensando que esses arquivos poderão ser utilizados para outros projetos, um exemplo disso é o modelo de cidades, que não possui basicamente nada, mas pode ser aumentado para inserção de cidades em um determinado estado.
Atualmente tudo deve ser feito diretamente no banco de dados. Vamos iniciar com sua criação:
DROP TABLE IF EXISTS `cidades`; CREATE TABLE IF NOT EXISTS `cidades` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `estado_id` int(10) UNSIGNED NOT NULL, `cidade` varchar(50) collate utf8_unicode_ci NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; DROP TABLE IF EXISTS `estados`; CREATE TABLE IF NOT EXISTS `estados` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `sigla` char(2) character SET utf8 NOT NULL, `estado` varchar(30) character SET utf8 NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
Logo depois criamos os modelos:
class Estado extends AppModel { var $name = 'Estado'; var $hasMany = array( 'Cidade' => array( 'dependent' => true ) ); } class Cidade extends AppModel { var $name = 'Cidade'; var $belongsTo = 'Estado'; }
Até esse ponto nenhuma novidade, agora vamos aos controllers. O controller de cidades está quase vazio, contém somente sua declaração.
Vou colocar apenas o de estados.
class EstadosController extends AppController { var $name = 'Estados'; var $helpers = array('Html', 'Form', 'Javascript', 'Ajax'); var $components = array('RequestHandler'); function lista_cidades() { $estados = $this->Estado->find('list', array( 'fields' => array('Estado.id', 'Estado.estado'), ) ); $this->set('estados', $estados); } function cidades_estado() { $cidades = $this->Estado->Cidade->find('list', array( 'conditions' => array('Cidade.estado_id' => $this->data['Estado']['estado']), 'fields' => array('Cidade.id', 'Cidade.cidade'), 'order' => array('Cidade.cidade ASC') ) ); $this->set('cidades', $cidades); } }
Aqui começa a ficar interessante, observem que foi necessário carregar os helpers Ajax e Javascript além do component RequestHandler. Esses são os responsáveis pela mágica. O primeiro método é a página que possui as combo box de estado e cidade. O segundo, é responsável por trazer as cidades referentes a um estado.
Vejam agora a primeira view:
<?php echo $javascript->link('prototype', false); echo $javascript->link('http://cidades-estados-js.googlecode.com/files/cidades-estados-1.0.js', false); echo $javascript->link('cidade_estado', false); ?> <fieldset> <legend>CakePHP Way</legend> <?php echo $form->input('Estado.estado', array('options' => $estados)); echo $form->input('Estado.cidades', array('options' => array())); echo $ajax->observeField('EstadoEstado', array( 'update' => 'EstadoCidades', 'url' => array('controller' => 'estados', 'action' => 'cidades_estado') ) ); ?> </fieldset> <fieldset> <legend>Cidades-Estado</legend> <?php echo $form->input('estado', array('type' => 'select')); echo $form->input('cidade', array('type' => 'select')); ?> </fieldset>
Nesta view chamados o prototype, necessário para que o cake consiga realizar as requisições ajax, preencher o segundo select box, etc. Lembrando que ele deverá estar na pasta webroot/js da sua aplicação. No primeiro fieldset declaramos dois campos select, sendo um já preenchido com os estados encontrados no banco e trazidos pelo controller, o segundo deverá ter seus valores preenchidos com as cidades de um estado selecionado. O método observeField do helper ajax é utilizado no select com id EstadoEstado, que chama a action cidades_estado do controller estado. Essa action é responsável por trazer a lista de cidades do estado selecionado. Ao finalizar a requisição, o select box com id EstadosCidade deverá ser atualizado com o resultado retornado pela action.
Esse resultado deverá ser um conjunto de options com as cidades, gerado na view abaixo:
if(!empty($cidades)) { foreach ($cidades as $id => $cidade) { echo '<option value="'.$id.'">'.$cidade.'</option>'; } }
Pronto, já temos um select de estados > cidade condicional em ajax funcionando. Lembrando que esse exemplo pode ser abstraído para qualquer outra necessidade semelhante.
Aproveitando que esse tipo de necessidade é bastate comum, aproveitei para mostrar um pequeno script que já faz todo o trabalho para você.
Trata-se de um projeto hospedado no googlecode, que faz todo o trabalho sujo para você chamado cidades-estado-js. A única coisa necessária é adicionar o fonte e dizer onde ele deverá funcionar.
Neste exemplo ele é o segundo script adicionado. O terceiro script é quem diz onde deverão ser colocadas as cidades e estados. Veja seu fonte, que utiliza prototype.
$(document).observe('dom:loaded', function(){ new dgCidadesEstados({ estado: $('estado'), cidade: $('cidade') }); });
Pronto, com poucas linhas de código já se tem toda a funcionalidade. Agora basta escolher a versão em CakePHP ou a cidades-estados-js.
Para quem deseja muita agilidade, utilize a cidades-estado-js. Para quem quer velocidade e flexibilidade total, mãos a obra com CakePHP.
Acredito que esse post do pinceladas web também possa interessar a quem está lendo até esse ponto. Ele contém um arquivo sql com todas as cidades e estados do Brasil.
[1] – http://www.devmoz.com/blog/2007/04/04/cakephp-update-a-select-box-using-ajax/
[2] – http://www.jamesfairhurst.co.uk/posts/view/using_ajax_to_populate_a_select_box_in_cakephp/
Posts relacionados:
- jshash – Implementando um Hash em JavaScript Recentemente escrevi algum código javascript que realizava algumas requisições, e...
Comentários (32)
-
001
alexandre correia
em 01/05/2009 14:02:39O assunto que refere o post é muito útil, mas como sou novo no cake PHP ainda fico meio perdido de onde colocar as coisas, por isto não consegui fazer funcionar.
Percebi que a primeira view terá nome lista_cidades.ctp e ficará na pasta …/view/estados. Mas deste ponto para o final do post não conseguir compreender como as coisas se encaixam e onde colcar os arquivos. Se fosse possível detalhar um pouco mais, inclusive com exemplo, ajudaria bastante. Obrigado :) -
002
Fabrício Ferracioli
em 01/05/2009 19:20:47Que bom que você achou o post útil, pena que não foi efetivo no seu caso.
Pelo que entendi seu problema é basicamente onde devem ser colocados os arquivos. Basta seguir as convenções do cake! A maioria delas você encontra nos subtópicos do manual na parte Developing with CakePHP. Mas vamos ao que interessa! Assumindo que você está na pasta de sua aplicação (por exemplo /cake/app/), ai vai a lista de onde devem ser colocados os arquivos:- Model Estado
- models/estado.php
- Model Cidade
- models/cidade.php
- Controller Estados
- controllers/estados_controller.php
- Controller Cidades
- controllers/cidades_controller.php
- Primeira View
- views/estados/lista_cidades.ctp
- Segunda View
- views/estados/cidades_estado.ctp
- Prototype
- webroot/js/prototype.js
- Script JS cidade_estado
- webroot/js/cidade_estado.js
Espero que agora o post sirva para você e que tudo funcione como deveria!
Coloquei o exemplo do post no github. Basta acessar este repositorio. -
003
alexandre correia
em 04/05/2009 09:53:40Boa tarde Fabrício,
Fantástico, depois dos esclarecimentos, compreendi todo o processo e está funcionando 100%. Vou aproveitar este post, para além de agradecer, perguntar-te se há algo que seja possível fazer com CHECKBOXES. Utilizando o teu exemplo de Estado e Cidades, se não seria possível ao selecionar um Estado, aparecer a lista das cidades (cada uma com uma CheckBox) onde ao marcar cada cidade o Estado e a(s) cidade(s) selecionada(s) seriam repassadas para o controller. -
004
alexandre correia
em 04/05/2009 10:02:12Não sei se ajuda, mas a pergunta anterior que fiz é porque preciso implementar uma grade de horários para cada usuário selecionado. Ou seja, tenho um usuário e é preciso mostrar uma lista de horários disponíveis para que ele escolha (podem ser um ou vários horários) e ao pressionar o botão deve ser gravado no banco de dados (tabela horario_users) o id do usuário (que se repete) a cada id do horário. E isto, infelizmente o manual do cake PHP não ensina (ao menos onde tenho procurado).
Tabela users:
id
name
…
Tabela horarios:
id
name
day
start_time
end_timeTabela horario_users:
id
id_horario
id_user
estado enum(‘livre’,'ocupado’)obrigado :)
-
005
Fabrício Ferracioli
em 04/05/2009 13:04:22Alexandre, o que você deseja fazer também não é difícil.
Você vai trocar os elementos de select de estado por checkboxes.
Depois, no select de cidades, você deverá colocar um elemento vazio com uma id que deverá ser passada para o observeField atualizar. No caso do exemplo, pode ser uma div assim:<div id="EstadoCidades"></div>
Depois disso é só mudar a view cidades_estado.ctp, para que ela coloque checkboxes ao invés de options.
Só muda uma linha, acredito que você consiga fazer facilmente.
Legal que a resposta serviu para você.
Quando tiver outras questões relacionadas a cake, o grupo de CakePHP poderá te ajudar com certeza.
Sucesso! -
006
alexandre correia
em 05/05/2009 06:54:58Olá Fabrício,
Obrigado pela dica, de fato após consultar mais uma vez o cook book e o teu precioso artigo, consegui colocar as checkboxes para funcionar. Agora quando escolho o Estado (na select, como era antes) aparecem várias checkboxes (uma para cada cidade).
Não me canso de agradecer. Obrigado e sucesso para ti :) -
007
Fabrício Ferracioli
em 05/05/2009 10:29:34Que bom que você consegiu! E sempre que tiver alguma dúvida pode procurar o pessoal da comunidade que está ai para isso, seja pelo KISS ou por outro recurso.
Sucesso pra você também. -
008
Benicio
em 13/06/2009 16:19:09Olá Fabrício,
Primeiramente parabenizar pelo excelente post. Estou fazendo algo similar com Categorias e Subcategorias. Seguindo a sua lógica coloquei no meu controller:
$subcategorias = $this->Categoria->Subcategoria->find(‘all’,
array(
‘conditions’ => array(‘Subcategoria.categoria_id’ => $this->data['Categoria']['id']),
‘fields’ => array(‘Subcategoria.id’, ‘Subcategoria.descricao’),
‘order’ => array(‘Subcategoria.descricao ASC’)
)
);Roda e não dá nenhum erro em compensação as subcategorias são aparecem. Aí ativei o debug do Cake pra ver como ele estava fazendo query e pra minha surpresa está vindo assim:
SELECT `Subcategoria`.`id`, `Subcategoria`.`descricao` FROM `subcategorias` AS `Subcategoria` LEFT JOIN `categorias` AS `Categoria` ON (`Subcategoria`.`categoria_id` = `Categoria`.`id`) WHERE `Subcategoria`.`categoria_id` IS NULL ORDER BY `Subcategoria`.`descricao` ASC
Com essa ponto que está sendo crucial `Subcategoria`.`categoria_id` IS NULL. Procurei na internet como colocar condições com outro campo mas só achei sentando com strings ou números. Vc tem alguma idéia de como solucionar ?
Obrigado
-
009
Benicio
em 13/06/2009 23:30:47Olá Fabrício,
Esqueci de comentar que a diferença é será tudo carregado no inicio sem ser selecionado nenhum evento tipo. Como no exemplo abaixo
Alimentação
Lanchonete
PizarriaVendas
Automóvel
Imovéis -
010
Fabrício Ferracioli
em 15/06/2009 08:26:24Olá Benicio.
Pelo que entendi a variável$this->data['Categoria']['id']
possui um valor nulo. Troque seu array de conditions por esse e veja o que acontece:
array(’Subcategoria.categoria_id !=’ => $this->data['Categoria']['id'])
-
011
Benicio
em 15/06/2009 14:20:57Olá Fabrício,
Aí aparecem todas as subcategorias que estão cadastradas sem estarem filtradas. Será que tem algo haver com find(“all”) ? Não estou usando o find(“list”) pq dá o seguinte erro:
Fatal error: Cannot use string offset as an array in C:\www\compartilhar\app\views\layouts\default.ctp on line 78
linha 78: link($categoria['Categoria']['descricao'],array(‘controller’=> ‘categorias’, ‘action’=>’view’, $categoria['Categoria']['id']));?>
Eu consegui colocar pra rodar fazendo um POG dentro da view mas irei precisar do filtro em outro lugares sem falar que com esse POG implica muito na questão desempenho
-
012
Benicio
em 15/06/2009 14:30:54Opsss segue o POG
link($categoria['Categoria']['descricao'],’#');?>
link($subcategoria['Subcategoria']['descricao'],’#'); ?>
-
013
Benicio
em 15/06/2009 14:36:03Desculpe esqueci que com as tags não funciona. Colocando isso na view abaixo funciona parecendo que tá perfeito heheheheheheh.
if ($categoria['Categoria']['id'] == $subcategoria['Subcategoria']['categoria_id'])
-
014
Fabrício Ferracioli
em 16/06/2009 10:48:17Olá Benicio.
Você está certo, deixar o filtro na view não é nem de longe a melhor opção.
O que eu estou achando bastante estranho é a variável $this->data['Categoria']['id'] estar nula. Verifique na sua view se esse valor realmente se encontra setado, pois isso já deve resolver o problema. -
015
Benício
em 16/06/2009 12:50:08Olá Fabrício,
Desde já agradeço pela sua atenção e paciência. Olhá só na minha view eu peço para ele exibir $categoria['Categoria']['id'] e está mostrando todos os id’s correspondentes. O $this->data['Categoria']['id'] era pra realmente trazer os id’s que estão cadastrados no banco, correto ? Na verdade acho que o problema está em ser exibido dois dados diretos sem a necessidade de chamada de uma outra função como no caso dos estados e cidades. Se fosse pra serem exibidas as cidades agrupadas por estado seria da mesma forma ??
-
016
Fabrício Ferracioli
em 17/06/2009 08:27:31Bom, para exibir todas as subcategorias agrupadas por categoria você não deve restringir seu select na clausula where. Você só deverá fazer a join pela chave. O Cake já faz tudo isso sozinho.
Se eu entendi corretamente, é só retirar o array de conditions que você vai chegar no resultado desejado.
Espero ter ajudado dessa vez… -
017
Benício
em 17/06/2009 11:04:43Olá Fabrício,
Eu tb achava que o Cake faria toda essa parte mas não está funcionando. Acontece que eu tou com algumas particularidades que eu não se interferem. Essa exibição que eu tou tentando fazer está no display da pages_controller juntamente com outros $uses. Nesse caso eu não tenho um Model para Page. Tou reunindo alguns “elementos” na minha default.ctp que é exatamente onde eu tenho foreach ($categorias as $categoria): onde eu listo o nome das categorias e um foreach ($subcategorias as $subcategoria): onde eu listo o nome das subcategorias. Desta forma que está apresentado está correto mas em compensação eu não vejo nenhuma filtragem já que eu não passei parametros na pages_controller
$this->set(“categorias”,$this->Categoria->find(‘all’,array(‘order’ => ‘Categoria.descricao ASC’,'fields’ => array(‘Categoria.id’, ‘Categoria.descricao’))));
$this->set(“subcategorias”,$this->Categoria->Subcategoria->find(‘all’,array(NULL, ‘fields’ => array(‘Subcategoria.id’, ‘Subcategoria.descricao’),’order’ => array(‘Subcategoria.descricao ASC’))));
Obrigado
-
018
Fabrício Ferracioli
em 18/06/2009 08:49:32Muito estranho, acreditava que o Cake faria as relações sozinho. Você já tentou pedir ajuda no grupo de Cake? Pode ser que você consiga resolver mais rapidamente o problema lá, visto que tem muito mais pessoas para ajudar.
Dessa vez o que eu acabei não entendendo foi o filtro que você deseja realizar. O objetivo não é exibir todas as subcategorias agrupadas por categorias? -
019
Benício
em 18/06/2009 11:54:07Olá Fabrício,
O objetivo é exatamente esse e vou precisar disso em outras partes. Já coloquei o post na comunidade estou esperando ele entrar no ar. Mas de qualquer forma se vc tiver alguma solução pode mandar um e-mail que eu agradeço. Estou usando a pages_controller.php e default.ctp. Abraço
-
020
Benício
em 19/06/2009 11:52:22Olá Fabrício,
Consegui resolver o problema e vou postar aqui pq podem haver pessoas precisando. No controller eu coloquei:
$this->Categoria->recursive = 1;
$this->set(“categorias”,$this->Categoria->find(‘all’,array(‘order’ => ‘descricao ASC’)));Na view
php foreach ($categorias as $categoria):
php foreach($categoria['Subcategoria'] as $subcategoria):
php echo $html->link($subcategoria['descricao'],’#');
php endforeach;
php endforeach; -
021
Fabrício Ferracioli
em 22/06/2009 08:40:55Olá Benício, legal que você consegiu resolver.
Acredito que a principal modificação responsável por isso foi a alteração do nível de recursividade. Confesso que esse é um atributo meio obscuro para mim também.
Vou deixar o link para a thread do grupo CakePHP Tuga se alguém acabar tendo o mesmo problema, lá está mais fácil de ver a solução. -
022
Alex Alves
em 29/09/2009 10:28:32Olá Fabrício,
Gostei muito do seu post, mas antes de implementar gostaria de saber se posso substituir o “prototype” pelo “Jquery”.
No mais agradeço sua atenção desde já.
-
023
Fabrício Ferracioli
em 29/09/2009 12:49:10Olá Alex.
Que bom que você gostou do post.
Infelizmente não tenho certeza se o CakePHP trabalha de maneira “automágica” com o JQuery, isso porque segundo o book do Cake o padrão é o Prototype [1] [2].
De qualquer maneira, encontrei um Helper que pode te ajudar a deixar sua tarefa mais simples, também nunca utilizei, mas espero que sirva para seus propósitos [3].
[1] – Javascript
[2] – Ajax
[3] – JAX do Túlio Faria -
024
Alex Alves
em 29/09/2009 15:32:38Suas explicações ajudaram muito, e sua dica também.
E foi com sua dica em relação ao Jquery e Cake, que achei um site que disponibiliza o Plugin em Ajax.
Ainda não testei, mas estou com uma boa espectativa, pois o mesmo utiliza a mesma sintaxe ao chamar os eventos. No mais espero ter contribuído.Até a próxima e muito obrigado.
-
025
Alex Alves
em 29/09/2009 15:33:20Ahhhh esqueci de postar a fonte…rsrs
Aí vai:
http://blog.loadsys.com/2009/05/01/cakephp-jquery-ajax-helper-easy-scriptaculous-replacement/
-
026
Fabrício Ferracioli
em 29/09/2009 15:59:17Alex, com certeza você contribuiu. É comum ver dúvidas do pessoal que utiliza JQuery com relação ao uso do Prototype no CakePHP, e esse helper que você encontrou certamente vai ajudar essas pessoas.
-
027
Elias Gomes
em 05/12/2009 22:14:41Cara, muito bom. Andei um bocado mas, cheguei aqui e resolvi meus problemas.
Acho que não entendi muito bem. No caso do cadastro, basta utilizar o prototype:Adicionei o script:
window.onload = function()
{
new dgCidadesEstados({
estado: document.getElementById(“estado”),
cidade: document.getElementById(“cidade”)});
}E no Form:
echo $form->input(‘uf’,array(‘type’=>’select’,'id’=>’estado’));
echo $form->input(‘cidade’,array(‘type’=>’select’,'id’=>’cidade’));Mas, e no view/~/edit.ctp, como faço para sincronizar os dados vindos do banco de dados com os mostrados pela implementação js, ou melhor, jogar estes dados nos comboboxs?
Valeu.
-
028
Fabrício Ferracioli
em 15/12/2009 14:00:31Olá Elias, desculpe a demora pra responder mas não recebi um E-mail com o seu comentário. Mistérios do Wordpress…
Não entendi muito bem sua dúvida, mas pelo que parece você deseja que durante a edição o usuário possa ver o estado atual do banco e alterá-lo, certo?
O Cake já deve trazer todos os dados para você durante a edição, não tem segredo nenhum. Ele inclusive deve exibir o campo selecionado pelo usuário durante o cadastro. Não sei se é realmente isso que você precisa, mas no manual ele explica como fazer. Da uma olhada no FormHelper. -
029
Luis Fernando
em 04/02/2010 16:38:35Olá fabrício,
fui fazer esta implementação e deu esse erro:Fatal error: Class ‘Shell’ not found in cake\console\libs\tasks\view.php on line 34
o que sera?
-
030
Fabrício Ferracioli
em 05/02/2010 18:11:37Olá Luis.
Esse erro é bastante estranho, parece não possui muita relação com as sugestões que eu faço no post.
Me parece que seu framework está com um arquivo faltando, pode ter sido deletado ou algo assim.
Faça novamente a instalação do framework e veja se o erro persiste. -
031
dramos
em 18/02/2010 09:45:28Primeiro parabéns pelo tutorial, consegui fazer direitinho com as explicações.
Acontece agora que além disso, em outro formulário eu estou tentando preencher o endereço após receber o cep.
O observeField() não funciona com campo de texto?
Tenho o seguinte:
echo $ajax->observeField(‘CandidatoCep’,
array(
‘update’ => ‘CandidatoEndereco’,
‘url’ => array(‘controller’ => ‘enderecos’,
‘action’ => ‘endereco_cep’)
)
);Sendo que o retorno da action endereco_cep é o nome da rua correspondente ao cep digitado. Mas eu digito o cep e saio do campo e nada acontece.
Para testar eu mudei o campo de cep para um select dai o observeField atualizou a div com o nome da rua.
Selecione
41635685
28635680Mais uma coisa, ainda utilizando o select para o cep, eu mudei a div que recebe o nome da rua para um campo texto, assim:
Mas o resultado não entra no value, como setar que é para atualizar o value?
TRAVESSA ARISTIDES MILTON
Resumo, preciso que os 2 campos sejam input type=text, onde informo o cep e onde mostra o resultado, caso isto não possa ser feito com o observe field, como posso fazê-lo?
Desde já muito obrigado!
-
032
Fabrício Ferracioli
em 18/02/2010 18:20:07Olá, e obrigado pelo elogio.
Tentei reproduzir seu problema e consegui entender o que está acontecendo. O método observeField atualiza o innerHTML do elemento que você definir na option update. Como o innerHTML em inputs não é exibido, pois fica entre as tags de input, aparentemente o método não funciona, apesar de fazer exatamente o que ele propõe.
Solucionei o problema do seguinte modo, na view que recebe os dados do processamento do método assíncrono, utilize um pequeno script que atualize o value do seu campo de input. Com o prototype fica assim:
$('idDoElementoAtualizado').setValue('Valor que eu trouxe do BD');
Sinceramente, não sei se é a solução mais elegante, mas resolve seu problema.
Abraço




