Form for Creating and Editing a Record

How to properly implement adding and editing a record in Nette, using the same form for both?

In many cases, the forms for adding and editing a record are the same, differing only by the label on the button. We will show examples of simple presenters where we use the form first to add a record, then to edit it, and finally combine the two solutions.

Adding a Record

An example of a presenter used to add a record. We will leave the actual database work to the Facade class, whose code is not relevant for the example.

use Nette\Application\UI\Form;

class RecordPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private Facade $facade,
	) {
	}

	protected function createComponentRecordForm(): Form
	{
		$form = new Form;

		// ... add form fields ...

		$form->onSuccess[] = [$this, 'recordFormSucceeded'];
		return $form;
	}

	public function recordFormSucceeded(Form $form, array $data): void
	{
		$this->facade->add($data); // add record to database
		$this->flashMessage('Successfully added');
		$this->redirect('...');
	}

	public function renderAdd(): void
	{
		// ...
	}
}

Editing a Record

Now let's see what a presenter used to edit a records would look like:

use Nette\Application\UI\Form;

class RecordPresenter extends Nette\Application\UI\Presenter
{
	private $record;

	public function __construct(
		private Facade $facade,
	) {
	}

	public function actionEdit(int $id): void
	{
		$record = $this->facade->get($id);
		if (
			!$record // verify the existence of the record
			|| !$this->facade->isEditAllowed(/*...*/) // check permissions
		) {
			$this->error(); // 404 error
		}

		$this->record = $record;
	}

	protected function createComponentRecordForm(): Form
	{
		// verify that the action is 'edit'
		if ($this->getAction() !== 'edit') {
			$this->error();
		}

		$form = new Form;

		// ... add form fields ...

		$form->setDefaults($this->record); // set default values
		$form->onSuccess[] = [$this, 'recordFormSucceeded'];
		return $form;
	}

	public function recordFormSucceeded(Form $form, array $data): void
	{
		$this->facade->update($this->record->id, $data); // update record
		$this->flashMessage('Successfully updated');
		$this->redirect('...');
	}
}

In the action method, which is invoked right at the beginning of the presenter lifecycle, we verify the existence of the record and the user's permission to edit it.

We store the record in the $record property so that it is available in the createComponentRecordForm() method for setting defaults, and recordFormSucceeded() for the ID. An alternative solution would be to set the default values directly in actionEdit() and the ID value, which is part of the URL, is retrieved using getParameter('id'):

	public function actionEdit(int $id): void
	{
		$record = $this->facade->get($id);
		if (
			// verify existence and check permissions
		) {
			$this->error();
		}

		// set default form values
		$this->getComponent('recordForm')
			->setDefaults($record);
	}

	public function recordFormSucceeded(Form $form, array $data): void
	{
		$id = (int) $this->getParameter('id');
		$this->facade->update($id, $data);
		// ...
	}
}

However, and this should be the most important takeaway from all the code, we need to make sure that the action is indeed edit when we create the form. Because otherwise the validation in the actionEdit() method wouldn't happen at all!

Same Form for Adding and Editing

And now we will combine both presenters into one. Either we could distinguish which action is involved in the createComponentRecordForm() method and configure the form accordingly, or we can leave it directly to the action-methods and get rid of the condition:

class RecordPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private Facade $facade,
	) {
	}

	public function actionAdd(): void
	{
		$form = $this->getComponent('recordForm');
		$form->onSuccess[] = [$this, 'addingFormSucceeded'];
	}

	public function actionEdit(int $id): void
	{
		$record = $this->facade->get($id);
		if (
			!$record // verify the existence of the record
			|| !$this->facade->isEditAllowed(/*...*/) // check permissions
		) {
			$this->error(); // 404 error
		}

		$form = $this->getComponent('recordForm');
		$form->setDefaults($record); // set defaults
		$form->onSuccess[] = [$this, 'editingFormSucceeded'];
	}

	protected function createComponentRecordForm(): Form
	{
		// verify that the action is 'add' or 'edit'
		if (!in_array($this->getAction(), ['add', 'edit'])) {
			$this->error();
		}

		$form = new Form;

		// ... add form fields ...

		return $form;
	}

	public function addingFormSucceeded(Form $form, array $data): void
	{
		$this->facade->add($data); // add record to database
		$this->flashMessage('Successfully added');
		$this->redirect('...');
	}

	public function editingFormSucceeded(Form $form, array $data): void
	{
		$id = (int) $this->getParameter('id');
		$this->facade->update($id, $data); // update record
		$this->flashMessage('Successfully updated');
		$this->redirect('...');
	}
}