Formulář s dynamickými selecty a setDefaults

toka
Člen | 253
+
0
-

Pokud mám připravený formulář dle postupu zde a chci formuláři např. v editaci nastavit výchozí hodnoty pro závislý select, např. v metodě actionEdit(), dostávám chybovou hlášku, že chci nastavit hodnotu, která neexistuje, protože povolené je prázdné pole.

Pokud to provedu přímo v createComponentForm, tak to funguje správně, ale nepřijde mi to jako správné řešení. Jak toto řešíte? Díky.

Nefunguje

class DemoPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private World $world,
	) {}

	protected function createComponentForm(): Form
	{
		$form = new Form;
		$country = $form->addSelect('country', 'Stát:', $this->world->getCountries())
			->setPrompt('----');

		$city = $form->addSelect('city', 'Město:');

		$items = [];

		foreach ($this->world->getCountries() as $id => $name) {
			$items[$id] = $this->world->getCities($id);
		}

		$city = $form->addSelect('city', 'Město:')
			->setHtmlAttribute('data-depends', $country->getHtmlName())
			->setHtmlAttribute('data-items', $items);

		$form->onAnchor[] = fn() =>
			$city->setItems($country->getValue()
				? $this->world->getCities($country->getValue())
				: []);

		// $form->onSuccess[] = ...
		return $form;
	}

	public function actionEdit(int $id)
	{
		// Data jsem získal někde s databáze

		$this['form']->setDefaults('...'); // Value '1' is out of allowed set [] in field 'city'
	}
}

Funguje

class DemoPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private World $world,
	) {}

	protected function createComponentForm(): Form
	{
		$form = new Form;
		$country = $form->addSelect('country', 'Stát:', $this->world->getCountries())
			->setPrompt('----');

		$city = $form->addSelect('city', 'Město:');

		$items = [];

		foreach ($this->world->getCountries() as $id => $name) {
			$items[$id] = $this->world->getCities($id);
		}

		$city = $form->addSelect('city', 'Město:')
			->setHtmlAttribute('data-depends', $country->getHtmlName())
			->setHtmlAttribute('data-items', $items);

		$form->onAnchor[] = fn() =>
			$city->setItems($country->getValue()
				? $this->world->getCities($country->getValue())
				: []);

		// $form->onSuccess[] = ...

		// Data jsem získal někde s databáze
		$form->->setDefaults('...'); // Funguje

		return $form;
	}
}
m.brecher
Generous Backer | 758
+
0
-

@toka

Pokud to provedu přímo v createComponentForm, tak to funguje správně, ale nepřijde mi to jako správné řešení.

Naopak, je běžnou praxí, že modelová třída řídí formulář. Co s tím má presenter společného? Je to záležitost formuláře a modelu.

Nicméně, sosat data do formuláře v akci presenteru lze a asi bude stačit odložit tuto akci na pozdější fázi životního cyklu formuláře:

public function actionEdit(int $id)
	{
		// Data jsem získal někde s databáze

		$this['form']->onRender[] = fn() => $this['form']->setDefaults($data);   // calback formou arrow funkce
	}

Nebo totéž přímo v createComponent:

public functioncreateComponentForm(): Form
	{
		$form = new Form;
		// .....

		$form->onRender[] = fn() => $form->setDefaults($data);   // calback formou arrow funkce
	}
toka
Člen | 253
+
0
-

Děkuji za reakci.

Toto právě vůbec nefunguje, s tím onRender[] to je jako bez něj, jako když tam bude pouze $this['form']->setDefaults($data).

A mít to zase v tom function createComponentForm() zase vyžaduje nějakým způsobem do té funkce dostat parametry navíc, kdy mi přijde logické to dělat v tom action. Pokud ten formulář nemá dynamické obsahy v selectech, tak je zcela funkční dát to actionEdit() to setDefaults() tak, jak to mám uvedeno výše.

Jde tedy o to, že ten select neobsahuje v tu dobu žádná data a je to prázdné pole.

public function actionEdit(int $id)
{
	// Data jsem získal někde s databáze

	// Nefunguje
	$this['form']->onRender[] = fn() => $this['form']->setDefaults($data);
}
toka
Člen | 253
+
0
-

Chci vlastně dosáhnout toho samého co je v ukázce.

