Skip to content

Commit

Permalink
datasets and confirmation (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomK authored Dec 10, 2021
1 parent 8529069 commit 78a4cf5
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 80 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"packaged/glimpse": "^2.6",
"packaged/ui": "^1.0",
"packaged-ui/bem-component": "~1.2",
"packaged/validate": "~2.6"
"packaged/validate": "~3.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
Expand Down
22 changes: 20 additions & 2 deletions demo/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
use Packaged\Form\Form\Form;
use Packaged\Helpers\Arrays;
use Packaged\SafeHtml\SafeHtml;
use Packaged\Validate\Validators\ConfirmationValidator;
use Packaged\Validate\Validators\EqualValidator;
use Packaged\Validate\Validators\RegexValidator;
use Packaged\Validate\Validators\RequiredValidator;
use Packaged\Validate\Validators\StringValidator;

Expand All @@ -38,6 +40,10 @@ class DemoForm extends Form
* @var SecureTextDataHandler
*/
public $password;
/**
* @var SecureTextDataHandler
*/
public $confirmPassword;
/**
* @var HiddenDataHandler
*/
Expand Down Expand Up @@ -67,14 +73,20 @@ class DemoForm extends Form
protected function _initDataHandlers()
{
$this->name = TextDataHandler::i()->addValidator(new StringValidator(2, 20));
$this->email = EmailDataHandler::i();
$this->email = EmailDataHandler::i()
->setAutocomplete('email');
$this->selection = EnumDataHandler::i(Arrays::fuse(['test1', 'test2', 'test3']))
->setDefaultValue($this->selection)
->styleSplit()
->addValidator(new EqualValidator('test3'));

$this->greedySelect = MultiValueEnumDataHandler::i(Arrays::fuse(['apple', 'orange', 'pear']))->styleSplit();
$this->password = SecureTextDataHandler::i()->setGuidance("(Min. 8 characters, 1 number, case-sensitive)");
$this->password = SecureTextDataHandler::i()->setGuidance("(Min. 8 characters, 1 number, case-sensitive)")
->setAutocomplete('new-password')
->addValidator(new StringValidator(8))
->addValidator(new RegexValidator('/\d+/', 'must contain one number'));
$this->confirmPassword = SecureTextDataHandler::i()
->setAutocomplete('new-password');
$this->secret = HiddenDataHandler::i()->setValue('Form displayed at ' . date("Y-m-d H:i:s"));
$this->agree = BooleanDataHandler::i()
->setGuidance(new SafeHtml('<a href="#">Terms & Conditions</a>'))
Expand All @@ -89,6 +101,12 @@ protected function _initDataHandlers()
->addValidator(new RequiredValidator());
//$this->setHandlerDecorator(new InputOnlyDataHandlerDecorator(), 'agree');
}

protected function _configureDataHandlers()
{
parent::_configureDataHandlers();
$this->confirmPassword->addValidator(new ConfirmationValidator($this->password->getName()));
}
}

$data = array_merge($_POST, $_FILES);
Expand Down
2 changes: 1 addition & 1 deletion js/dist/form.min.js

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions js/src/WeakMappedSet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export class WeakMappedSet
{
/**
* @type {WeakMap<Object, Set>}
* @private
*/
_map = new WeakMap();

/**
* @param {Object} key
* @param value
*/
add(key, value)
{
if(!this._map.has(key))
{
this._map.set(key, new Set);
}

this._map.get(key).add(value);
}

/**
* @param {Object} key
* @param value
*/
delete(key, value)
{
if(!this._map.has(key))
{
return;
}

this._map.get(key).delete(value);
}

clear(key)
{
this._map.delete(key);
}

get(key)
{
return this._map.get(key);
}
}
74 changes: 30 additions & 44 deletions js/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {addErrors, clearErrors, validateForm, validateHandler} from './validation.js';
import {clearErrors, validateForm, validateHandler} from './validation.js';

export * from './validation.js';

Expand All @@ -20,25 +20,12 @@ export function init(rootElement = document)
*/
const form = e.path && e.path[0] || e.target;
const results = validateForm(form);
results.forEach(
(result, handlerName) =>
{
// show errors if necessary
const errContainer = form.querySelector(`.p-form__field[handler-name="${handlerName}"] .p-form__errors`);
if(errContainer)
{
errContainer.classList.toggle('p-form__errors--hidden', result.errors.length === 0);
}

if(result.errors.length > 0)
{
clearErrors(form, handlerName);
addErrors(form, handlerName, result.errors);
e.preventDefault();
e.stopImmediatePropagation();
}
},
);
const hasErrors = !Array.from(results.values()).every(r => r.errors.length === 0);
if(hasErrors)
{
e.preventDefault();
e.stopImmediatePropagation();
}
},
);

Expand All @@ -57,30 +44,29 @@ export function init(rootElement = document)
rootElement.addEventListener('input', e =>
{
const inputEle = e.path && e.path[0] || e.target;
const handlerContainer = inputEle.closest('.p-form__field');
if(!handlerContainer || !handlerContainer.hasAttribute('handler-name'))
{
return;
}
const handlerName = handlerContainer.getAttribute('handler-name');

const form = inputEle.closest('form');
if(!form)
{
return;
}
_validateInput(inputEle, false);
});

const result = validateHandler(form, handlerName);
const errContainer = handlerContainer.querySelector(`.p-form__errors`);
if(errContainer && result.errors.length === 0)
{
clearErrors(form, handlerName);
errContainer.classList.add('p-form__errors--hidden');
}
else if(!result.potentiallyValid)
{
clearErrors(form, handlerName);
addErrors(form, handlerName, result.errors);
}
rootElement.addEventListener('focusout', e =>
{
const inputEle = e.path && e.path[0] || e.target;
_validateInput(inputEle, true);
});
}

