├── 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 |
17 | {/define}
18 |
19 |
20 | {define local controls, array $controls}
21 | {* Loop over form controls and render each one *}
22 |
23 |
26 |
27 | | {label $control /} |
28 |
29 |
30 | {input $control}
31 |
32 | {$control->getOption(description)}
33 | {$control->error}
34 | |
35 |
36 |
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 |
61 | errors as $error): ?>
62 |
63 |
64 |
65 |
66 |
67 |
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 |
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