Skip to content

Commit

Permalink
Merge pull request #177 from atk4/feature/add-form-validation
Browse files Browse the repository at this point in the history
Reimplemented Form Validation
  • Loading branch information
romaninsh authored Jun 19, 2017
2 parents 3a0bb28 + fe077c3 commit 2950217
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 45 deletions.
30 changes: 30 additions & 0 deletions demos/database.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,41 @@ public function init()
{
parent::init();
$this->addField('name', ['actual'=>'nicename', 'required'=>true, 'type'=>'string']);
$this->addField('sys_name', ['actual'=>'name', 'system'=>true]);

$this->addField('iso', ['caption'=>'ISO', 'required'=>true, 'type'=>'string']);
$this->addField('iso3', ['caption'=>'ISO3', 'required'=>true, 'type'=>'string']);
$this->addField('numcode', ['caption'=>'ISO Numeric Code', 'type'=>'number', 'required'=>true]);
$this->addField('phonecode', ['caption'=>'Phone Prefix', 'type'=>'number']);

$this->addHook('beforeSave', function ($m) {
if (!$m['sys_name']) {
$m['sys_name'] = strtoupper($m['name']);
}
});
}

public function validate()
{
$errors = parent::validate();

if (strlen($this['iso']) !== 2) {
$errors['iso'] = 'Must be exactly 2 characters';
}

if (strlen($this['iso3']) !== 3) {
$errors['iso3'] = 'Must be exactly 3 characters';
}

// look if name is unique
$c = clone $this;
$c->unload();
$c->tryLoadBy('name', $this['name']);
if ($c->loaded() && $c->id != $this->id) {
$errors['name'] = 'Country name must be unique';
}

return $errors;
}
}

Expand Down
45 changes: 39 additions & 6 deletions demos/form2.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@

// create form
$form = $layout->add(new \atk4\ui\Form(['segment']));
$form->add(['Label', 'Input new country information here', 'top attached'], 'AboveFields');

$form->setModel(new Country($db), false)->loadBy('iso', 'GB');
$form->setModel(new Country($db), false);

