Perzistentní parametry po odeslání formuláře v komponentě

roman.campula
Člen | 60
+
0
-

Zdravím,
už jsem si myslel, že perzistentním parametrům rozumím, ale asi ještě ne.

Mám komponentu na výběr odpovědí k otázce (klasický test), celkem 10 otázek. Chci, abych po se výběru odpovědi odeslal ajaxem formulář, inkrementovalo se ID otázky a překreslila komponenta. Tím bych viděl druhou otázku a tak dále.

Místo toho však vidím stále první otázku. Předpokládám, že je to tím, že po překreslení se v konstruktoru asi znovu nastaví questionId = 1. Jak mám tedy questionId po odeslání formuláře inkrementovat? Je potřeba to pak někde odchytávat z parametrů URL?

Předem díky za pomoc, toto už řeším poněkolikáté v různých variantách a ještě se nezadařilo.

presenter (zjednodušeno)

function createComponentTestControl(): ParticipationModule\Components\TestControl {
  $testControl = $this->testControlFactory->create($this->actualPresenter);

  return $testControl;
}

komponenta (zjednodušeno)

class TestControl extends Nette\Application\UI\Control {
  /** @persistent */
  public int $questionId;

  function __construct() {
    $this->questionId = 1;
  }

  function render() {
    $question = $this->testQuestionsFacade->getById($this->questionId);
    $this->template->question = $question;
    $this->template->setFile(__DIR__ . "/templates/default.latte");
    $this->template->render();
  )

  function createComponentForm(): Nette\Application\UI\Form {
    $answers = $this->testAnswersFacade->findBy(["testQuestion_id" => $this->questionId])
                                       ->fetchPairs("id", "text");

    $form = $this->baseFormFactory->create();

    $form->addHidden("questionId", $this->getParameter("questionId"));
    $form->addRadioList("answer", null, $answers)
         ->setRequired();
    $form->addSubmit("save", "Další otázka");

    $form->onSuccess[] = [$this,
                          "formSucceeded"];

    return $form;
  }

  function formSucceeded($form, $values) {
    if ($values->answer) {
      try {
        $testRealAnswerEntity = new ParticipationModule\Model\TestRealAnswerEntity;
        $testRealAnswerEntity->testQuestion_id = $values->questionId;
        $testRealAnswerEntity->testAnswer_id = $values->answer;

        $testRealAnswer = $this->testRealAnswersFacade->insert($testRealAnswerEntity);

      } catch (\Throwable $e) {
        Debugger::log($e, Debugger::EXCEPTION);

        $this->getPresenter()->flashMessage("Odpověď se nepodařilo uložit", "error");
      } finally {
        $this->questionId = $this->questionId + 1;
        $this->redrawControl();
      }
    }
  }
}
Marek Bartoš
Nette Blogger | 1167
+
0
-

Hádám že hledáš persistentní komponenty.
https://doc.nette.org/…n/components#…

roman.campula
Člen | 60
+
0
-

To si nejsem jistý. V dokumentaci se píše, že u perzistentních komponent se „její persistentní parametry přenáší i mezi různými akcemi presenteru nebo mezi více presentery“. To já asi nepotřebuji. Jsem pořád na stejné akci i presenteru. Pouhé přidání

/**
 * Online test
 * @persistent(testControl)    // nebo TestControl nebo test nebo Test
 */
class TestPresenter extends Nette\Application\UI\Presenter {
}

nepomohlo.

Šaman
Člen | 2635
+
0
-

Nebude problém v tom, že v kontruktoru nastavuješ natvrdo questionId na 1? RedrawControl vytvoří další request na pozadí, incrementovaná questionId se v konstruktoru přepíše a máš tam zase jedničku.

function __construct() {
    $this->questionId = 1;
  }

Podle dokumentace od boku (teď jsem nějakou dobu persistentní parametry nepoužíval):

/** @persistent */
public int $questionId = 1;

function __construct() {
}

