Events in Nette\Forms\Controls\BaseControl

5 years ago

nanuqcz
Member | 844
+
0
-

I figured out some tasks is really difficult with actual Nette\Forms.

Model example task: Form's dependent input

We have a form with one select (lets call it foo) and second dependent input (lets call it bar). It can be TextInput , SelectBox, etc. Form also contains submit button, named save.

When foo is empty, we want form to contain no dependent item. When we choose some foo's option, dependent item is added to the form. It can be TextInput, SelectBox, etc., based on foo's value.

We want dependent input to work:

  1. without any javascripts,
  2. based on real actual value (dependent input should change when $form->setDefaults('foo' => ...) or $form['foo']->setDefaultValue(...) is called)
  3. in right order of form elements (we want add dependent input BEFORE save button)

Bad idea solutions

A) Variable in URL

Like described in Planette article.

Problems:

  1. Doesn't work with $form->setDefaults() & $input->setDefaultValue().
  2. Doesn't work without javascript.
  3. Can't dynamically remove / add elements, can only change values of existing elements.
B) Simple “Select” button

Should look like this:

protected function createComponentForm(){
    ...

    $form->addSelect('foo', ...);
    $form->addSubmit('foo_change', 'Select Foo')
        ->onClick[] = callback($this, 'changeFoo');
    if ($form['foo']->value) {
        $form->addSelect('bar', ...);
    }

    ...
}


public function changeFoo(Button $button){
    // nothing here ;-)
}

Problems:

  1. Doesn't work with $form->setDefaults() & $input->setDefaultValue(). It is imposible to use this form as editing form.
C) Session variable

Should look like this:

public function startup(){
    parent::startup();

    $this->formSession = $this->getSession($this->name . '-form');
}


protected function createComponentForm(){
    ...

    $form->addSelect('foo', ...);
    $form->addSubmit('foo_change', 'Select Foo')
        ->onClick[] = callback($this, 'changeFoo');
    if ($this->formSession->foo) {
        $form->addSelect('bar', ...);
    }

    ...
}


public function changeFoo(Button $button){
    $values = $button->form->values;

    $this->formSession->foo = $values->foo;

    $this->redirect('this');
}

Problems:

  1. Doesn't work with $form->setDefaults() & $input->setDefaultValue().
  2. Clears all form's inputs (except main select)
  3. What if I open same form in two different tabs?
D) Own Nette\Forms\Controls\BaseControl implementation

Yes, this is only one possible way in this moment. Not elegant.

Right idea solution (thanks to matej21)

Dependent inputs always depend on its “main input” value. So, we want ability to handle moment, when main input obtain some value.

So, Nette\Forms\Controls\BaseControl would implement events:

abstract class BaseControl extends Nette\ComponentModel\Component implements IControl {
    /** @var array */
    public $onChange;

    ...

    public function setValue($value)
    {
        $this->value = $value;

        $this->onChange($this);

        return $this;
    }
}

And our form with dependent item can be implemented like this:

protected function createComponentForm(){
    ...

    $form->addSelect('foo', ...)
        ->onChange[] = callback($this, 'fooChanged');
    $form->addSubmit('foo_change', 'Select Foo');

    ...
}


public function fooChanged(SelectBox $select){
    $form = $select->form;

    if ($form->values->foo) {
        $bar = new SelectBox('bar', ...);
        $form->addComponent($bar, ..., $form['save']);
    }
}

This solution:

  1. Works without any javascripts. We can also add javascript & AJAX.
  2. Works with $form->setDefaults('foo' => ...) or $form['foo']->setDefaultValue(...).
  3. We are able add dependent input in order we want.

Thank you.

Last edited by nanuqcz (2014-01-13 03:18)

5 years ago

Pavel Kouřil
Member | 128
+
0
-

Filip Procházka wrote:

