Skip to content

Commit

Permalink
Merge pull request #22 from peckadesign/nette-optional-v3
Browse files Browse the repository at this point in the history
Nette optional v3
  • Loading branch information
klobinoid authored Oct 11, 2019
2 parents 6f52046 + feb8ddf commit 0444650
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 18 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[![Build Status](https://travis-ci.com/peckadesign/pdForms.svg?branch=master)](https://travis-ci.com/peckadesign/pdForms)
[![Licence](https://img.shields.io/packagist/l/pd/forms.svg?style=flat-square)](https://packagist.org/packages/pd/forms)

# Pd\Form\Rules + pdForms.js
Knihovna poskytuje nástroje, pomocí kterých je možné zaregistrovat vlastní validační pravidla do Nette\Forms a navíc poskytuje podporu pro live, měkkou a ajaxovou validaci, které lze zaregistrovat v PHP kódu. Řešení vychází z nativní podpory Nette pro custom validační pravidla https://pla.nette.org/cs/vlastni-validacni-pravidla, ale nespoléhá ani nekopíruje interní quirks Nette frameworku.
Expand All @@ -7,3 +8,10 @@ Knihovna poskytuje nástroje, pomocí kterých je možné zaregistrovat vlastní

- [Jak přidávat validace na formulářích](doc/index.md)
- [Changelog](https://github.com/peckadesign/pdForms/releases)

## Verze

Version | Branch | Nette | PHP |
---------|----------|--------|---------|
`^3.0.0` | `master` | `2.4` | `>=7.1` |
`^2.0.0` | `v2.0` | `2.3` | `>=7.1` |
1 change: 1 addition & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ Knihovna poskytuje nástroje, pomocí kterých je možné zaregistrovat vlastní
- [Vlastní pravidla, měkká validace a validační kontext](custom_rules.md)
- [Je libo AJAX?](ajax.md)
- [AJAX s JS callbackem, závilost na více formulářových polích](ajax_dependent_inputs.md)
- [Měkká validace pomocí dostupných Nette validátorů](nette_optional.md)
39 changes: 39 additions & 0 deletions doc/nette_optional.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
## Měkká validace pomocí dostupných Nette validátorů

Nette má velkou sadu validačních pravidel pokrývající obecné validace vstupů. V případě, že byste chtěli některý z nich použít, museli byste si ho volat na frontendu, ale backend o této validaci vůbec neví. Když k formuláři přijde backend programátor, tak bude koukat, kde se ta validace bere. Navíc byste si museli dopsat frontend obsluhu pro konkrétní prvek a volat si Nette validátor. Celé to jde hezky zobecnit a vzájemně propojit. `pd/forms` poskytuje jednoduchý mechanismus jak toho docílit - obecné pravidlo `\Pd\Forms\Rules::NETTE_RULE_PROXY`.

Obecné pravidlo `\Pd\Forms\Rules::NETTE_RULE_PROXY` navážeme na požadovaný formulářový prvek - to zajistí zpracování (vlastně nezpracování na backendu) a předá potřebná nastavení pro frontendové zpracování. Pravidlo nakonfigurujeme pomocí `Pd\Form\RuleOptions`, které vytvoříme z dostupné továrny metodou `createNetteOptional()`. Ta má dva parametry - Nette pravidlo a argument(y) pro toto pravidlo.

```php
<?php declare(strict_types = 1);

class FormFactory
{
/**
* @var \Pd\Forms\RuleOptionsFactory
*/
private $ruleOptionsFactory;

public function __construct(\Pd\Forms\RuleOptionsFactory $ruleOptionsFactory)
{
$this->ruleOptionsFactory = $ruleOptionsFactory;
}

public function create(): \Nette\Forms\Form
{
$form = new \Nette\Forms\Form;

$companyIdentifierOptions = $this->ruleOptionsFactory->createNetteOptional(
\Nette\Forms\Form::PATTERN,
'([0-9]\s*){0,8}'
);

$form->addText('companyIdentifier', 'IČO')
->addCondition(\Nette\Forms\Form::FILLED)
->addRule(\Pd\Forms\Rules::NETTE_RULE_PROXY, '_msg_ico_pattern', $companyIdentifierOptions)
;
}
}
```

Na frontendu obsluha pd/forms zavolá validátor `pattern` z `netteForms.js` a provede validaci formulářového prvku a výsledek oznámí uživateli. Při odeslání formuláře nedochází k backendové validaci, protože o tu nemáme zájem. Chceme pouze uživatele informovat, že je IČO není validaní, ale necháme ho např. dokončit objednávku, abychom nepřišli o konverzi.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "pd-forms",
"title": "pdForms",
"description": "Customization of netteForms for use in PeckaDesign.",
"version": "3.0.0",
"version": "3.0.1",
"author": "PeckaDesign, s.r.o <support@peckadesign.cz>",
"contributors": [
"Radek Šerý <radek.sery@peckadesign.cz>",
Expand Down
23 changes: 23 additions & 0 deletions src/RuleOptionsFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

final class RuleOptionsFactory
{
private const NETTE_RULE = 'netteRule';
private const NETTE_RULE_ARGS = 'netteRuleArgs';

/**
* @var \Nette\Localization\ITranslator
*/
Expand All @@ -27,4 +30,24 @@ public function createRequired(): \Pd\Forms\RuleOptions
{
return new \Pd\Forms\RuleOptions($this->translator, FALSE);
}


public function createNetteOptional(string $netteRule, $ruleArguments = NULL): \Pd\Forms\RuleOptions
{
$options = new \Pd\Forms\RuleOptions($this->translator, TRUE);

$options->addContext(self::NETTE_RULE, $netteRule);

if ($ruleArguments !== NULL) {
if ( ! \is_scalar($ruleArguments) && ! \is_array($ruleArguments)) {
throw new \InvalidArgumentException(
\sprintf('Arguments for optional Nette rule must be scalar or array, %s given', \gettype($ruleArguments))
);
}

$options->addContext(self::NETTE_RULE_ARGS, $ruleArguments);
}

return $options;
}
}
13 changes: 12 additions & 1 deletion src/Rules.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
final class Rules
{
public const AJAX = self::class . '::ajax';
public const NETTE_RULE_PROXY = self::class . '::netteRuleProxy';

public const PHONE = self::class . '::phone';
public const CONTAINS_NUMBER = self::class . '::containsNumber';
public const NO_EXTERNAL_SOURCES = self::class . '::noExternalSources';
Expand All @@ -30,7 +32,7 @@ public static function ajax(\Nette\Forms\IControl $control, \Pd\Forms\RuleOption
->validateInput($control->getValue(), $options->getNormalizedDependentInputs());

if ($validationResult->getStatus() === \Pd\Forms\RuleOptions::STATUS_TIMEOUT) {
return TRUE; // externí služba není dostupná, formulář musí projít
return TRUE;
}

if ( ! $validationResult->isValid()) {
Expand All @@ -48,6 +50,15 @@ public static function ajax(\Nette\Forms\IControl $control, \Pd\Forms\RuleOption
}


/**
* Rule is never validated on backend, used for proxying nette rules as optional only
*/
public static function netteRuleProxy(\Nette\Forms\IControl $control, \Pd\Forms\RuleOptions $options): bool
{
return TRUE;
}


public static function phone(\Nette\Forms\IControl $control, ?\Pd\Forms\RuleOptions $options): bool
{
if ($options !== NULL && $options->isOptional()) {
Expand Down
34 changes: 18 additions & 16 deletions src/assets/pdForms.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @name pdForms
* @author Radek Šerý <radek.sery@peckadesign.cz>
* @version 3.0.0
* @version 3.0.1
*
* Features:
* - live validation
Expand Down Expand Up @@ -45,7 +45,7 @@

var pdForms = window.pdForms || {};

pdForms.version = '3.0.0';
pdForms.version = '3.0.1';


/**
Expand Down Expand Up @@ -156,14 +156,20 @@

validate.forEach(function(elem) {
if (elem.getAttribute('data-pdforms-ever-focused')) {
// validate control using nette-rules && pd-rules (which are inside nette-rules actually)
var ret = Nette.validateControl(elem);
// assumes the input is valid, therefore removing all messages except those associated with ajax rules; this
// prevents flashing of message, when ajax rule is evaluated - ajax rules removes their messages when the ajax
// rule is evaluated
pdForms.removeMessages(elem, false);

var rules = Nette.parseJSON(elem.getAttribute('data-nette-rules'));
rules = pdForms.normalizeRules(rules);

// validate control using nette-rules && pd-rules (which are inside nette-rules actually)
var valid = Nette.validateControl(elem, rules);
var hasAjaxRule = pdForms.hasAjaxRule(rules);

// has to be here and not inside validateControl as it should add ok class only if whole input is valid (not only parts of condional rule etc.)
if (ret && ! hasAjaxRule) {
if (valid && ! hasAjaxRule) {
// add pdforms-valid class name if the input is valid
pdForms.addMessage(elem, null, pdForms.constants.MESSAGE_VALID);
}
Expand All @@ -176,13 +182,6 @@
* Validates form element using optional nette-rules.
*/
pdForms.validateControl = function(elem, rules, onlyCheck, value, emptyOptional) {
// assumes the input is valid, therefore removing all messages except those associated with ajax rules; this
// prevents flashing of message, when ajax rule is evaluated - ajax rules removes their messages when the ajax
// rule is evaluated; when onlyCheck is true, we dont' want to modify DOM at all
if (! onlyCheck) {
pdForms.removeMessages(elem, false);
}

// validate rules one-by-one to know which passed
for (var id = 0, len = rules.length; id < len; id++) {
var rule = rules[id];
Expand Down Expand Up @@ -548,16 +547,19 @@
*/
Nette.validateControl = function(elem, rules, onlyCheck, value, emptyOptional) {
elem = elem.tagName ? elem : elem[0]; // RadioNodeList
rules = rules || Nette.parseJSON(elem.getAttribute('data-nette-rules'));

if (! rules) {
rules = Nette.parseJSON(elem.getAttribute('data-nette-rules'));

// convert arg property in rules into Nette format
rules = pdForms.normalizeRules(rules);
}

// no rules -> skip element validation
if (rules.length === 0) {
return true;
}

// convert arg property in rules into Nette format
rules = pdForms.normalizeRules(rules);

return pdForms.validateControl(elem, rules, onlyCheck, value, emptyOptional);
};

Expand Down
15 changes: 15 additions & 0 deletions src/assets/validators/pdForms.validator.netteRuleProxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @name pdForms.validator.netteRuleProxy
* @author Radek Šerý <radek.sery@peckadesign.cz>
*
* Implementation of proxy rule. This rule is used for optional validating using other Nette validators.
*/
(function() {

Nette.validators.PdFormsRules_netteRuleProxy = function (elem, arg, val) {
var validator = pdForms.formatOperation(arg.context.netteRule);

return validator in Nette.validators ? Nette.validators[validator](elem, arg.context.netteRuleArgs, val) : true;
};

})();
6 changes: 6 additions & 0 deletions tests/Unit/Forms/RuleOptionsFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ public function testCreateOptions(): void
$required = $this->ruleOptionsFactory->createRequired();

\Tester\Assert::false($required->isOptional());

$netteOptional = $this->ruleOptionsFactory->createNetteOptional('someNetteRule', 'argumentForNetteRule');

\Tester\Assert::true($netteOptional->isOptional());
\Tester\Assert::same('someNetteRule', $netteOptional->getContext('netteRule'));
\Tester\Assert::same('argumentForNetteRule', $netteOptional->getContext('netteRuleArgs'));
}


Expand Down
11 changes: 11 additions & 0 deletions tests/Unit/Forms/RuleOptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ public function testSerialization(): void

\Tester\Assert::same(\Nette\Utils\Json::encode(['optional' => TRUE]), \Nette\Utils\Json::encode($optional));

$netteOptional = $this->ruleOptionsFactory->createNetteOptional('someNetteRule', 'argumentForNetteRule');
$netteOptionalExpected = [
'optional' => TRUE,
'context' => [
'netteRule' => 'someNetteRule',
'netteRuleArgs' => 'argumentForNetteRule',
],
];

\Tester\Assert::same(\Nette\Utils\Json::encode($netteOptionalExpected), \Nette\Utils\Json::encode($netteOptional));

$required = $this->ruleOptionsFactory->createRequired()
->enableAjax('http://ajaxValidationTarget.pecka', $this->validationService)
->addValidationMessage('first', 'message')
Expand Down

0 comments on commit 0444650

Please sign in to comment.