Plus označení komponenty jako perzistentní.

Btw. nejsi stále na stejné akci presenteru (resp. nejsi ve stejném requestu). Po odeslání formuláře dojde k odeslání a novému requestu, ať už viditelně, nebo skrytě ajaxově.

Editoval Šaman (13. 11. 2021 13:56)

roman.campula
Člen | 60
+
0
-

Tak dobře, díky za navedení. Každopádně: komponenta je perzistentní

/**
 * @persistent(testControl)
 */
class TestPresenter extends Nette\Application\UI\Presenter {
(...)
}

proměnná v komponentě je také perzistentní a v konstruktoru nic není

class TestControl extends Nette\Application\UI\Control {
(...)
  /** @persistent */
  public int $questionId = 1;
}
(...)

ve funkci render() je správná hodnota (inkrementovaná o 1)

function render() {
  dump($this->questionId); // 2
}

ale ve funkci createComponentForm() je stále původní hodnota questionId z předchozí otázky/zobrazení

function createComponentForm(): Nette\Application\UI\Form {
  $form = $this->baseFormFactory->create();
  $form->addHidden("questionId", $this->questionId); // 1
}

To znamená, že se createComponentForm() zavolá dříve, než je nastavena proměnná $this->questionId?

Editoval roman.campula (13. 11. 2021 15:04)

Martk
Člen | 655
+
0
-

Spíše to vypadá na nekorektní překreslení. Vše je v životním cyklu. Takže cesta je zjednodušeně taková:

request → vytvoření komponenty → vytvoření formuláře (otázka č.1), protože se musí nejprve zpracovat → událost onSuccess (inkrementace questionId) → žádost o překreslení → začátek vykreslení šablony → vyžádání formuláře (formulář byl už dříve vytvořen, aby byl zpracován, takže pořád bude otázka č.1, takže se nevykoná znova metoda createComponentForm) → tvůj problém.

Kdybys dal namísto redrawControl → redirect('this') bez ajaxu tak vše bude fungovat, protože budou 2 requesty.

Editoval Martk (13. 11. 2021 22:01)

Milo
Nette Core | 1283
+
+1
-

Zhruba jak píše @Martk. Metoda createComponentForm() se zavolá pouze jednou a v té nastavíš formuláři současné questonId. A zavolá se dost brzy před renderováním, aby formulář mohl přijmout POST data. Potom se vyvolá onSuccess callback a ty v něm questionId inkremetuješ, jenže formulář se o tom už nedozví. Kdybys teď udělal HTTP redirect, questionId po něm bude očekávaně 2. Ale ty ho neuděláš, takže musíš nějak změnit hodnoty ve formuláři na ty další, aby až se vykreslí jako HTML do snippetu, zobrazoval už druhou otázku. Zjednodušeně například:

function formSucceeded($form, $values) {
	# ... uložení odpovědi, vše OK
	$this->questionId++;
	$form->setValues([
		'id' => $this->questionId,
		'answer' => ...,
	]);
}

Pár poznámek:

  • formuláře jsou v tomhle trochu extra, protože přenášejí svoje parametry v POST a ne v URL
  • persistentní parametr a hidden políčko se duplikují – zvaž, jestli nepoužít pouze jedno
  • dělat navazující formuláře správně ajaxově je celkem těžké, třeba aby se vykreslilo něco smysluplného při stisku F5 kdykoliv v průběhu
roman.campula
Člen | 60
+
0
-

Díky moc za rady, pomohly, vše funguje a já jsem si rozšířil obzory.

Pro další generace uvádím výsledný funkční kód. Shrnutí: Na stránce je test s otázkami, ke každé několik odpovědí. Po zaznamenání odpovědi a přechodu na další otázku se předchozí odpověď uloží do databáze a přejde se na další otázku. Po poslední se zobrazí vyhodnocení (není součástí kódu, to už je mimo téma).

TestPresenter.php

