├── examples ├── assets │ ├── logo.png │ └── style.css ├── localization.ini ├── latte │ ├── page.latte │ ├── form.latte │ └── form-bootstrap5.latte ├── latte.php ├── custom-validator.php ├── containers.php ├── html5.php ├── localization.php ├── manual-rendering.php ├── live-validation.php ├── custom-rendering.php ├── bootstrap4-rendering.php ├── custom-control.php ├── basic-example.php └── bootstrap5-rendering.php ├── src ├── assets │ ├── index.esm.ts │ ├── index.umd.ts │ ├── dist │ │ └── package.json │ ├── webalize.ts │ ├── types.ts │ ├── validators.ts │ └── formValidator.ts ├── Forms │ ├── FormRenderer.php │ ├── SubmitterControl.php │ ├── Rendering │ │ ├── LatteRenderer.php │ │ └── DataClassGenerator.php │ ├── Controls │ │ ├── TextArea.php │ │ ├── ImageButton.php │ │ ├── ColorPicker.php │ │ ├── Button.php │ │ ├── Checkbox.php │ │ ├── MultiSelectBox.php │ │ ├── HiddenField.php │ │ ├── CsrfProtection.php │ │ ├── TextInput.php │ │ ├── SubmitButton.php │ │ ├── RadioList.php │ │ ├── CheckboxList.php │ │ ├── ChoiceControl.php │ │ ├── TextBase.php │ │ ├── SelectBox.php │ │ ├── UploadControl.php │ │ ├── MultiChoiceControl.php │ │ ├── DateTimeControl.php │ │ └── BaseControl.php │ ├── Rule.php │ ├── Control.php │ ├── ControlGroup.php │ ├── Blueprint.php │ ├── Rules.php │ ├── Helpers.php │ └── Validator.php └── Bridges │ ├── FormsLatte │ ├── Nodes │ │ ├── InputErrorNode.php │ │ ├── FormContainerNode.php │ │ ├── FormPrintNode.php │ │ ├── InputNode.php │ │ ├── FormNNameNode.php │ │ ├── LabelNode.php │ │ ├── FormNode.php │ │ └── FieldNNameNode.php │ ├── FormsExtension.php │ └── Runtime.php │ └── FormsDI │ └── FormsExtension.php ├── .phpstorm.meta.php ├── tsconfig.json ├── eslint.config.js ├── package.json ├── composer.json ├── rollup.config.js ├── license.md └── readme.md /examples/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nette/forms/HEAD/examples/assets/logo.png -------------------------------------------------------------------------------- /src/assets/index.esm.ts: -------------------------------------------------------------------------------- 1 | import { FormValidator } from './formValidator'; 2 | 3 | // TODO 4 | export function initialize() { 5 | } 6 | 7 | export { FormValidator }; 8 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | '@|Nette\Utils\ArrayHash'])); 8 | override(new \Nette\Forms\Container, map(['' => 'Nette\Forms\Controls\BaseControl'])); 9 | -------------------------------------------------------------------------------- /src/assets/index.umd.ts: -------------------------------------------------------------------------------- 1 | import { FormValidator } from './formValidator'; 2 | import { webalize } from './webalize'; 3 | import { version } from './dist/package.json'; 4 | 5 | type NetteForms = FormValidator & { version: string; webalize: typeof webalize }; 6 | let nette = new FormValidator as NetteForms; 7 | nette.version = version; 8 | nette.webalize = webalize; 9 | 10 | export default nette; 11 | -------------------------------------------------------------------------------- /examples/localization.ini: -------------------------------------------------------------------------------- 1 | Personal data = Osobní údaje 2 | Your name: = Jméno: 3 | Enter your name = Zadejte jméno 4 | Your age: = Věk: 5 | Enter your age = Zadejte váš věk 6 | Age must be numeric value = Věk musí být číslo 7 | Age must be in range from %d to %d = Věk musí být v rozmezí %d až %d 8 | Country: = Země: 9 | Select your country = Vyberte zemi 10 | World = Svět 11 | other = jiná 12 | Send = Odeslat 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es2022", 5 | "resolveJsonModule": true, 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noUncheckedIndexedAccess": true 12 | }, 13 | "include": [ 14 | "src/assets/*.ts" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/Forms/FormRenderer.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | Nette Forms rendering using Latte 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Nette Forms & Bootstrap v5 rendering example

