Dependent form select with AJAX

This is case you want to create form select, which values depend on another select value. Simple use case: select country, than select city from the country, than select street from the city.

Requires jQuery 1.8.2 and nette.ajax.js addon, built on Nette 2.0.10 sandbox.

Single dependent select

Logic is very simple: when first select is changed a handle* method is called by AJAX in background (in this case handleFirstSelect($value)). This method specifies values for second select, loads select input with them and invalidates snippet.

If you don't know, what snippet or Nette Ajax is, I recommend you to check Quickstart.

HomepagePresenter.php

use Nette\Application\UI\Form;

class HomepagePresenter extends BasePresenter
{

    /**
     * Load values to second select
     * @param int
     */
    public function handleFirstChange($value)
    {
        if ($value) {
            $secondItems = [
                1 => 'First option ' . $value . ' - second option 1',
                2 => 'First option ' . $value . ' - second option 2'
            ];

            $this['selectForm']['second']->setPrompt('Select')
                ->setItems($secondItems);

        } else {
            $this['selectForm']['second']->setPrompt('Select from first')
                ->setItems([]);
        }

        $this->redrawControl('wrapper');
        $this->redrawControl('secondSnippet');
    }


    /********************** form **********************/


    protected function createComponentSelectForm()
    {
        $firstItems = [
            1 => 'First option 1',
            2 => 'First option 2'
        ];

        $form = new Form;
        $form->addSelect('first', 'First select:', $firstItems)
            ->setPrompt('Select');
        $form->addSelect('second', 'Second select:')
            ->setPrompt('Select from first');
        $form->addSubmit('send', 'Submit');

        $form->onSuccess[] = [$this, 'processSelectForm'];

        return $form;
    }


    /**
     * @param form
     */
    public function processSelectForm(Form $form)
    {
        // $form->getValues() ignores invalidated input's values
        $values = $form->getHttpData();
        unset($values['send']);
        dump($values);

        // ...
    }

}

Homepage/default.latte

{define content}

{snippetArea wrapper}
    {form selectForm}
        {label first /} {input first}
        {snippet secondSnippet}
            {label second /} {input second}
        {/snippet}
        {input send}
    {/form}
{/snippetArea}

{include jsCallback, input => first, link => firstChange}

{/define}


{define jsCallback}
<script>

$('#' + {$control["selectForm"][$input]->htmlId}).off('change').on('change', function () {
    $.nette.ajax({
        type: 'GET',
        url: {link {$link}!},
        data: {
            'value': $(this).val(),
        }
    });
});

</script>
{/define}

Multiple dependent selects

HomepagePresenter.php

use Nette\Application\UI\Form;

class HomepagePresenter extends BasePresenter
{

    public function renderDefault()
    {
        $this->template->_form = $this['selectForm'];
    }


    /**
     * Load values to second select
     * @param int
     */
    public function handleFirstChange($value)
    {
        if ($value) {
            $secondItems = [
                1 => 'First option ' . $value . ' - second option 1',
                2 => 'First option ' . $value . ' - second option 2'
            ];

            $this['selectForm']['second']->setPrompt('Select')
                ->setItems($secondItems);

            $this['selectForm']['third']->setPrompt('Select from second');

        } else {
            $this['selectForm']['second']->setPrompt('Select from first')
                ->setItems([]);

            $this['selectForm']['third']->setPrompt('Select from first')
                ->setItems([]);
        }

        $this->redrawControl('wrapper');
        $this->redrawControl('secondSnippet');
    }


    /**
     * Load values to third select
     * @param int
     */
    public function handleSecondChange($value)
    {
        if ($value) {
            $thirdItems = [
                1 => 'Second option ' . $value . ' - third option 1',
                2 => 'Second option ' . $value . ' - third option 2'
            ];

            $this['selectForm']['third']->setPrompt('Select')
                ->setItems($thirdItems);

        } else {
            $this['selectForm']['third']->setPrompt('Select from second')
                ->setItems([]);
        }

        $this->redrawControl('wrapper');
        $this->redrawControl('thirdSnippet');
    }


    /********************** form **********************/


    protected function createComponentSelectForm()
    {
        $firstItems = [
            1 => 'First option 1',
            2 => 'First option 2'
        ];

        $form = new Form;
        $form->addSelect('first', 'First select:', $firstItems)
            ->setPrompt('Select');
        $form->addSelect('second', 'Second select:')
            ->setPrompt('Select from first');
        $form->addSelect('third', 'Third select:')
            ->setPrompt('Select from first');
        $form->addSubmit('send', 'Submit');

        $form->onSuccess[] = [$this, 'processSelectForm'];

        return $form;
    }


    /**
     * @param form
     */
    public function processSelectForm(Form $form)
    {
        // same as above
    }

}

Homepage/default.latte

To keep ajax alive for all snippets, we have to place it into last changing one (in this case into {snippet secondSnippet}).

{define content}

{snippetArea wrapper}
    {form selectForm}
        {label first /} {input first}
        {snippet secondSnippet}
            {label second /} {input second}

            {snippet thirdSnippet}
                {label third /} {input third}
            {/snippet}

            {include js}
        {/snippet}
        {input send}
    {/form}
{/snippetArea}

{/define}


{define js}

{include jsCallback, input => first, link => firstChange}
{include jsCallback, input => second, link => secondChange}

{/define}


{define jsCallback}
{* same as above *}
{/define}