Forms

Forms #

Nette Forms is great library for forms. However, in many cases, you will need more logic around your input controls. To make things easier to use, you will need to extend at least two classes:

  • Nette\Application\UI\Form
  • Nette\Forms\Container

In this two classes, there should be available methods to add your custom input controls. To make this possible, create namespace for your implementations:

app/
|-- Forms
|   |-- Controls/
|   |-- Extensions/
|   |-- Container.php
|   |-- Form.php
|   |-- RegisterControls.php

Two directories in this namespace is created for future use:

  • Controls for implementation of custom input controls, extended input controls, etc.
  • Extensions for traits to used in your custom input controls.

Firstly, define Form.php as follows:

<?php

declare(strict_types=1);

namespace App\Forms;

use Nette\Application\UI\Form as NetteForm;

/**
 * @property-read string $name
 */
final class Form extends NetteForm
{
    use RegisterControls;

}

To use the same methods to add your custom input controls, you will need also Container.php as follows:

<?php

declare(strict_types=1);

namespace App\Forms;

use Nette\Forms\Container as NetteContainer;

/**
 * @property-read string $htmlId
 */
class Container extends NetteContainer
{
	use RegisterControls;

	private ?Translator $translator = null;

	public function getHtmlId(): string
	{
		return sprintf('container-%s', $this->lookupPath());
	}

}

Controls Registration #

To make your methods available to all places (all factories) where you will build your forms, whether you are working with Form or Container, you need to define your methods in used trait named RegisterControls.

<?php

namespace App\Forms;

trait RegisterControls
{

    // here will you add your methods for custom input controls

	/**
	 * @param int|string $name
	 *
	 * @return Container
	 */
	public function addContainer($name): Container
	{
		$control = new Container();
		$control->currentGroup = $this->currentGroup;
		if ($this->currentGroup !== null) {
			$this->currentGroup->add($control);
		}
		return $this[$name] = $control;
	}
	
}

Method addContainer ensures, that all created containers will have your custom class App\Forms\Container with all your defined methods from trait RegisterControls.

Even creating a shortcut methods (for logic you use on many places in your code) is simple, you can create methods here for example for:

  • translatable parts of your forms,
  • replicable containers of your forms,
  • common container for entity publication settings,
  • smart image uploads with preview or cropping,
  • select boxes with custom layouts (e.g. with flags),
  • wysiwyg editors,
  • date and time pickers,
  • color picker,
  • formatted number inputs,
  • formatted phone inputs,
  • checkbox switches.

Basic Form Factory #

Across your application, you should not create forms with just calling $form = new Form();, mainly because if you want to change behaviour of all your forms, you will need to edit every place where this is.

It is a best practice to create BasicFormFactory where this is handled.

<?php

declare(strict_types=1);

namespace App\Forms;

final class BasicFormFactory
{

	public function create(): Form
	{
		return new Form();
	}

}

Basic Form Factory with Translator #

If you are working with translator, this is the place where to set your translator – only on one place in your application. Every factory is then clean of this.

<?php

declare(strict_types=1);

namespace App\Forms;

use Nette\Localization\Translator;

final class BasicFormFactory
{
	private Translator $translator;

	public function __construct(Translator $translator)
	{
		$this->translator = $translator;
	}

	public function create(): Form
	{
		$form = new Form();
		$form->setTranslator($this->translator);

		return $form;
	}

}

Select Items with Translator #

If you are working with translator to make your application multi-language, a little work around your items (whether it is select box, radio list, etc.) will be needed. There are two possible requirements:

  • translate labels of your items – if you are selecting for example status of your entity,
  • do not translate labels – if you are selecting relation to another entity, where only name should be shown.

Currently, we recommend to use contributte/translation, which is great library to handle translations, configure different sources (loaders), configure locales, fallback locales, resolvers, etc.

Translatable Items #

In cases where items should be translated (e.g. status of your entity), there is need to create the items with a way where array contains:

  • key – with the value you use in your entity,
  • value – with translation key you need to translate using translator.

This will be used again across all your form factories in your application. To make this code simple, create a utility method in your Form class to handle this:

final class Form extends NetteForm
{

	/**
	 * @param string $prefix
	 * @param array<string, string> $values
	 *
	 * @return array<string, string>
	 */
	public static function items(string $prefix, array $values): array
	{
		return collect(array_combine($values, $values))
			->map(fn(string $value) => "$prefix.$value")
			->all();
	}

}

For shorter notation, we are using illuminate/collections to handle arrays with functional way.

Why is it in Form class? That is simple – just for convenience in use. You probably already use Form class in your factory for example in conditions definition. Then in your input you will use something like this:

$statuses = ['new', 'processing', 'finished'];
$form->addSelect('status')
    ->setPrompt('form.prompt.select')
    ->setItems(Form::items('form.user.statuses', $statuses))

To make this code more dynamic – based on your entity enums, you can fetch enum values right from your orm like this:

$metadata = $this->orm->orders->getEntityMetadata();
$property = $metadata->getProperty('status');

$form->addSelect('status')
    ->setPrompt('form.prompt.select')
    ->setItems(Form::items('form.user.statuses', $property->enum))

Items without Translations #

In cases where you want to select related entity (e.g. owner of a given record) or similar cases, you will probably use something like this:

$ownerItems = $this->orm->users->findAll()->fetchPairs('id', 'name');

$form->addSelect('owner')
    ->setPrompt('form.prompt.select')
    ->setItems($ownerItems);

In this case, your translation extension will show you that every name is not translated. This can be suppressed with NotTranslate:

$ownerItems = collect($this->orm->users->findAll()->fetchPairs('id', 'name'))
    ->map(fn (string $title) => new NotTranslate($title))
    ->all();

$form->addSelect('owner')
    ->setPrompt('form.prompt.select')
    ->setItems($ownerItems);