// form basic field group
$f_address = $form->addGroup('Basic Country Information');
$f_address->addField('name', ['width'=>'ten'])
$f_address->addField('name', ['width'=>'sixteen'])
->addAction(['Check Duplicate', 'iconRight'=>'search'])
->on('click', function ($val) {
// We can't get the value until https://github.com/atk4/ui/issues/77
Expand All @@ -24,17 +25,21 @@

// form codes field group
$f_codes = $form->addGroup(['Codes']);
$f_codes->addField('iso', ['Post Code', 'width'=>'three'])->iconLeft = 'flag';
$f_codes->addField('iso3', ['Post Code', 'width'=>'three'])->iconLeft = 'flag';
$f_codes->addField('iso', ['width'=>'four'])->iconLeft = 'flag';
$f_codes->addField('iso3', ['width'=>'four'])->iconLeft = 'flag';
$f_codes->addField('numcode', ['width'=>'four'])->iconLeft = 'flag';
$f_codes->addField('phonecode', ['width'=>'four'])->iconLeft = 'flag';

// form names field group
$f_names = $form->addGroup(['Client name', 'inline'=>true]);
$f_names = $form->addGroup(['More Information about you']);
$f_names->addField('first_name', ['width'=>'eight']);
$f_names->addField('middle_name', ['width'=>'three', 'disabled'=>true]); // 'disabled' attribute doesn't work here (bug?)
$f_names->addField('middle_name', ['width'=>'three']);
$f_names->addField('last_name', ['width'=>'five']);

// form on submit
$form->onSubmit(function ($f) {

// In-form validation
$errors = [];
if (strlen($f->model['first_name']) < 3) {
$errors[] = $f->error('first_name', 'too short, '.$f->model['first_name']);
Expand All @@ -50,10 +55,38 @@
return $errors;
}

// Model will have some validation too
$f->model->save();

return $f->success(
'Record Added',
'there are now '.$f->model->action('count')->getOne().' records in DB'
);
});

class Person extends \atk4\data\Model
{
public $table = 'person';

public function init()
{
parent::init();
$this->addField('name', ['required'=>true]);
$this->addField('surname');
$this->addField('gender', ['enum' => ['M', 'F']]);
}

public function validate()
{
$errors = parent::validate();

if ($this['name'] == $this['surname']) {
$errors['surname'] = 'Your surname cannot be same as the name';
}

return $errors;
}
}

$app->layout->add('Form')
->setModel(new Person($db));
107 changes: 76 additions & 31 deletions docs/form.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,40 @@ Forms
.. php:class:: Form
One of the most important views of Agile UI is a "Form" View, which will appear in your application just like this:
One of the most important components of Agile UI is the "Form" - a View:

.. image:: images/form.png

Features of a Form include:

- Rendering a valid HTML form:
- including form fields
- Rendering a beautiful and valid form:
- wide range of supported field types
- field grouping (more than one field per line)
- adds and links labels, placeholders and hints
- allows any "Field" variations (see :ref:`field`) - checkboxes, drop-downs, etc.
- automatically sets ID for form/fields/labels
- define field width, positioning and size
- labels, placeholders and hints
- supports automated layouts or you can define a custom one

- Integration with Model objects:
- automatically populate all or specific fields
- handle multi-field validation
- use of semi-automated layouting (you can arrange group of fields)
- respect caption and other ui-related settings defined in a model
- map field "type" into appropriate :ref:`field`
- lookup referenced models for data
- data types are converted automatically (e.g. date, time, boolean)

- Augment Form with JS integration:
- JavaScript integration
- form is submitted using JavaScript
- during submit, the loading indicator is shown
- javascript sends data through POST
- POST data is automatically parsed and imported into Model

- You may define onSubmit PHP handler that:
- can execute data validation, save data and make decisions in PHP code
- display errors for single or multiple fields
- perform custom actions on "input" element such as insert value
- perform custom action on "field" div, such as use checkbox APIs
- execute any other arbitrary JavaScript or jQuery code
- can handle more validation
- make advanced decisions before saving data
- perform a different Actions, such as reload parts of page or close dialog.
- save data into multiple models
- indicate successful completion of a form through a nicely formatted message
- anything else really!


Creating Basic Forms
Expand All @@ -53,26 +54,59 @@ To create a form you need the following code::
$form = new \atk4\ui\Form();
$form->addField('email');

$layout->add($form);
$app->layout->add($form);

The first line creates a "Form" object that is assigned to variable `$f`. Next
line defines a new field that is placed inside a form.
line defines a new field that is placed inside a form. Once form is defined, it
needs to be placed somewhere in a Render Tree, so that the users can see it.

Once form is defined, it needs to be placed somewhere in a Render Tree.
If $form is not yet associated with a model (like above) then an empty model will
be created. If you are not using the model, you will need to define
:php:meth:`onSubmit()` handler. (Your other option is to use :php:meth:`setModel()`.

.. php:method:: addField(name)
Adding Fields to a form
^^^^^^^^^^^^^^^^^^^^^^^

Create a new field on a form. If form is associated with a model, which have
a field with a matching name, then field meta-information will be loaded from
the model.
.. php:method:: addField(data_field, form_field = null)
If $form is not yet associated with a model (like above) then an empty model will
be created. The arguments to addField are compatible with Model::addField()::
Create a new field on a form. The first argument is a data field definition.
This can simply be a string "email". Additionally you can specify an array or
even a instance of
`\atk4\data\Field <http://agile-data.readthedocs.io/en/develop/fields.html>`_

If form is associated with a model, and the specified field exists, then
Data Field object will be looked up. Data Field defines type, caption, possible
values and other information about the data itself.

Second argument can be used to describe area around the field, which is a visual
object derived from :php:class:`Form::Field`. The class usually is guessed
from the data field type, but you can specify your own object here. Alternatively
you can pass array which will be used as defaults when creating appropriate
Form Field.

Here are some of the examples::

// Data field type decides form field class
$form->addField(['is_accept_terms', 'type'=>'boolean']);

// Specifying enum makes form use drop-down
$form->addField(['agree', 'enum'=>['Yes', 'No']]);

// We can switch to use Radio selection
$form->addField(['agree', 'enum'=>['Yes', 'No']], new \atk4\ui\FormField\Radio());

$form->addField('is_accept_terms', ['type'=>'boolean']);
.. important:: Always use `'type'=>` because this also takes care of
`type-casting <http://agile-data.readthedocs.io/en/develop/typecasting.html>`_
e.g. converting data formats.

Integrating Form with a Model
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

As you work on your application, in most cases you will be linking Form with
`Model <http://agile-data.readthedocs.io/en/develop/model.html>`. This is much
more convenient and takes care of handling data flow all the way from the user
input to storing them in the database.

Additionally, any fields that are added into Form directly are marked with the
flag "never_persist".

.. php:method:: setModel($model, [$fields])
Expand All @@ -88,7 +122,7 @@ flag "never_persist".
Model that is currently associated with a Form.

For the next demo, lets actually define a model::
For the next demo, lets actually define a model `Person`::

class Person extends \atk4\data\Model
{
Expand All @@ -97,18 +131,29 @@ For the next demo, lets actually define a model::
public function init()
{
parent::init();
$this->addField('name');
$this->addField('name', ['required'=>true]);
$this->addField('surname');
$this->addField('gender', ['enum' => ['M', 'F']]);
}

public function validate()
{
$errors = parent::validate();

if ($this['name'] == $this['surname']) {
$errors['surname'] = 'Your surname cannot be same as the name';
}

return $errors;
}
}

We can now populate form fields based around the fields as they are defined inside
a model. I will also add one extra checkbox where user can accept terms and conditions::
We can now populate form fields based around the data fields defined in the model::

$form = $layout->add('Form'); // using short version
$app->layout->add('Form')
->setModel(new Person($db));

$form->setModel(new Person($db));
This should display a following form:

$form->addField(
'terms',
Expand Down
29 changes: 23 additions & 6 deletions src/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public function init()
$this->initLayout();

// When form is submitted, will perform POST field loading.
$this->addHook('submit', [$this, 'loadPOST']);
/*
$this->addHook('submit', function () {
// Field validation
Expand Down Expand Up @@ -101,6 +101,7 @@ public function init()
return $return;
}
});
*/
}

/**
Expand Down Expand Up @@ -401,12 +402,12 @@ public function loadPOST()

$this->model[$key] = $this->app->ui_persistence->typecastLoadField($field->field, $value);
} catch (\atk4\core\Exception $e) {
$errors[] = $this->error($key, $e->getMessage());
$errors[$key] = $e->getMessage();
}
}

if ($errors) {
$this->breakHook($errors);
throw new \atk4\data\ValidationException($errors);
}
}

Expand All @@ -430,9 +431,25 @@ public function ajaxSubmit()
->setAttr('type', 'hidden');

$cb->set(function () {
$response = $this->hook('submit');
if (!$response) {
return new jsExpression('console.log([])', ['Form submission is not handled']);
try {
$this->loadPOST();
$response = $this->hook('submit');
if (!$response) {
if (!$this->model instanceof \atk4\ui\misc\ProxyModel) {
$this->model->save();

return $this->success('Form data has been saved');
} else {
return new jsExpression('console.log([])', ['Form submission is not handled']);
}
}
} catch (\atk4\data\ValidationException $val) {
$response = [];
foreach ($val->errors as $field=>$error) {
$response[] = $this->error($field, $error);
}

return $response;
}

return $response;
Expand Down
5 changes: 3 additions & 2 deletions template/semantic-ui/form.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@

<form id="{$_id}" class="{_ui}ui{/} {$class} {_class}form{/}" style="{$style}" {$attributes}>{$Content}</form>
<form id="{$_id}" class="{_ui}ui{/} {$class} {_class}form{/}" style="{$style}" {$attributes}>
{$AboveFields}
{$Content}</form>
1 change: 1 addition & 0 deletions template/semantic-ui/form.pug
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<form id="{$_id}" class="{_ui}ui{/} {$class} {_class}form{/}" style="{$style}" {$attributes}>
| {$AboveFields}
| {$Content}
</form>

0 comments on commit 2950217

Please sign in to comment.