diff --git a/doc/index.md b/doc/index.md index 84b87c1..2a22ddf 100644 --- a/doc/index.md +++ b/doc/index.md @@ -5,6 +5,7 @@ - **Měkká validace**: live validace, která upozorní uživatele na problém v inputu, ale umožní odeslání formuláře (není kontrolována na straně serveru) - **Ajax validace**: live validace, která pro výsledek validace volá asynchronně zpracování na backendu - **Standardní validace**: validace, která je zpracována na straně serveru a *může* být kontrolována i na straně klienta +- **Dynamické validační zprávy**: validační zprávy, které nejsou generované předem, ale jsou součástí odpovědi AJAXové validace ## 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. @@ -14,3 +15,4 @@ Knihovna poskytuje nástroje, pomocí kterých je možné zaregistrovat vlastní - [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) +- [Dynamické validační zprávy a našeptávání hodnot inputů přes validační zprávy](suggestions.md) diff --git a/doc/suggestions.md b/doc/suggestions.md new file mode 100644 index 0000000..5f01651 --- /dev/null +++ b/doc/suggestions.md @@ -0,0 +1,28 @@ +## Dynamické zprávy a našeptávání hodnot inputů přes validační zprávy + +`pdForms` je možné použít k našeptávání například překlepových výrazů u domén e-mailů apod. K tomu slouží takzvané _dynamické zprávy_. Tyto validační zprávy jsou vygenerovány až v průběhu validace v PHP a na frontend jsou v případě AJAXové validace vráceny přes response. + +```php +$validationResultWithMessage = new \Pd\Forms\Validation\ValidationResult(TRUE); +$validationResultWithMessage->setMessage('My message'); +``` + +Pokud chceme takovou dynamickou zprávu použít k našeptání hodnoty, stačí k tomu, aby našeptaná hodnota byla obalena do libovolného tagu (``, ``, ...) s `class="pdforms-suggestion"`. Frontend JS se pak postará o to, že při kliknutí na tento element dojde k vyplnění obsahu do validovaného inputu. + +```php +$validationResultWithMessage = new \Pd\Forms\Validation\ValidationResult(TRUE); + +$suggestion = \Nette\Utils\Html::el('a'); +$suggestion->addAttributes([ + 'href' => '#', + 'class' => 'pdforms-suggestion', +]); +$suggestion->setText('Suggested value'); + +$message = \Nette\Utils\Html::el('span'); +$message->setHtml('Did you mean ' . $suggestion . '?'); + +$validationResultWithMessage->setMessage((string) $suggestion); +``` + +Narozdíl od validačních zpráv, které na prvku přidáváte přes `addRule`, se tyto zprávy **neescapují**, aby se správně vykreslily HTML elementy. Z toho důvodu je potřeba na to myslet a dát si pozor na to, co z backendu do této validační zprávy posíláte. Proto doporučuji tyto zprávy skládat pomocí `\Nette\Utils\Html::el` prvků a neskládat je jako stringy. diff --git a/package.json b/package.json index 4ed9825..674a184 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "pd-forms", "title": "pdForms", "description": "Customization of netteForms for use in PeckaDesign.", - "version": "3.3.0", + "version": "3.4.2", "author": "PeckaDesign, s.r.o ", "contributors": [ "Radek Šerý ", diff --git a/src/Validation/ValidationResult.php b/src/Validation/ValidationResult.php index 1e9294e..0090777 100644 --- a/src/Validation/ValidationResult.php +++ b/src/Validation/ValidationResult.php @@ -16,6 +16,8 @@ final class ValidationResult implements \JsonSerializable private string $messageType = ''; + private ?string $message = NULL; + public function __construct(bool $valid, ?string $status = NULL) { @@ -58,6 +60,18 @@ public function setMessageType(string $messageType): void } + public function getMessage(): ?string + { + return $this->message; + } + + + public function setMessage(?string $message): void + { + $this->message = $message; + } + + /** * @return array */ @@ -71,6 +85,7 @@ public function jsonSerialize() 'status' => $this->status, 'messageType' => $this->messageType, 'dependentInputs' => $this->dependentInputs, + 'message' => $this->message, ]; return $valid + \array_filter($rest); diff --git a/src/assets/pdForms.js b/src/assets/pdForms.js index a76bdc0..043dc74 100644 --- a/src/assets/pdForms.js +++ b/src/assets/pdForms.js @@ -1,7 +1,7 @@ /** * @name pdForms * @author Radek Šerý - * @version 3.3.0 + * @version 3.4.2 * * Features: * - live validation @@ -45,7 +45,7 @@ var pdForms = window.pdForms || {}; - pdForms.version = '3.3.0'; + pdForms.version = '3.4.2'; /** @@ -189,7 +189,7 @@ // if ajax validator is used, validate & push into queue of not-yet resolved rules if (rule.isAjax) { - var key = pdForms.getAjaxQueueKey(elem, op); + var key = pdForms.getAjaxQueueKey(elem, op, rule.arg.ajaxUrl); pdForms.ajaxQueue[key] = { msg: rule.msg, isOptional: rule.isOptional, @@ -274,8 +274,8 @@ /** * Get key to ajax queue for given element and operation */ - pdForms.getAjaxQueueKey = function(elem, op) { - return elem.getAttribute('id') + '--' + op; + pdForms.getAjaxQueueKey = function(elem, op, url) { + return elem.getAttribute('id') + '--' + op + '--' + url; }; @@ -284,7 +284,7 @@ * after response is received. */ pdForms.ajaxEvaluate = function(elem, op, status, payload, arg) { - var key = pdForms.getAjaxQueueKey(elem, op); + var key = pdForms.getAjaxQueueKey(elem, op, arg.ajaxUrl); // found request in queue, otherwise do nothing if (key in pdForms.ajaxQueue) { @@ -298,24 +298,25 @@ // remove old messages, only when onlyCheck is false pdForms.removeMessages(elem, true); - if (status in msg && msg[status]) { - var msgType = pdForms.constants.MESSAGE_ERROR; + var msgType = pdForms.constants.MESSAGE_ERROR; - if (typeof payload === 'object' && payload.messageType) { - msgType = payload.messageType; - } else if (status === 'timeout') { - msgType = pdForms.constants.MESSAGE_INFO; - } else if (status === 'valid') { - msgType = pdForms.constants.MESSAGE_VALID; - } + if (typeof payload === 'object' && payload.messageType) { + msgType = payload.messageType; + } else if (status === 'timeout') { + msgType = pdForms.constants.MESSAGE_INFO; + } else if (status === 'valid') { + msgType = pdForms.constants.MESSAGE_VALID; + } - if (isOptional && msgType === pdForms.constants.MESSAGE_ERROR) { - msgType = pdForms.constants.MESSAGE_INFO; - } + if (isOptional && msgType === pdForms.constants.MESSAGE_ERROR) { + msgType = pdForms.constants.MESSAGE_INFO; + } + if (typeof payload === 'object' && payload.message) { + pdForms.addMessage(elem, payload.message, msgType, true, false); + } else if (status in msg && msg[status]) { pdForms.addMessage(elem, msg[status], msgType, true); - } - else if (status === 'valid') { + } else if (status === 'valid') { // add pdforms-valid class name if the input is valid and no message is specified pdForms.addMessage(elem, null, pdForms.constants.MESSAGE_VALID, true); } @@ -354,17 +355,22 @@ var input = document.getElementById(inputId); if (input && ! input.value) { - var ev = document.createEvent('Event'); - ev.initEvent('change', true, true); - - input.value = payload.dependentInputs[inputId]; - input.dispatchEvent(ev); + pdForms.setInputValue(input, payload.dependentInputs[inputId]); } } } }; + pdForms.setInputValue = function (elem, value) { + var ev = document.createEvent('Event'); + ev.initEvent('change', true, true); + + elem.value = value; + elem.dispatchEvent(ev); + }; + + /** * Find the placeholder element for a given input element. */ @@ -409,7 +415,7 @@ * Using data-pdforms-messages-tagname we could change the default span (p in case of global messages) element. * Using data-pdforms-messages-global on elem we could force the message to be displayed in global message placeholder. */ - pdForms.addMessage = function(elem, message, type, isAjaxRuleMessage) { + pdForms.addMessage = function(elem, message, type, isAjaxRuleMessage, escapeMessage) { var placeholder = pdForms.getMessagePlaceholder(elem); if (! placeholder.elem) { @@ -441,7 +447,13 @@ className = (tagName === 'p') ? 'message message--' + type : className; var msg = document.createElement(tagName); - msg.textContent = message; + + if (typeof escapeMessage === 'undefined' || escapeMessage) { + msg.textContent = message; + } else { + msg.innerHTML = message; + } + msg.setAttribute('class', className + ' pdforms-message'); msg.setAttribute('data-elem', elem.name); @@ -451,6 +463,8 @@ if (tagName === 'label') { msg.setAttribute('for', elem.id); + } else { + msg.setAttribute('data-for', elem.id); } placeholder.elem.getAttribute('data-pdforms-messages-prepend') ? @@ -541,6 +555,42 @@ }; + /** + * Fills in the suggestion from clicked e.target into associated input element. + */ + pdForms.useSuggestion = function(e) { + e.preventDefault(); + + var suggestion = e.target.text; + var elem = pdForms.getSuggestionInput(e.target); + + if (! suggestion || ! elem) { + return false; + } + + // clear suggestion message and validate again + pdForms.removeMessages(elem, true); + pdForms.setInputValue(elem, suggestion); + pdForms.liveValidation({ target: elem }); + }; + + + /** + * For given suggestion element inside validation message finds and returns associated input element (or null). + */ + pdForms.getSuggestionInput = function(suggestion) { + var msgElem = suggestion.closest('.pdforms-message'); + + if (! msgElem) { + return null; + } + + var elemId = msgElem.getAttribute('for') || msgElem.getAttribute('data-for'); + + return document.getElementById(elemId); + } + + /** * Optional rules are defined using "optional" property in "arg". We have to convert arg into Nette format before * validating. This means removing all properties but data from arg and storing them elsewhere. @@ -637,6 +687,9 @@ addDelegatedEventListener(form, 'validate focusout change', 'select', pdForms.liveValidation); addDelegatedEventListener(form, 'validate change', 'input[type="checkbox"], input[type="radio"]', pdForms.liveValidation); + // Suggestions from custom messages + addDelegatedEventListener(form, 'click', '.pdforms-suggestion', pdForms.useSuggestion); + // Validation on custom events var pdformsValidateOnArr = Array.prototype.slice.call(form.elements); pdformsValidateOnArr = pdformsValidateOnArr.filter(function(elem) { diff --git a/tests/Unit/Forms/ValidationResultTest.php b/tests/Unit/Forms/ValidationResultTest.php index aca94bc..ae3e747 100644 --- a/tests/Unit/Forms/ValidationResultTest.php +++ b/tests/Unit/Forms/ValidationResultTest.php @@ -41,6 +41,25 @@ public function testSerialization(): void $validationResult->addDependentInput('second', 2); \Tester\Assert::same(\Nette\Utils\Json::encode($expected), \Nette\Utils\Json::encode($validationResult)); + + $validationResultWithMessage = new \Pd\Forms\Validation\ValidationResult(TRUE); + $validationResultWithMessage->setMessage('My message'); + + $expected = [ + 'valid' => TRUE, + 'message' => 'My message', + ]; + + \Tester\Assert::same(\Nette\Utils\Json::encode($expected), \Nette\Utils\Json::encode($validationResultWithMessage)); + } + + + public function testResultDynamicMessage(): void + { + $validation = new \Pd\Forms\Validation\ValidationResult(TRUE); + \Tester\Assert::null($validation->getMessage()); + $validation->setMessage('Test'); + \Tester\Assert::equal('Test', $validation->getMessage()); } }