Skip to content

Commit

Permalink
Merge pull request #11837 from jeremyharris/html5-error-messages
Browse files Browse the repository at this point in the history
[RFC] HTML5 validation error messages
  • Loading branch information
markstory committed Jun 6, 2018
2 parents b12ac33 + 468280b commit bf70253
Show file tree
Hide file tree
Showing 13 changed files with 494 additions and 21 deletions.
67 changes: 53 additions & 14 deletions src/Validation/Validator.php
Expand Up @@ -104,24 +104,14 @@ public function errors(array $data, $newRecord = true)
{
$errors = [];

$requiredMessage = 'This field is required';
$emptyMessage = 'This field cannot be left empty';

if ($this->_useI18n) {
$requiredMessage = __d('cake', 'This field is required');
$emptyMessage = __d('cake', 'This field cannot be left empty');
}

foreach ($this->_fields as $name => $field) {
$keyPresent = array_key_exists($name, $data);

$providers = $this->_providers;
$context = compact('data', 'newRecord', 'field', 'providers');

if (!$keyPresent && !$this->_checkPresence($field, $context)) {
$errors[$name]['_required'] = isset($this->_presenceMessages[$name])
? $this->_presenceMessages[$name]
: $requiredMessage;
$errors[$name]['_required'] = $this->getRequiredMessage($name);
continue;
}
if (!$keyPresent) {
Expand All @@ -132,9 +122,7 @@ public function errors(array $data, $newRecord = true)
$isEmpty = $this->_fieldIsEmpty($data[$name]);

if (!$canBeEmpty && $isEmpty) {
$errors[$name]['_empty'] = isset($this->_allowEmptyMessages[$name])
? $this->_allowEmptyMessages[$name]
: $emptyMessage;
$errors[$name]['_empty'] = $this->getNotEmptyMessage($name);
continue;
}

Expand Down Expand Up @@ -1956,6 +1944,57 @@ public function regex($field, $regex, $message = null, $when = null)
]);
}

/**
* Gets the required message for a field
*
* @param string $field Field name
* @return string|null
*/
public function getRequiredMessage($field)
{
if (!isset($this->_fields[$field])) {
return null;
}

$defaultMessage = 'This field is required';
if ($this->_useI18n) {
$defaultMessage = __d('cake', 'This field is required');
}

return isset($this->_presenceMessages[$field])
? $this->_presenceMessages[$field]
: $defaultMessage;
}

/**
* Gets the notEmpty message for a field
*
* @param string $field Field name
* @return string|null
*/
public function getNotEmptyMessage($field)
{
if (!isset($this->_fields[$field])) {
return null;
}

$defaultMessage = 'This field cannot be left empty';
if ($this->_useI18n) {
$defaultMessage = __d('cake', 'This field cannot be left empty');
}

$notBlankMessage = null;
foreach ($this->_fields[$field] as $rule) {
if ($rule->get('rule') === 'notBlank' && $rule->get('message')) {
return $rule->get('message');
}
}

return isset($this->_allowEmptyMessages[$field])
? $this->_allowEmptyMessages[$field]
: $defaultMessage;
}

/**
* Returns false if any validation for the passed rule set should be stopped
* due to the field missing in the data array
Expand Down
31 changes: 27 additions & 4 deletions src/View/Form/ArrayContext.php
Expand Up @@ -16,6 +16,7 @@

use Cake\Http\ServerRequest;
use Cake\Utility\Hash;
use Cake\Validation\Validator;

/**
* Provides a basic array based context provider for FormHelper.
Expand All @@ -29,7 +30,8 @@
* will be used when there is no request data set. Data should be nested following
* the dot separated paths you access your fields with.
* - `required` A nested array of fields, relationships and boolean
* flags to indicate a field is required.
* flags to indicate a field is required. The value can also be a string to be used
* as the required error message
* - `schema` An array of data that emulate the column structures that
* Cake\Database\Schema\Schema uses. This array allows you to control
* the inferred type for fields and allows auto generation of attributes
Expand All @@ -53,7 +55,12 @@
* 'defaults' => [
* 'id' => 1,
* 'title' => 'First post!',
* ]
* ],
* 'required' => [
* 'id' => true, // will use default required message
* 'title' => 'Please enter a title',
* 'body' => false,
* ],
* ];
* ```
*/
Expand Down Expand Up @@ -194,16 +201,32 @@ public function val($field, $options = [])
* @return bool
*/
public function isRequired($field)
{
return (bool)$this->getRequiredMessage($field);
}