onChange makes somewhat sense to me, but the usage is wrong. You should never be adding inputs on form events. Prepare your inputs (even if they won't have any items or values) and use toggles to hide them.

Well, there is at least one use case in which adding new inputs in an event is IMHO OK. Basically when you have a button to dynamically add new inputs. Since you don't know how many inputs will you need when creating the form, you cannot prepare them beforehand.

5 years ago

hrach
Member | 1810
+
0
-

Could you explain how it should work without ajax? Some submit should be needed, shouldn't it? How you then prevent onSuccess[] triggering?

Last edited by hrach (2014-01-13 18:52)

5 years ago

Filip Procházka
Moderator | 4693
+
0
-

Fuck, forum is messing with me… I saw double post, deleted one and both disapeared… My post is in @Pajka's quote

5 years ago

nanuqcz
Member | 844
+
0
-

hrach: Yes, submit button is needed for non-javascript usage. I don't see anything wrong with it.

When any form has more than one submit buttons, and we want to separate their actions, we should use $submit->onClick[] callbacks.

Last edited by nanuqcz (2014-01-13 22:55)

5 years ago

hrach
Member | 1810
+
0
-

Personally, I consider onClick as antipattern. Also, UX without JS is quite bad. I'm not against the rfc, but I don't see the proper use-case.

5 years ago

nanuqcz
Member | 844
+
0
-

UX without JS is quite bad

JS can everyone add to the functional, non-JS base.

I consider onClick as antipattern.

Ok, so you can use onSuccess[] callback with condition:

public function formSubmitted(Form $form) {
    if ($form->submitted == $form['save']) {
        // ...
    }
}

But this is IMHO off-topic, not important for this RFC.

5 years ago

norbe
Backer | 402
+
0
-

hrach: I don'ť understand why onClick event should be antipattern, can you explain it?

5 years ago

Honza Marek
Member | 1674
+
0
-

Complicated forms can be easily created if you use some modern javascript framework. If you try to solve these problems with backend framework only, the code would be probably hard to maintain and UX would be awful.

5 years ago

nanuqcz
Member | 844
+
0
-

Honza Marek: I don't agree.

Ok, your solution is: I can “simple” make form with all dependent-items possibilities and then with java-scipt can hide all which I don't want to see. But:

  1. What about default values, dependent select-box items, etc.? When this depends on other item's value and I neeed to select some data from DB, I must implement filling this data into the dependent input at server-side. (model task: When choose article category in first select, then show second select filled with its articles.)
  2. What if I don't know, what and how many items I will need? As described Pajka above. I can't prepare form beforehand when I don't know that.

So, my conclusion is: We need proper way how to change form (at least form input's data – selectbox items etc.) when form values is changed, by server-side. And $item->onChange[] event is simple and nice solution.

5 years ago

JakubJarabica
Gold Partner | 182
+
0
-

I am satisfied with current solution – no need to define all necessary form fields(possibly in template only) and grab them using data types constants. With really complex forms I don't want to trigger multiple onchange events. Just write one complex server side validator(just in case) and let frontend UI handle everything, way shorter solution.

5 years ago

norbe
Backer | 402
+
0
-

JAM3SoN & Honza Marek: Then you have to write everything (validation, enumeration of allowed fields, …) twice, haven't you? If not, can you please show some example? I can't imagine that described solution is shorter and no harder to maintain (but I don't say it's not possible :-))… Thx

5 years ago

JakubJarabica
Gold Partner | 182
+
0
-

Norbe: Basically, yes, but as Nette/server-side developer I do it just once. I don't (want to) care how frontend will look like(and when it'll be ready) – despite I am often frontend dev as well, which I am currently changing :)

When you have complex frontend logic that are we talking about, you can't rely on default validators as they won't work. You have to submit it to server on every blur/change event – for example think of mobile devices and 3G internet with terrible coverage, that will be unusable.

5 years ago

Filip Procházka
Moderator | 4693
+
0
-

Guys, Nette cannot solve everything, it's not a magic box that has a button which can create apps for you from start to the end.

Sometimes when you're writing complicated forms, you have to write parts of it by youself. Be it custom form controls, or custom client-side handling.

If you're not writing application that will be used in corporation that has allowed only IE6, there is nothing wrong with depending on javascript. In fact, it may be wrong not to.

5 years ago

nanuqcz
Member | 844
+
0
-

Filip Procházka wrote:

Sometimes when you're writing complicated forms, you have to write parts of it by youself. Be it custom form controls, or custom client-side handling.

Yes. Either that, or we can add 4 lines of code to Nette's BaseControl.php. And Nette Forms will be a little more robust again, including dynamic forms, dependent select-boxes etc.

Last edited by nanuqcz (2014-01-15 16:47)

5 years ago

Filip Procházka
Moderator | 4693
+
0
-

And slower.

5 years ago

Honza Marek
Member | 1674
+
0
-
  1. It is absolutely no problem to create inherited class where you can implement the setValue method as you wish. So there is no need to implement onCreate event directly in Nette.
  2. Maybe the event has wrong name. OnSetValue might be better.
  3. Assuming that you really want to run this event every time the value is changed, there is no way how to do it properly:
    • If you change an input value in browser, server side will not know about it. Of course AJAX request can be sent, but this is useless if you want to change values of dependent select box or redraw web page in any other way.
    • If you submit the form, nette will not know if the value has been changed since last time. HTTP is stateless and previous value is not remembered anywhere.