class TestPresenter extends Nette\Application\UI\Presenter {
  use Traits\BaseSettingsTrait;
  use Traits\FunctionsTrait;
  use Traits\ParticipationLayoutTrait;

  private ParticipationModule\Model\TestAnswersFacade $testAnswersFacade;
  private ParticipationModule\Model\TestQuestionsFacade $testQuestionsFacade;
  private ParticipationModule\Model\TestRealAnswersFacade $testRealAnswersFacade;
  private ParticipationModule\Components\ITestControlFactory $testControlFactory;

  /**
   * Konstruktor
   *
   * @param ParticipationModule\Model\TestAnswersFacade $testAnswersFacade
   * @param ParticipationModule\Model\TestQuestionsFacade $testQuestionsFacade
   * @param ParticipationModule\Model\TestRealAnswersFacade $testRealAnswersFacade
   * @param ParticipationModule\Components\ITestControlFactory $testControlFactory
   */
  function __construct(ParticipationModule\Model\TestAnswersFacade $testAnswersFacade,
                       ParticipationModule\Model\TestQuestionsFacade $testQuestionsFacade,
                       ParticipationModule\Model\TestRealAnswersFacade $testRealAnswersFacade,
                       ParticipationModule\Components\ITestControlFactory $testControlFactory) {
    parent::__construct();
    $this->testAnswersFacade = $testAnswersFacade;
    $this->testQuestionsFacade = $testQuestionsFacade;
    $this->testRealAnswersFacade = $testRealAnswersFacade;
    $this->testControlFactory = $testControlFactory;
  }

  /**
   * Vykreslí výchozí zobrazení
   */
  function renderDefault() {
    if ($this["testControl"]->getUserToken() === "") {
      $this["testControl"]->setUserToken(Nette\Utils\Random::generate(8, "a-zA-Z0-9"));
    }
  }

  /**
   * Vytvoří test
   *
   * @return ParticipationModule\Components\TestControl
   */
  function createComponentTestControl(): ParticipationModule\Components\TestControl {
    $testControl = $this->testControlFactory->create($this->actualPresenter);

    return $testControl;
  }
}

TestControl.php

class TestControl extends Nette\Application\UI\Control {
  private Nette\Database\Table\ActiveRow $actualPresenter;
  private Nette\Localization\Translator $translator;
  private AppModule\Model\BaseFormFactory $baseFormFactory;
  private AppModule\Model\LogsFacade $logsFacade;
  private ParticipationModule\Model\TestAnswersFacade $testAnswersFacade;
  private ParticipationModule\Model\TestQuestionsFacade $testQuestionsFacade;
  private ParticipationModule\Model\TestRealAnswersFacade $testRealAnswersFacade;

  /** @persistent */
  public string $userToken = "";

  /** @persistent */
  public int $questionId = 1;

  /**
   * Konstruktor
   *
   * @param Nette\Database\Table\ActiveRow $actualPresenter
   * @param Nette\Localization\Translator $translator
   * @param AppModule\Model\BaseFormFactory $baseFormFactory
   * @param AppModule\Model\LogsFacade $logsFacade
   * @param ParticipationModule\Model\TestAnswersFacade $testAnswersFacade
   * @param ParticipationModule\Model\TestQuestionsFacade $testQuestionsFacade
   * @param ParticipationModule\Model\TestRealAnswersFacade $testRealAnswersFacade
   */
  function __construct(Nette\Database\Table\ActiveRow $actualPresenter,
                       Nette\Localization\Translator $translator,
                       AppModule\Model\BaseFormFactory $baseFormFactory,
                       AppModule\Model\LogsFacade $logsFacade,
                       ParticipationModule\Model\TestAnswersFacade $testAnswersFacade,
                       ParticipationModule\Model\TestQuestionsFacade $testQuestionsFacade,
                       ParticipationModule\Model\TestRealAnswersFacade $testRealAnswersFacade) {
    $this->actualPresenter = $actualPresenter;
    $this->translator = $translator;
    $this->baseFormFactory = $baseFormFactory;
    $this->logsFacade = $logsFacade;
    $this->testAnswersFacade = $testAnswersFacade;
    $this->testQuestionsFacade = $testQuestionsFacade;
    $this->testRealAnswersFacade = $testRealAnswersFacade;
  }