/**
* {@inheritDoc}
*/
public function getRequiredMessage($field)
{
if (!is_array($this->_context['required'])) {
return false;
return null;
}
$required = Hash::get($this->_context['required'], $field);
if ($required === null) {
$required = Hash::get($this->_context['required'], $this->stripNesting($field));
}

return (bool)$required;
if ($required === false) {
return null;
}

if ($required === true) {
$required = __d('cake', 'This field is required');
}

return $required;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/View/Form/ContextInterface.php
Expand Up @@ -16,6 +16,8 @@

/**
* Interface for FormHelper context implementations.
*
* @method string|null getRequiredMessage($field) Gets the default "required" error message for a field
*/
interface ContextInterface
{
Expand Down
28 changes: 28 additions & 0 deletions src/View/Form/EntityContext.php
Expand Up @@ -426,6 +426,34 @@ public function isRequired($field)
return false;
}

/**
* {@inheritDoc}
*/
public function getRequiredMessage($field)
{
$parts = explode('.', $field);

$validator = $this->_getValidator($parts);
$fieldName = array_pop($parts);
if (!$validator->hasField($fieldName)) {
return null;
}

$ruleset = $validator->field($fieldName);

$requiredMessage = $validator->getRequiredMessage($fieldName);
$emptyMessage = $validator->getNotEmptyMessage($fieldName);

if ($ruleset->isPresenceRequired() && $requiredMessage) {
return $requiredMessage;
}
if (!$ruleset->isEmptyAllowed() && $emptyMessage) {
return $emptyMessage;
}

return null;
}

/**
* Get the field names from the top level entity.
*
Expand Down
28 changes: 28 additions & 0 deletions src/View/Form/FormContext.php
Expand Up @@ -139,6 +139,34 @@ public function isRequired($field)
return false;
}

/**
* {@inheritDoc}
*/
public function getRequiredMessage($field)
{
$parts = explode('.', $field);

$validator = $this->_form->getValidator();
$fieldName = array_pop($parts);
if (!$validator->hasField($fieldName)) {
return null;
}

$ruleset = $validator->field($fieldName);

$requiredMessage = $validator->getRequiredMessage($fieldName);
$emptyMessage = $validator->getNotEmptyMessage($fieldName);

if ($ruleset->isPresenceRequired() && $requiredMessage) {
return $requiredMessage;
}
if (!$ruleset->isEmptyAllowed() && $emptyMessage) {
return $emptyMessage;
}

return null;
}

/**
* {@inheritDoc}
*/
Expand Down
8 changes: 8 additions & 0 deletions src/View/Form/NullContext.php
Expand Up @@ -83,6 +83,14 @@ public function isRequired($field)
return false;
}

/**
* {@inheritDoc}
*/
public function getRequiredMessage($field)
{
return null;
}

/**
* {@inheritDoc}
*/
Expand Down
22 changes: 21 additions & 1 deletion src/View/Helper/FormHelper.php
Expand Up @@ -165,7 +165,9 @@ class FormHelper extends Helper
'submitContainer' => '<div class="submit">{{content}}</div>',
//Confirm javascript template for postLink()
'confirmJs' => '{{confirm}}',
]
],
// set HTML5 validation message to custom required/empty messages
'autoSetCustomValidity' => false,
];

/**
Expand Down Expand Up @@ -1443,10 +1445,28 @@ protected function _magicOptions($fieldName, $options, $allowOverride)
{
$context = $this->_getContext();

$options += [
'templateVars' => []
];

if (!isset($options['required']) && $options['type'] !== 'hidden') {
$options['required'] = $context->isRequired($fieldName);
}

if (method_exists($context, 'getRequiredMessage')) {
$message = $context->getRequiredMessage($fieldName);
$message = h($message);

if ($options['required'] && $message) {
$options['templateVars']['customValidityMessage'] = $message;

if ($this->getConfig('autoSetCustomValidity')) {
$options['oninvalid'] = "this.setCustomValidity('$message')";
$options['onvalid'] = "this.setCustomValidity('')";
}
}
}

$type = $context->type($fieldName);
$fieldDef = $context->attributes($fieldName);

Expand Down
63 changes: 62 additions & 1 deletion tests/TestCase/ORM/RulesCheckerIntegrationTest.php
Expand Up @@ -32,7 +32,8 @@ class RulesCheckerIntegrationTest extends TestCase
*/
public $fixtures = [
'core.articles', 'core.articles_tags', 'core.authors', 'core.tags',
'core.special_tags', 'core.categories', 'core.site_articles', 'core.site_authors'
'core.special_tags', 'core.categories', 'core.site_articles', 'core.site_authors',
'core.comments',
];

/**
Expand Down Expand Up @@ -652,6 +653,66 @@ public function testExistsInInvalidAssociation()
$table->save($entity);
}

/**
* Tests existsIn does not prevent new entities from saving if parent entity is new
*
* @return void
*/
public function testExistsInHasManyNewEntities()
{
$table = $this->getTableLocator()->get('Articles');
$table->hasMany('Comments');
$table->Comments->belongsTo('Articles');

$rules = $table->Comments->rulesChecker();
$rules->add($rules->existsIn(['article_id'], $table));

$article = $table->newEntity([
'title' => 'new article',
'comments' => [
$table->Comments->newEntity([
'user_id' => 1,
'comment' => 'comment 1',
]),
$table->Comments->newEntity([
'user_id' => 1,
'comment' => 'comment 2',
]),
]
]);

$this->assertNotFalse($table->save($article));
}

/**
* Tests existsIn does not prevent new entities from saving if parent entity is new,
* getting the parent entity from the association
*
* @return void
*/
public function testExistsInHasManyNewEntitiesViaAssociation()
{
$table = $this->getTableLocator()->get('Articles');
$table->hasMany('Comments');
$table->Comments->belongsTo('Articles');

$rules = $table->Comments->rulesChecker();
$rules->add($rules->existsIn(['article_id'], 'Articles'));

$article = $table->newEntity([
'title' => 'test',
]);

$article->comments = [
$table->Comments->newEntity([
'user_id' => 1,
'comment' => 'test',
])
];

$this->assertNotFalse($table->save($article));
}

/**
* Tests the checkRules save option
*
Expand Down

0 comments on commit bf70253

Please sign in to comment.