function _validateInput(inputEle, errorOnPotentiallyValid = false)
{
const handlerContainer = inputEle.closest('.p-form__field');
if(!handlerContainer || !handlerContainer.hasAttribute('handler-name'))
{
return;
}
const handlerName = handlerContainer.getAttribute('handler-name');

const form = inputEle.closest('form');
if(!form)
{
return;
}
validateHandler(form, handlerName, errorOnPotentiallyValid);
}
104 changes: 89 additions & 15 deletions js/src/validation.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import base64 from 'base-64';
import {ValidationResponse, Validator} from '@packaged/validate';
import {DataSetValidator, ValidationResponse, Validator} from '@packaged/validate';
import {ConfirmationValidator} from '@packaged/validate/js/validators/ConfirmationValidator.js';
import {WeakMappedSet} from './WeakMappedSet.js';

function _getEleValue(ele)
{
Expand Down Expand Up @@ -105,6 +107,7 @@ export function clearErrors(form, handlerName)
if(errContainer)
{
errContainer.innerHTML = '';
errContainer.classList.add('p-form__errors--hidden');
}
}

Expand All @@ -128,18 +131,19 @@ export function addErrors(form, handlerName, errors = [])

_updateValidationState(form, handlerName, ValidationResponse.error(errors));
const errUl = errContainer.querySelector(':scope > ul') || document.createElement('ul');
errors.forEach(
(err) =>
{
const errEle = document.createElement('li');
errEle.innerText = err;
errUl.append(errEle);
}
);
errors.forEach((err) =>
{
const errEle = document.createElement('li');
errEle.innerText = err;
errUl.append(errEle);
});
errContainer.append(errUl);
errContainer.classList.remove('p-form__errors--hidden');
}

export function validateHandler(form, handlerName, errorOnPotentiallyValid = false)
const _reverseConfirmations = new WeakMappedSet();

export function validateHandler(form, handlerName, errorOnPotentiallyValid = false, _processedMap = new WeakMap)
{
const fieldValue = _getHandlerValue(form, handlerName);
const handlerScope = _getHandlerScope(form, handlerName);
Expand All @@ -148,14 +152,34 @@ export function validateHandler(form, handlerName, errorOnPotentiallyValid = fal
{
return result;
}
if(_processedMap.has(handlerScope))
{
return _processedMap.get(handlerScope);
}

const validators = _getValidators(handlerScope);

try
{
const validators = JSON.parse(base64.decode(handlerScope.getAttribute('validation')));
const formData = new FormData(form);
const data = {};
for(let [key, val] of formData.entries())
{
data[key] = val;
}

validators.forEach(
(validatorObj) =>
(validator) =>
{
const validator = Validator.fromJsonObject(validatorObj);
if(validator instanceof DataSetValidator)
{
validator.setData(data);
}
if(validator instanceof ConfirmationValidator)
{
const fld = _getHandlerScope(form, validator._field);
_reverseConfirmations.add(fld, handlerName);
}
result.combine(validator.validate(fieldValue));
}
);
Expand All @@ -164,7 +188,27 @@ export function validateHandler(form, handlerName, errorOnPotentiallyValid = fal
{
}

_processedMap.set(handlerScope, result);

const confirms = _reverseConfirmations.get(handlerScope);
if(confirms)
{
confirms.forEach(
(c) =>
{
validateHandler(form, c, true, _processedMap);
}
);
}

_updateValidationState(form, handlerName, result, errorOnPotentiallyValid);

clearErrors(form, handlerName);
if(!result.potentiallyValid || errorOnPotentiallyValid)
{
addErrors(form, handlerName, result.errors);
}

return result;
}

Expand All @@ -175,21 +219,51 @@ export function validateHandler(form, handlerName, errorOnPotentiallyValid = fal
export function validateForm(form)
{
const fullResult = new Map();
if(!form instanceof HTMLFormElement)
if(!(form instanceof HTMLFormElement))
{
console.error('not a form element');
return fullResult;
}

const _processedMap = new WeakMap;
const fields = form.querySelectorAll('.p-form__field[validation]');
fields.forEach(
(container) =>
{
const handlerName = container.getAttribute('handler-name');
const result = validateHandler(form, handlerName, true);
const result = validateHandler(form, handlerName, true, _processedMap);
fullResult.set(handlerName, result);
}
);

return fullResult;
}

const _validatorsMap = new WeakMap();

function _getValidators(handlerScope)
{
if(!_validatorsMap.has(handlerScope))
{
const validationString = handlerScope.getAttribute('validation');
if(validationString)
{
const validatorsObj = JSON.parse(base64.decode(validationString));
const validators = validatorsObj.map(
(validatorObj) =>
{
try
{
return Validator.fromJsonObject(validatorObj);
}
catch(e)
{
return null;
}
}
);
_validatorsMap.set(handlerScope, validators.filter(v => v instanceof Validator));
}
}
return _validatorsMap.get(handlerScope);
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@packaged/form",
"version": "4.0.1",
"version": "4.0.2",
"main": "/index.js",
"repository": "[email protected]:packaged/form",
"author": "Tom Kay <[email protected]>",
Expand All @@ -10,7 +10,7 @@
"index.js"
],
"dependencies": {
"@packaged/validate": "^2.7.1",
"@packaged/validate": "^3.0.2",
"base-64": "^1.0.0"
},
"devDependencies": {
Expand All @@ -27,6 +27,6 @@
"rollup-plugin-terser": "^7.0.2"
},
"scripts": {
"build": "rollup -c rollup.config.js -w"
"build": "rollup -c rollup.config.js"
}
}
Loading

0 comments on commit 78a4cf5

Please sign in to comment.