Tam v actionEdit není nic jiného, než co chci já – $form->setDefaults($record).

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 // oveření existence záznamu
			|| !$this->facade->isEditAllowed(/*...*/) // kontrola oprávnění
		) {
			$this->error(); // chyba 404
		}

		$form = $this->getComponent('recordForm');
		$form->setDefaults($record); // nastavení výchozích hodnot
		$form->onSuccess[] = [$this, 'editingFormSucceeded'];
	}

	protected function createComponentRecordForm(): Form
	{
		// ověříme, že akce je 'add' nebo 'edit'
		if (!in_array($this->getAction(), ['add', 'edit'])) {
			$this->error();
		}

		$form = new Form;

		// ... přidáme políčka formuláře ...

		return $form;
	}

	public function addingFormSucceeded(Form $form, array $data): void
	{
		$this->facade->add($data); // přidání záznamu do databáze
		$this->flashMessage('Successfully added');
		$this->redirect('...');
	}

	public function editingFormSucceeded(Form $form, array $data): void
	{
		$id = (int) $this->getParameter('id');
		$this->facade->update($id, $data); // aktualizace záznamu
		$this->flashMessage('Successfully updated');
		$this->redirect('...');
	}
}
Martk
Člen | 656
+
0
-

Je opravdu ten funkční kód 1:1? Protože se mi zdá, že by neměl fungovat a nebo co jsou za výchozí hodnoty v tom funkčním?

toka
Člen | 253
+
0
-

Ano, pokud je to setDefaults() v tom createComponentForm(), tak to funguje dle očekávání – správně. Aktuálně to tak používám, ale nepřijde mi to ideální řešení – nelíbí se mi.

Rád bych to řešil v action.

Skutečně se správně doplní ta hodnota toho druhého selectu, který je závislý na prvním.

Editoval toka (29. 11. 2023 9:20)

Martk
Člen | 656
+
+1
-

Tohle mi totiž nefunguje, ani nemůže, protože nastavím city na CZ_CITY2, ale onAnchor se ještě nezavolal, takže je z toho chyba: Value 'CZ_CITY_2' is out of allowed set [] in field 'city', protože items je prázdné pole.

	protected function createComponentTest()
	{
		$countries = ['CZ', 'SK', 'PL'];
		$cities = [
			'CZ' => ['CZ_CITY_1', 'CZ_CITY_2', 'CZ_CITY_3'],
			'SK' => ['SK_CITY_1', 'SK_CITY_2', 'SK_CITY_3'],
			'PL' => ['PL_CITY_1', 'PL_CITY_2', 'PL_CITY_3'],
		];

		$form = new Form();
		$country = $form->addSelect('country', 'Stát:', array_combine($countries, $countries))
			->setPrompt('----');

		$city = $form->addSelect('city', 'Město:')
			->setHtmlAttribute('data-depends', $country->getHtmlName())
			->setHtmlAttribute('data-items', $cities);

		$form->onAnchor[] = fn() =>
			$city->setItems($country->getValue()
				? array_combine($cities[$country->getValue()], $cities[$country->getValue()])
				: []);

		$form->setDefaults([
			'country' => 'CZ',
			'city' => 'CZ_CITY_2',
		]);

		return $form;
	}

Proto se divím, že ti to funguje. Zkouším to na v3.1.11.

Fungovat by mohlo něco takového:

	protected function createComponentTest()
	{
		$countries = ['CZ', 'SK', 'PL'];
		$cities = [
			'CZ' => ['CZ_CITY_1', 'CZ_CITY_2', 'CZ_CITY_3'],
			'SK' => ['SK_CITY_1', 'SK_CITY_2', 'SK_CITY_3'],
			'PL' => ['PL_CITY_1', 'PL_CITY_2', 'PL_CITY_3'],
		];

		$form = new Form();
		$country = $form->addSelect('country', 'Stát:', array_combine($countries, $countries))
			->setPrompt('----');

		$form['city'] = (new DependentSelectBox('Město:', $country, fn(?string $value) =>
			$value
			? array_combine($cities[$value], $cities[$value])
			: []))
			->setHtmlAttribute('data-depends', $country->getHtmlName())
			->setHtmlAttribute('data-items', $cities);

		$form->setDefaults([
			'country' => 'CZ',
			'city' => 'CZ_CITY_2',
		]);

		return $form;
	}
<?php declare(strict_types = 1);

namespace App;

use Nette\Forms\Control;
use Nette\Forms\Controls\SelectBox;

final class DependentSelectBox extends SelectBox
{

	/** @var callable */
	private $getItems;

	public function __construct($label, private Control $dependency, callable $getItems)
	{
		parent::__construct($label);

		$this->getItems = $getItems;
	}

	public function loadHttpData(): void
	{
		$this->loadItems();

		parent::loadHttpData();
	}

	public function setValue(mixed $value): void
	{
		$this->loadItems();

		parent::setValue($value);
	}

	private function loadItems(): void
	{
		if (isset($this->getItems)) { // BaseControl calls $this->setValue(null), skip it
			$this->setItems(($this->getItems)($this->dependency->getValue()));
		}
	}

}