Events in Nette\Forms\Controls\BaseControl
- nanuqcz
- Member | 822
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:
- without any javascripts,
- based on real actual value (dependent input should change when
$form->setDefaults('foo' => ...)
or$form['foo']->setDefaultValue(...)
is called) - 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:
- Doesn't work with
$form->setDefaults()
&$input->setDefaultValue()
. - Doesn't work without javascript.
- 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:
- 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:
- Doesn't work with
$form->setDefaults()
&$input->setDefaultValue()
. - Clears all form's inputs (except main select)
- 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:
- Works without any javascripts. We can also add javascript & AJAX.
- Works with
$form->setDefaults('foo' => ...)
or$form['foo']->setDefaultValue(...)
. - We are able add dependent input in order we want.
Thank you.
Last edited by nanuqcz (2014-01-13 03:18)
- Pavel Kouřil
- Member | 128
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.
- Filip Procházka
- Moderator | 4668
Fuck, forum is messing with me… I saw double post, deleted one and both disapeared… My post is in @Pajka's quote
- nanuqcz
- Member | 822
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.
- Honza Marek
- Member | 1664
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.
- nanuqcz
- Member | 822
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:
- 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.)
- 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.
- JakubJarabica
- Gold Partner | 184
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.
- norbe
- Backer | 405
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
- JakubJarabica
- Gold Partner | 184
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.
- Filip Procházka
- Moderator | 4668
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.
- nanuqcz
- Member | 822
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)
- Honza Marek
- Member | 1664
- 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.
- Maybe the event has wrong name. OnSetValue might be better.
- 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.