  /**
   * Vrátí uživatelský token
   *
   * @return string
   */
  function getUserToken(): string {
    return $this->userToken;
  }

  /**
   * Nastaví uživatelský token
   *
   * @param string $userToken
   */
  function setUserToken(string $userToken) {
    $this->userToken = $userToken;
  }

  /**
   * Vrátí číslo otázky
   *
   * @return int
   */
  function getQuestionId(): int {
    return $this->questionId;
  }

  /**
   * Nastaví číslo otázky
   *
   * @param int $questionId
   */
  function setQuestionId(int $questionId) {
    $this->questionId = $questionId;
  }

  /**
   * Vykreslí výchozí zobrazení
   */
  function render() {
    $this->questionId = $this->getParameter("questionId");

    $question = $this->testQuestionsFacade->getById($this->questionId);
    $questionsCount = $this->testQuestionsFacade->getAll()
                                                ->count();
    $answers = $question->related("testAnswers");

    $this->template->answers = $answers;
    $this->template->question = $question;
    $this->template->questionId = $this->questionId;
    $this->template->questionsCount = $questionsCount;
    $this->template->setFile(__DIR__ . "/templates/default.latte");
    $this->template->render();
  }

  /**
   * Vytvoří formulář pro výběr odpovědi
   *
   * @return Nette\Application\UI\Form
   */
  function createComponentForm(): Nette\Application\UI\Form {
    $answers = $this->testAnswersFacade->findBy(["testQuestion_id" => $this->questionId])
                                       ->fetchPairs("id", "text");

    $form = $this->baseFormFactory->create();

    $form->setTranslator($this->translator);
    $form->addRadioList("answer", null, $answers)
         ->setRequired();
    $form->addSubmit("save");

    $form->onSuccess[] = [$this,
                          "formSucceeded"];

    return $form;
  }

  /**
   * Uloží odpověď
   *
   * @param $form
   * @param $values
   * @throws Nette\Application\AbortException
   */
  function formSucceeded($form, $values) {
    if ($values->answer) {
      try {
        $testRealAnswerEntity = new ParticipationModule\Model\TestRealAnswerEntity;
        $testRealAnswerEntity->userToken = $this->userToken;
        $testRealAnswerEntity->testQuestion_id = $this->questionId;
        $testRealAnswerEntity->testAnswer_id = $values->answer;

        $testRealAnswer = $this->testRealAnswersFacade->insert($testRealAnswerEntity);

        $this->logsFacade->newLog($this->actualPresenter->id, ["action" => "default",
                                                               "testRealAnswer" => ["id" => $testRealAnswer->id,
                                                                                    "userToken" => $testRealAnswer->userToken],
                                                               "testQuestion" => ["id" => $testRealAnswer->testQuestion_id]]);
      } catch (\Throwable $e) {
        Debugger::log($e, Debugger::EXCEPTION);

        $this->getPresenter()->flashMessage("Odpověď se nepodařilo uložit", "error");
      } finally {
        if ($this->questionId < 10) {
          $this->questionId = $this->questionId + 1;
          $this->redirect("this");
        }
        else {
          $this->getPresenter()->redirect("results", ["userToken" => $this->userToken]);
        }
      }
    }
  }
}

/**
 * Interface ITestControlFactory
 *
 * @package App\ParticipationModule\Components
 */
interface ITestControlFactory {
  /**
   * Vytvoří komponentu
   *
   * @param Nette\Database\Table\ActiveRow $actualPresenter
   * @return TestControl
   */
  function create(Nette\Database\Table\ActiveRow $actualPresenter): TestControl;
}