15 | 16 | {include bootstrap-form, $form} 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/assets/webalize.ts: -------------------------------------------------------------------------------- 1 | let webalizeTable: Record = { \u00e1: 'a', \u00e4: 'a', \u010d: 'c', \u010f: 'd', \u00e9: 'e', \u011b: 'e', \u00ed: 'i', \u013e: 'l', \u0148: 'n', \u00f3: 'o', \u00f4: 'o', \u0159: 'r', \u0161: 's', \u0165: 't', \u00fa: 'u', \u016f: 'u', \u00fd: 'y', \u017e: 'z' }; 2 | 3 | /** 4 | * Converts string to web safe characters [a-z0-9-] text. 5 | * @param {string} s 6 | * @return {string} 7 | */ 8 | export function webalize(s: string): string { 9 | s = s.toLowerCase(); 10 | let res = ''; 11 | for (let i = 0; i < s.length; i++) { 12 | let ch = webalizeTable[s.charAt(i)]; 13 | res += ch ? ch : s.charAt(i); 14 | } 15 | return res.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); 16 | } 17 | -------------------------------------------------------------------------------- /src/Forms/Rendering/LatteRenderer.php: -------------------------------------------------------------------------------- 1 | generateLatte($form); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Forms/Controls/TextArea.php: -------------------------------------------------------------------------------- 1 | control->setName('textarea'); 25 | $this->setOption('type', 'textarea'); 26 | } 27 | 28 | 29 | public function getControl(): Nette\Utils\Html 30 | { 31 | return parent::getControl() 32 | ->setText((string) $this->getRenderedValue()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Forms/Rule.php: -------------------------------------------------------------------------------- 1 | validator) 34 | || Nette\Utils\Callback::isStatic($this->validator); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/types.ts: -------------------------------------------------------------------------------- 1 | export type FormElement = (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement) & { form: HTMLFormElement }; 2 | 3 | // number can only be created by the validator 4 | export type FormElementValue = string | string[] | boolean | FileList | number | null; 5 | 6 | export type Validator = ( 7 | elem: FormElement, 8 | arg: unknown, 9 | value: unknown, 10 | newValue: { value: unknown }, 11 | ) => boolean | null; 12 | 13 | export type Rule = { 14 | op: string; 15 | neg?: boolean; 16 | msg: string; 17 | arg?: unknown; 18 | rules?: Rule[]; 19 | condition?: boolean; 20 | control?: string; 21 | toggle?: Record; 22 | }; 23 | 24 | export type FormError = { 25 | element: FormElement; 26 | message: string; 27 | }; 28 | 29 | export type ToggleState = { 30 | elem: FormElement; 31 | state: boolean; 32 | }; 33 | -------------------------------------------------------------------------------- /src/Forms/Control.php: -------------------------------------------------------------------------------- 1 | getValues() result? 39 | */ 40 | function isOmitted(): bool; 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "devDependencies": { 4 | "@nette/eslint-plugin": "^0.1.2", 5 | "@rollup/plugin-json": "^6.1.0", 6 | "@rollup/plugin-node-resolve": "^16.0.1", 7 | "@rollup/plugin-terser": "^0.4.4", 8 | "@rollup/plugin-typescript": "^12.1.2", 9 | "eslint": "^9.26.0", 10 | "globals": "^15.3.0", 11 | "jasmine": "^5.7.1", 12 | "jasmine-core": "^5.7.1", 13 | "karma": "^6.4.4", 14 | "karma-chrome-launcher": "^3.2.0", 15 | "karma-jasmine": "^5.1.0", 16 | "rollup": "^4.40.2", 17 | "rollup-plugin-dts": "^6.2.1", 18 | "terser": "^5.39.1", 19 | "typescript": "^5.8.3", 20 | "typescript-eslint": "^8.32.1" 21 | }, 22 | "scripts": { 23 | "typecheck": "tsc -noemit", 24 | "lint": "eslint --cache", 25 | "lint:fix": "eslint --cache --fix", 26 | "test": "karma start tests/netteForms/karma.conf.ts", 27 | "build": "rollup -c", 28 | "postbuild": "npm run test" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Forms/Rendering/DataClassGenerator.php: -------------------------------------------------------------------------------- 1 | generateDataClass($form, $this->propertyPromotion, $baseName); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Forms/Controls/ImageButton.php: -------------------------------------------------------------------------------- 1 | control->type = 'image'; 26 | $this->control->src = $src; 27 | $this->control->alt = $alt; 28 | } 29 | 30 | 31 | public function loadHttpData(): void 32 | { 33 | parent::loadHttpData(); 34 | $this->value = $this->value 35 | ? [(int) array_shift($this->value), (int) array_shift($this->value)] 36 | : null; 37 | } 38 | 39 | 40 | public function getHtmlName(): string 41 | { 42 | return parent::getHtmlName() . '[]'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/latte/form.latte: -------------------------------------------------------------------------------- 1 | {* Generic form template *} 2 | 3 | {define form, $name} 4 |
5 | {* List for form-level error messages *} 6 |
    7 |
  • {$error}
  • 8 |
9 | 10 |
11 | 12 | {include controls $group->getControls()} 13 |
14 | 15 | {include controls $form->getControls()} 16 |
17 | {/define} 18 | 19 | 20 | {define local controls, array $controls} 21 | {* Loop over form controls and render each one *} 22 | 23 | 26 | 27 | 28 | 29 | 35 | 36 |
{label $control /} 30 | {input $control} 31 | 32 | {$control->getOption(description)} 33 | {$control->error} 34 |
37 | {/define} 38 | -------------------------------------------------------------------------------- /src/Bridges/FormsLatte/Nodes/InputErrorNode.php: -------------------------------------------------------------------------------- 1 | outputMode = $tag::OutputKeepIndentation; 29 | $tag->expectArguments(); 30 | 31 | $node = new static; 32 | $node->name = $tag->parser->parseUnquotedStringOrExpression(); 33 | return $node; 34 | } 35 | 36 | 37 | public function print(PrintContext $context): string 38 | { 39 | return $context->format( 40 | 'echo %escape($this->global->forms->item(%node)->getError()) %line;', 41 | $this->name, 42 | $this->position, 43 | ); 44 | } 45 | 46 | 47 | public function &getIterator(): \Generator 48 | { 49 | yield $this->name; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Bridges/FormsDI/FormsExtension.php: -------------------------------------------------------------------------------- 1 | config = new class { 24 | /** @var string[] */ 25 | public array $messages = []; 26 | }; 27 | } 28 | 29 | 30 | public function afterCompile(Nette\PhpGenerator\ClassType $class): void 31 | { 32 | $initialize = $this->initialization ?? $class->getMethod('initialize'); 33 | 34 | foreach ($this->config->messages as $name => $text) { 35 | if (defined('Nette\Forms\Form::' . $name)) { 36 | $initialize->addBody('Nette\Forms\Validator::$messages[Nette\Forms\Form::?] = ?;', [$name, $text]); 37 | } elseif (defined($name)) { 38 | $initialize->addBody('Nette\Forms\Validator::$messages[' . $name . '] = ?;', [$text]); 39 | } else { 40 | throw new Nette\InvalidArgumentException('Constant Nette\Forms\Form::' . $name . ' or constant ' . $name . ' does not exist.'); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Bridges/FormsLatte/FormsExtension.php: -------------------------------------------------------------------------------- 1 | Nodes\FormNode::create(...), 25 | 'formContext' => Nodes\FormNode::create(...), 26 | 'formContainer' => Nodes\FormContainerNode::create(...), 27 | 'label' => Nodes\LabelNode::create(...), 28 | 'input' => Nodes\InputNode::create(...), 29 | 'inputError' => Nodes\InputErrorNode::create(...), 30 | 'formPrint' => Nodes\FormPrintNode::create(...), 31 | 'formClassPrint' => Nodes\FormPrintNode::create(...), 32 | 'n:name' => fn(Latte\Compiler\Tag $tag) => yield from strtolower($tag->htmlElement->name) === 'form' 33 | ? Nodes\FormNNameNode::create($tag) 34 | : Nodes\FieldNNameNode::create($tag), 35 | ]; 36 | } 37 | 38 | 39 | public function getProviders(): array 40 | { 41 | return [ 42 | 'forms' => new Runtime, 43 | ]; 44 | } 45 | 46 | 47 | public function getCacheKey(Latte\Engine $engine): mixed 48 | { 49 | return 2; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Forms/Controls/ColorPicker.php: -------------------------------------------------------------------------------- 1 | setOption('type', 'color'); 25 | } 26 | 27 | 28 | /** 29 | * @param ?string $value 30 | */ 31 | public function setValue($value): static 32 | { 33 | if ($value === null) { 34 | $this->value = '#000000'; 35 | } elseif (is_string($value) && preg_match('~#?[0-9a-f]{6}~Ai', $value)) { 36 | $this->value = '#' . strtolower(ltrim($value, '#')); 37 | } else { 38 | throw new Nette\InvalidArgumentException('Color must have #rrggbb format.'); 39 | } 40 | return $this; 41 | } 42 | 43 | 44 | public function loadHttpData(): void 45 | { 46 | try { 47 | parent::loadHttpData(); 48 | } catch (Nette\InvalidArgumentException) { 49 | $this->setValue(null); 50 | } 51 | } 52 | 53 | 54 | public function getControl(): Nette\Utils\Html 55 | { 56 | return parent::getControl()->addAttributes([ 57 | 'type' => 'color', 58 | 'value' => $this->value, 59 | ]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nette/forms", 3 | "description": "📝 Nette Forms: generating, validating and processing secure forms in PHP. Handy API, fully customizable, server & client side validation and mature design.", 4 | "keywords": ["nette", "forms", "validation", "csrf", "javascript", "bootstrap"], 5 | "homepage": "https://nette.org", 6 | "license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"], 7 | "authors": [ 8 | { 9 | "name": "David Grudl", 10 | "homepage": "https://davidgrudl.com" 11 | }, 12 | { 13 | "name": "Nette Community", 14 | "homepage": "https://nette.org/contributors" 15 | } 16 | ], 17 | "require": { 18 | "php": "8.2 - 8.5", 19 | "nette/component-model": "^3.2", 20 | "nette/http": "^3.3", 21 | "nette/utils": "^4.1" 22 | }, 23 | "require-dev": { 24 | "nette/application": "^3.3", 25 | "nette/di": "^3.2", 26 | "nette/tester": "^2.5.2", 27 | "latte/latte": "^3.1", 28 | "tracy/tracy": "^2.11", 29 | "phpstan/phpstan-nette": "^2.0@stable" 30 | }, 31 | "conflict": { 32 | "latte/latte": "<3.1 || >=3.2" 33 | }, 34 | "suggest": { 35 | "ext-intl": "to use date/time controls" 36 | }, 37 | "autoload": { 38 | "classmap": ["src/"], 39 | "psr-4": { 40 | "Nette\\": "src" 41 | } 42 | }, 43 | "minimum-stability": "dev", 44 | "scripts": { 45 | "phpstan": "phpstan analyse", 46 | "tester": "tester tests -s" 47 | }, 48 | "extra": { 49 | "branch-alias": { 50 | "dev-master": "4.0-dev" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/assets/style.css: -------------------------------------------------------------------------------- 1 | /** common style for Nette examples */ 2 | 3 | html { 4 | font: 16px/1.5 sans-serif; 5 | border-top: 4.7em solid #F4EBDB; 6 | } 7 | 8 | body { 9 | max-width: 990px; 10 | margin: -4.7em auto 0; 11 | background: white; 12 | color: #333; 13 | } 14 | 15 | h1 { 16 | font-size: 1.9em; 17 | margin: .5em 0 1.5em; 18 | background: url(logo.png) right center no-repeat; 19 | color: #7A7772; 20 | text-shadow: 1px 1px 0 white; 21 | } 22 | 23 | fieldset { 24 | padding: .2em 1em 1em; 25 | margin: .5em 0; 26 | background: #E4F1FC; 27 | border: 1px solid #B2D1EB; 28 | } 29 | 30 | textarea, 31 | select, 32 | input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="image"]):not([type="range"]) { 33 | padding: .3em .5em; 34 | color: black; 35 | background: white; 36 | border: 1px solid silver; 37 | } 38 | 39 | .has-error textarea, 40 | .has-error select, 41 | .has-error input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="image"]):not([type="range"]) { 42 | border-color: #E22; 43 | } 44 | 45 | select { 46 | padding-right: .3em; 47 | } 48 | 49 | input[type="submit"] { 50 | font-size: 120%; 51 | } 52 | 53 | th { 54 | width: 10em; 55 | text-align: right; 56 | font-weight: normal; 57 | } 58 | 59 | .required label { 60 | font-weight: bold; 61 | } 62 | 63 | .error { 64 | color: #E22; 65 | font-weight: bold; 66 | margin-left: 1em; 67 | } 68 | 69 | footer a { 70 | font-size: 70%; 71 | color: gray; 72 | } 73 | -------------------------------------------------------------------------------- /examples/latte.php: -------------------------------------------------------------------------------- 1 | addText('name', 'Your name') 23 | ->setRequired('Enter your name') 24 | ->setOption('description', 'Name and surname'); 25 | 26 | $form->addDate('birth', 'Date of birth'); 27 | 28 | $form->addRadioList('gender', 'Your gender', [ 29 | 'male', 'female', 30 | ]); 31 | 32 | $form->addCheckboxList('colors', 'Favorite colors', [ 33 | 'red', 'green', 'blue', 34 | ]); 35 | 36 | $form->addSelect('country', 'Country', [ 37 | 'Buranda', 'Qumran', 'Saint Georges Island', 38 | ]); 39 | 40 | $form->addCheckbox('send', 'Ship to address'); 41 | 42 | $form->addColor('color', 'Favourite colour'); 43 | 44 | $form->addPassword('password', 'Choose password'); 45 | $form->addUpload('avatar', 'Picture'); 46 | $form->addTextArea('note', 'Comment'); 47 | 48 | $form->addSubmit('submit', 'Send'); 49 | $form->addSubmit('cancel', 'Cancel'); 50 | 51 | 52 | 53 | if ($form->isSuccess()) { 54 | echo '

Form was submitted and successfully validated

'; 55 | Dumper::dump($form->getValues()); 56 | exit; 57 | } 58 | 59 | $latte = new Latte\Engine; 60 | $latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension); 61 | 62 | $latte->render(__DIR__ . '/latte/page.latte', ['form' => $form]); 63 | -------------------------------------------------------------------------------- /examples/custom-validator.php: -------------------------------------------------------------------------------- 1 | value % $arg === 0; 27 | } 28 | } 29 | 30 | 31 | $form = new Form; 32 | 33 | $form->addText('num1', 'Multiple of 8:') 34 | ->setDefaultValue(5) 35 | ->addRule('MyValidators::divisibilityValidator', 'First number must be %d multiple', 8); 36 | 37 | $form->addSubmit('submit', 'Send'); 38 | 39 | 40 | if ($form->isSuccess()) { 41 | echo '

Form was submitted and successfully validated

'; 42 | Dumper::dump($form->getValues()); 43 | exit; 44 | } 45 | 46 | 47 | ?> 48 | 49 | 50 | Nette Forms custom validator example 51 | 52 | 53 | 54 | 59 | 60 |

Nette Forms custom validator example

61 | 62 | render() ?> 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/containers.php: -------------------------------------------------------------------------------- 1 | addGroup('First person'); 25 | 26 | $first = $form->addContainer('first'); 27 | $first->addText('name', 'Your name:'); 28 | $first->addText('email', 'Email:'); 29 | $first->addText('street', 'Street:'); 30 | $first->addText('city', 'City:'); 31 | 32 | // group Second person 33 | $form->addGroup('Second person'); 34 | 35 | $second = $form->addContainer('second'); 36 | $second->addText('name', 'Your name:'); 37 | $second->addText('email', 'Email:'); 38 | $second->addText('street', 'Street:'); 39 | $second->addText('city', 'City:'); 40 | 41 | // group for button 42 | $form->addGroup(); 43 | 44 | $form->addSubmit('submit', 'Send'); 45 | 46 | 47 | if ($form->isSuccess()) { 48 | echo '

Form was submitted and successfully validated

'; 49 | Dumper::dump($form->getValues()); 50 | exit; 51 | } 52 | 53 | 54 | ?> 55 | 56 | 57 | Nette Forms containers example 58 | 59 | 60 |

Nette Forms containers example

61 | 62 | render() ?> 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/Bridges/FormsLatte/Nodes/FormContainerNode.php: -------------------------------------------------------------------------------- 1 | */ 29 | public static function create(Tag $tag): \Generator 30 | { 31 | $tag->outputMode = $tag::OutputRemoveIndentation; 32 | $tag->expectArguments(); 33 | 34 | $node = $tag->node = new static; 35 | $node->name = $tag->parser->parseUnquotedStringOrExpression(); 36 | [$node->content] = yield; 37 | return $node; 38 | } 39 | 40 | 41 | public function print(PrintContext $context): string 42 | { 43 | return $context->format( 44 | '$this->global->forms->begin($formContainer = $this->global->forms->item(%node)) %line; ' 45 | . '%node ' 46 | . '$this->global->forms->end(); $formContainer = $this->global->forms->current();' 47 | . "\n\n", 48 | $this->name, 49 | $this->position, 50 | $this->content, 51 | ); 52 | } 53 | 54 | 55 | public function &getIterator(): \Generator 56 | { 57 | yield $this->name; 58 | yield $this->content; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/html5.php: -------------------------------------------------------------------------------- 1 | addGroup(); 24 | 25 | $form->addText('query', 'Search:') 26 | ->setHtmlType('search') 27 | ->setHtmlAttribute('autofocus'); 28 | 29 | $form->addInteger('count', 'Number of results:') 30 | ->setDefaultValue(10) 31 | ->addRule($form::Range, 'Must be in range from %d to %d', [1, 100]); 32 | 33 | $form->addFloat('precision', 'Precision:') 34 | ->setHtmlType('range') 35 | ->setDefaultValue(50) 36 | ->addRule($form::Range, 'Precision must be in range from %d to %d', [0, 100]); 37 | 38 | $form->addEmail('email', 'Send to email:') 39 | ->setHtmlAttribute('autocomplete', 'off') 40 | ->setHtmlAttribute('placeholder', 'Optional, but Recommended'); 41 | 42 | $form->addSubmit('submit', 'Send'); 43 | 44 | 45 | if ($form->isSuccess()) { 46 | echo '

Form was submitted and successfully validated

'; 47 | Dumper::dump($form->getValues()); 48 | exit; 49 | } 50 | 51 | 52 | ?> 53 | 54 | 55 | Nette Forms and HTML5 56 | 57 | 58 | 59 |

Nette Forms and HTML5

60 | 61 | render() ?> 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/Bridges/FormsLatte/Nodes/FormPrintNode.php: -------------------------------------------------------------------------------- 1 | name === 'formPrint') { 31 | trigger_error('Tag {formPrint} is deprecated, use Nette\Forms\Blueprint::latte($form)', E_USER_DEPRECATED); 32 | } else { 33 | trigger_error('Tag {formClassPrint} is deprecated, use Nette\Forms\Blueprint::dataClass($form)', E_USER_DEPRECATED); 34 | } 35 | $node = new static; 36 | $node->name = $tag->parser->isEnd() 37 | ? null 38 | : $tag->parser->parseUnquotedStringOrExpression(); 39 | $node->mode = $tag->name; 40 | return $node; 41 | } 42 | 43 | 44 | public function print(PrintContext $context): string 45 | { 46 | return $context->format( 47 | 'Nette\Forms\Blueprint::%raw(' 48 | . ($this->name 49 | ? 'is_object($ʟ_tmp = %node) ? $ʟ_tmp : $this->global->uiControl[$ʟ_tmp]' 50 | : '$this->global->forms->current()') 51 | . ') %2.line; exit;', 52 | $this->mode === 'formPrint' ? 'latte' : 'dataClass', 53 | $this->name, 54 | $this->position, 55 | ); 56 | } 57 | 58 | 59 | public function &getIterator(): \Generator 60 | { 61 | if ($this->name) { 62 | yield $this->name; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Forms/Controls/Button.php: -------------------------------------------------------------------------------- 1 | control->type = 'button'; 25 | $this->setOption('type', 'button'); 26 | $this->setOmitted(); 27 | } 28 | 29 | 30 | /** 31 | * Is button pressed? 32 | */ 33 | public function isFilled(): bool 34 | { 35 | $value = $this->getValue(); 36 | return $value !== null && $value !== []; 37 | } 38 | 39 | 40 | /** 41 | * Bypasses label generation. 42 | */ 43 | public function getLabel($caption = null): Html|string|null 44 | { 45 | return null; 46 | } 47 | 48 | 49 | public function renderAsButton(bool $state = true): static 50 | { 51 | $this->control->setName($state ? 'button' : 'input'); 52 | return $this; 53 | } 54 | 55 | 56 | /** 57 | * Generates control's HTML element. 58 | */ 59 | public function getControl(string|Stringable|null $caption = null): Html 60 | { 61 | $this->setOption('rendered', true); 62 | $caption = $this->translate($caption ?? $this->getCaption()); 63 | $el = (clone $this->control)->addAttributes([ 64 | 'name' => $this->getHtmlName(), 65 | 'disabled' => $this->isDisabled(), 66 | ]); 67 | if ($caption instanceof Html || ($caption !== null && $el->getName() === 'button')) { 68 | $el->setName('button')->setText($caption); 69 | } else { 70 | $el->value = $caption; 71 | } 72 | 73 | return $el; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Bridges/FormsLatte/Nodes/InputNode.php: -------------------------------------------------------------------------------- 1 | outputMode = $tag::OutputKeepIndentation; 33 | $tag->expectArguments(); 34 | 35 | $node = new static; 36 | $node->name = $tag->parser->parseUnquotedStringOrExpression(colon: false); 37 | if ($tag->parser->stream->tryConsume(':')) { 38 | $node->part = $tag->parser->isEnd() || $tag->parser->stream->is(',') 39 | ? new StringNode('') 40 | : $tag->parser->parseUnquotedStringOrExpression(); 41 | } 42 | $tag->parser->stream->tryConsume(','); 43 | $node->attributes = $tag->parser->parseArguments(); 44 | return $node; 45 | } 46 | 47 | 48 | public function print(PrintContext $context): string 49 | { 50 | return $context->format( 51 | 'echo $this->global->forms->item(%node)->' 52 | . ($this->part ? ('getControlPart(%node)') : 'getControl()') 53 | . ($this->attributes->items ? '->addAttributes(%2.node)' : '') 54 | . ' %3.line;', 55 | $this->name, 56 | $this->part, 57 | $this->attributes, 58 | $this->position, 59 | ); 60 | } 61 | 62 | 63 | public function &getIterator(): \Generator 64 | { 65 | yield $this->name; 66 | if ($this->part) { 67 | yield $this->part; 68 | } 69 | yield $this->attributes; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/localization.php: -------------------------------------------------------------------------------- 1 | table = $table; 30 | } 31 | 32 | 33 | /** 34 | * Translates the given string. 35 | */ 36 | public function translate($message, ...$parameters): string 37 | { 38 | return $this->table[$message] ?? $message; 39 | } 40 | } 41 | 42 | 43 | $form = new Form; 44 | 45 | $translator = new MyTranslator(parse_ini_file(__DIR__ . '/localization.ini')); 46 | $form->setTranslator($translator); 47 | 48 | $form->addGroup('Personal data'); 49 | $form->addText('name', 'Your name:') 50 | ->setRequired('Enter your name'); 51 | 52 | $form->addText('age', 'Your age:') 53 | ->setRequired('Enter your age') 54 | ->addRule($form::Integer, 'Age must be numeric value') 55 | ->addRule($form::Range, 'Age must be in range from %d to %d', [10, 100]); 56 | 57 | $countries = [ 58 | 'World' => [ 59 | 'bu' => 'Buranda', 60 | 'qu' => 'Qumran', 61 | 'st' => 'Saint Georges Island', 62 | ], 63 | '?' => 'other', 64 | ]; 65 | $form->addSelect('country', 'Country:', $countries) 66 | ->setPrompt('Select your country'); 67 | 68 | $form->addSubmit('submit', 'Send'); 69 | 70 | 71 | if ($form->isSuccess()) { 72 | echo '

Form was submitted and successfully validated

'; 73 | Dumper::dump($form->getValues()); 74 | exit; 75 | } 76 | 77 | 78 | ?> 79 | 80 | 81 | Nette Forms localization example 82 | 83 | 84 | 85 |

Nette Forms localization example

86 | 87 | render() ?> 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/Forms/Controls/Checkbox.php: -------------------------------------------------------------------------------- 1 | control->type = 'checkbox'; 30 | $this->container = Html::el(); 31 | $this->setOption('type', 'checkbox'); 32 | } 33 | 34 | 35 | /** 36 | * Sets control's value. 37 | * @internal 38 | */ 39 | public function setValue($value): static 40 | { 41 | if (!is_scalar($value) && $value !== null) { 42 | throw new Nette\InvalidArgumentException(sprintf("Value must be scalar or null, %s given in field '%s'.", get_debug_type($value), $this->getName())); 43 | } 44 | 45 | $this->value = (bool) $value; 46 | return $this; 47 | } 48 | 49 | 50 | public function isFilled(): bool 51 | { 52 | return $this->getValue() !== false; // back compatibility 53 | } 54 | 55 | 56 | public function getControl(): Html 57 | { 58 | return $this->container->setHtml($this->getLabelPart()->insert(0, $this->getControlPart())); 59 | } 60 | 61 | 62 | /** 63 | * Bypasses label generation. 64 | */ 65 | public function getLabel($caption = null): Html|string|null 66 | { 67 | return null; 68 | } 69 | 70 | 71 | public function getControlPart(): Html 72 | { 73 | return parent::getControl()->checked($this->value); 74 | } 75 | 76 | 77 | public function getLabelPart(): Html 78 | { 79 | return parent::getLabel(); 80 | } 81 | 82 | 83 | /** 84 | * Returns container HTML element template. 85 | */ 86 | public function getContainerPrototype(): Html 87 | { 88 | return $this->container; 89 | } 90 | 91 | 92 | #[\Deprecated('use getContainerPrototype()')] 93 | public function getSeparatorPrototype(): Html 94 | { 95 | trigger_error(__METHOD__ . '() was renamed to getContainerPrototype()', E_USER_DEPRECATED); 96 | return $this->container; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Bridges/FormsLatte/Nodes/FormNNameNode.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | final class FormNNameNode extends StatementNode 26 | { 27 | public ExpressionNode $name; 28 | public AreaNode $content; 29 | 30 | 31 | public static function create(Tag $tag): \Generator 32 | { 33 | $tag->expectArguments(); 34 | $node = $tag->node = new static; 35 | $node->name = $tag->parser->parseUnquotedStringOrExpression(colon: false); 36 | [$node->content] = yield; 37 | $node->init($tag); 38 | return $node; 39 | } 40 | 41 | 42 | public function print(PrintContext $context): string 43 | { 44 | return $context->format( 45 | '$this->global->forms->begin($form = ' 46 | . ($this->name instanceof StringNode 47 | ? '$this->global->uiControl[%node]' 48 | : '(is_object($ʟ_tmp = %node) ? $ʟ_tmp : $this->global->uiControl[$ʟ_tmp])') 49 | . ') %line;' 50 | . '%node ' 51 | . '$this->global->forms->end();', 52 | $this->name, 53 | $this->position, 54 | $this->content, 55 | ); 56 | } 57 | 58 | 59 | private function init(Tag $tag): void 60 | { 61 | $el = $tag->htmlElement; 62 | 63 | $tag->replaceNAttribute(new AuxiliaryNode(fn(PrintContext $context) => $context->format( 64 | 'echo $this->global->forms->renderFormBegin(%dump, false) %line;', 65 | array_fill_keys(FieldNNameNode::findUsedAttributes($el), null), 66 | $this->position, 67 | ))); 68 | 69 | $el->content = new Latte\Compiler\Nodes\FragmentNode([ 70 | $el->content, 71 | new AuxiliaryNode(fn(PrintContext $context) => $context->format( 72 | 'echo $this->global->forms->renderFormEnd(false) %line;', 73 | $this->position, 74 | )), 75 | ]); 76 | } 77 | 78 | 79 | public function &getIterator(): \Generator 80 | { 81 | yield $this->name; 82 | yield $this->content; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Forms/Controls/MultiSelectBox.php: -------------------------------------------------------------------------------- 1 | setOption('type', 'select'); 30 | } 31 | 32 | 33 | /** 34 | * Sets options and option groups from which to choose. 35 | */ 36 | public function setItems(array $items, bool $useKeys = true): static 37 | { 38 | if (!$useKeys) { 39 | $res = []; 40 | foreach ($items as $key => $value) { 41 | unset($items[$key]); 42 | if (is_array($value)) { 43 | foreach ($value as $val) { 44 | $res[$key][(string) $val] = $val; 45 | } 46 | } else { 47 | $res[(string) $value] = $value; 48 | } 49 | } 50 | 51 | $items = $res; 52 | } 53 | 54 | $this->options = $items; 55 | return parent::setItems(Nette\Utils\Arrays::flatten($items, preserveKeys: true)); 56 | } 57 | 58 | 59 | public function getControl(): Nette\Utils\Html 60 | { 61 | $items = []; 62 | foreach ($this->options as $key => $value) { 63 | $items[is_array($value) ? $this->translate($key) : $key] = $this->translate($value); 64 | } 65 | 66 | return Nette\Forms\Helpers::createSelectBox( 67 | $items, 68 | [ 69 | 'disabled:' => $this->disabledChoices, 70 | ] + $this->optionAttributes, 71 | $this->value, 72 | )->addAttributes(parent::getControl()->attrs)->multiple(true); 73 | } 74 | 75 | 76 | /** @deprecated use setOptionAttribute() */ 77 | public function addOptionAttributes(array $attributes): static 78 | { 79 | $this->optionAttributes = $attributes + $this->optionAttributes; 80 | return $this; 81 | } 82 | 83 | 84 | public function setOptionAttribute(string $name, mixed $value = true): static 85 | { 86 | $this->optionAttributes[$name] = $value; 87 | return $this; 88 | } 89 | 90 | 91 | public function getOptionAttributes(): array 92 | { 93 | return $this->optionAttributes; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /examples/manual-rendering.php: -------------------------------------------------------------------------------- 1 | addText('name') 23 | ->setRequired('Enter your name'); 24 | 25 | $form->addText('age') 26 | ->setRequired('Enter your age'); 27 | 28 | $form->addRadioList('gender', null, [ 29 | 'm' => 'male', 30 | 'f' => 'female', 31 | ]); 32 | 33 | $form->addEmail('email'); 34 | 35 | $form->addSubmit('submit'); 36 | 37 | if ($form->isSuccess()) { 38 | echo '

Form was submitted and successfully validated

'; 39 | Dumper::dump($form->getValues()); 40 | exit; 41 | } 42 | 43 | 44 | ?> 45 | 46 | 47 | 48 | 49 | Nette Forms manual form rendering 50 | 51 | 52 | 53 | 54 | 55 |

Nette Forms manual form rendering

56 | 57 | render('begin') ?> 58 | 59 | errors): ?> 60 | 65 | 66 | 67 |
68 | Personal data 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
getLabel('Your name:') ?>control->cols(35) ?> error ?>
getLabel('Your age:') ?>control->cols(5) ?> error ?>
getLabel('Your gender:') ?>control ?> error ?>
getLabel('Email:') ?>control->cols(35) ?> error ?>
87 |
88 | 89 |
90 | getControl('Send') ?> 91 |
92 | 93 | render('end'); ?> 94 | 95 | 96 | -------------------------------------------------------------------------------- /examples/latte/form-bootstrap5.latte: -------------------------------------------------------------------------------- 1 | {* Generic form template for Bootstrap v5 *} 2 | 3 | {define bootstrap-form, $name} 4 |
5 | {* List for form-level error messages *} 6 |
    7 |
  • {$error}
  • 8 |
9 | 10 |
11 | 12 | {include controls $group->getControls()} 13 |
14 | 15 | {include controls $form->getControls()} 16 |
17 | {/define} 18 | 19 | 20 | {define local controls, array $controls} 21 | {* Loop over form controls and render each one *} 22 |
25 | 26 | {* Label for the control *} 27 |
{label $control /}
28 | 29 |
30 | {include control $control} 31 | {if $control->getOption(type) === button} 32 | {while $iterator->nextValue?->getOption(type) === button} 33 | {input $iterator->nextValue class => "btn btn-secondary"} 34 | {do $iterator->next()} 35 | {/while} 36 | {/if} 37 | 38 | {* Display control-level errors or descriptions, if present *} 39 | {$control->error} 40 | {$control->getOption(description)} 41 |
42 |
43 | {/define} 44 | 45 | 46 | {define local control, Nette\Forms\Controls\BaseControl $control} 47 | {* Conditionally render controls based on their type with appropriate Bootstrap classes *} 48 | {if $control->getOption(type) in [text, select, textarea, datetime, file]} 49 | {input $control class => form-control} 50 | 51 | {elseif $control->getOption(type) === button} 52 | {input $control class => "btn btn-primary"} 53 | 54 | {elseif $control->getOption(type) in [checkbox, radio]} 55 | {var $items = $control instanceof Nette\Forms\Controls\Checkbox ? [''] : $control->getItems()} 56 |
57 | {input $control:$key class => form-check-input}{label $control:$key class => form-check-label /} 58 |
59 | 60 | {elseif $control->getOption(type) === color} 61 | {input $control class => "form-control form-control-color"} 62 | 63 | {else} 64 | {input $control} 65 | {/if} 66 | {/define} 67 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import json from '@rollup/plugin-json'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import typescript from '@rollup/plugin-typescript'; 4 | import terser from '@rollup/plugin-terser'; 5 | import dts from 'rollup-plugin-dts'; 6 | 7 | 8 | // adds a header and calls initOnLoad() in the browser 9 | function fix() { 10 | return { 11 | renderChunk(code) { 12 | code = `/*! 13 | * NetteForms - simple form validation. 14 | * 15 | * This file is part of the Nette Framework (https://nette.org) 16 | * Copyright (c) 2004 David Grudl (https://davidgrudl.com) 17 | */ 18 | ` 19 | + code; 20 | code = code.replace('global.Nette = factory()', 'global.Nette?.noInit ? (global.Nette = factory()) : (global.Nette = factory()).initOnLoad()'); 21 | code = code.replace(/\/\*\*.*\* \*\//s, ''); 22 | return code; 23 | }, 24 | }; 25 | } 26 | 27 | function spaces2tabs() { 28 | return { 29 | renderChunk(code) { 30 | return code.replaceAll(' ', '\t'); 31 | }, 32 | }; 33 | } 34 | 35 | 36 | export default [ 37 | { // TODO: consider the possibility of cutting off the UMD versions completely due to collision 38 | input: 'src/assets/index.umd.ts', 39 | output: [ 40 | { 41 | format: 'umd', 42 | name: 'Nette', 43 | dir: 'src/assets/dist', 44 | entryFileNames: 'nette-forms.umd.js', 45 | generatedCode: 'es2015', 46 | }, 47 | { 48 | format: 'umd', 49 | name: 'Nette', 50 | dir: 'src/assets/dist', 51 | entryFileNames: 'nette-forms.umd.min.js', 52 | generatedCode: 'es2015', 53 | plugins: [ 54 | terser(), 55 | ], 56 | }, 57 | ], 58 | plugins: [ 59 | json(), 60 | nodeResolve(), 61 | typescript(), 62 | fix(), 63 | spaces2tabs(), 64 | ], 65 | }, 66 | 67 | { 68 | input: 'src/assets/index.esm.ts', 69 | output: [ 70 | { 71 | format: 'es', 72 | dir: 'src/assets/dist', 73 | entryFileNames: 'nette-forms.esm.js', 74 | generatedCode: 'es2015', 75 | }, 76 | { 77 | format: 'es', 78 | dir: 'src/assets/dist', 79 | entryFileNames: 'nette-forms.esm.min.js', 80 | generatedCode: 'es2015', 81 | plugins: [ 82 | terser(), 83 | ], 84 | }, 85 | ], 86 | plugins: [ 87 | json(), 88 | nodeResolve(), 89 | typescript(), 90 | spaces2tabs(), 91 | ], 92 | }, 93 | 94 | { 95 | input: 'src/assets/index.esm.ts', 96 | output: [{ 97 | file: 'src/assets/dist/nette-forms.d.ts', 98 | format: 'es', 99 | }], 100 | plugins: [ 101 | dts(), 102 | spaces2tabs(), 103 | ], 104 | }, 105 | ]; 106 | -------------------------------------------------------------------------------- /src/Bridges/FormsLatte/Nodes/LabelNode.php: -------------------------------------------------------------------------------- 1 | */ 37 | public static function create(Tag $tag): \Generator 38 | { 39 | if ($tag->isNAttribute()) { 40 | throw new CompileException('Did you mean