diff --git a/demos/_unit-test/callback.php b/demos/_unit-test/callback.php new file mode 100644 index 0000000000..a1103d7e26 --- /dev/null +++ b/demos/_unit-test/callback.php @@ -0,0 +1,42 @@ +db))->setLimit(5); + +$vp = $app->add(new \atk4\ui\VirtualPage()); +$vp->cb->triggerOnReload = false; + +$form = Form::addTo($vp); +$form->setModel($m->tryLoadAny(), ['name']); +$form->getControl('name')->caption = 'TestName'; + +$table = $app->add(new \atk4\ui\Table()); +$table->setModel($m); + +$button = Button::addTo($app, ['First', ['ui' => 'atk-test']]); +$button->on('click', new \atk4\ui\JsModal('Edit First Record', $vp)); + +$form->onSubmit(function ($form) use ($table) { + $form->model->save(); + + return [ + $table->jsReload(), + new JsToast('Save'), + (new Jquery('.ui.modal.visible.active.front'))->modal('hide'), + ]; +}); diff --git a/demos/_unit-test/callback_2.php b/demos/_unit-test/callback_2.php new file mode 100644 index 0000000000..d72b533947 --- /dev/null +++ b/demos/_unit-test/callback_2.php @@ -0,0 +1,45 @@ +db))->setLimit(5); +$m->getUserAction('edit')->ui['button'] = new Button(['Edit', ['ui' => 'atk-test button']]); + +$loader = Loader::addTo($app); +$loader->loadEvent = false; + +$loader->set(function ($p) use ($m) { + $loader_1 = Loader::addTo($p); + $loader_1->loadEvent = false; + + Header::addTo($p, ['Loader-1', 'size' => 4]); + + $loader_1->set(function ($p) use ($m) { + Header::addTo($p, ['Loader-2', 'size' => 4]); + $loader_3 = Loader::addTo($p); + + $loader_3->set(function ($p) use ($m) { + Header::addTo($p, ['Loader-3', 'size' => 4]); + + $c = Crud::addTo($p, ['ipp' => 4]); + $c->setModel($m, ['name']); + }); + }); + \atk4\ui\Button::addTo($p, ['Load2'])->js('click', $loader_1->jsLoad()); +}); + +\atk4\ui\Button::addTo($app, ['Load1'])->js('click', $loader->jsLoad()); diff --git a/demos/_unit-test/console.php b/demos/_unit-test/console.php index f1486152df..821da16504 100644 --- a/demos/_unit-test/console.php +++ b/demos/_unit-test/console.php @@ -10,7 +10,7 @@ require_once __DIR__ . '/../init-app.php'; $sse = JsSse::addTo($app); -$sse->urlTrigger = 'console_test'; +$sse->setUrlTrigger('console_test'); $console = \atk4\ui\Console::addTo($app, ['sse' => $sse]); diff --git a/demos/_unit-test/console_exec.php b/demos/_unit-test/console_exec.php index 163688ecf0..958c2ce806 100644 --- a/demos/_unit-test/console_exec.php +++ b/demos/_unit-test/console_exec.php @@ -10,7 +10,7 @@ require_once __DIR__ . '/../init-app.php'; $sse = JsSse::addTo($app); -$sse->urlTrigger = 'console_test'; +$sse->setUrlTrigger('console_test'); $console = \atk4\ui\Console::addTo($app, ['sse' => $sse]); $console->exec('/bin/pwd'); diff --git a/demos/_unit-test/console_run.php b/demos/_unit-test/console_run.php index 8623b029e3..685df938fc 100644 --- a/demos/_unit-test/console_run.php +++ b/demos/_unit-test/console_run.php @@ -30,7 +30,7 @@ public function test() }); $sse = JsSse::addTo($app); -$sse->urlTrigger = 'console_test'; +$sse->setUrlTrigger('console_test'); $console = \atk4\ui\Console::addTo($app, ['sse' => $sse]); $console->runMethod($testRunClass::addTo($app), 'test'); diff --git a/demos/_unit-test/exception.php b/demos/_unit-test/exception.php index a6e7062712..b1ea7f2482 100644 --- a/demos/_unit-test/exception.php +++ b/demos/_unit-test/exception.php @@ -12,7 +12,7 @@ // JUST TO TEST Exceptions and Error throws $cb = CallbackLater::addTo($app); -$cb->urlTrigger = 'm_cb'; +$cb->setUrlTrigger('m_cb'); $modal = \atk4\ui\Modal::addTo($app, ['cb' => $cb]); $modal->name = 'm_test'; @@ -24,9 +24,7 @@ $button = \atk4\ui\Button::addTo($app, ['Test modal exception']); $button->on('click', $modal->show()); -$cb1 = CallbackLater::addTo($app); - -$cb1->urlTrigger = 'm2_cb'; +$cb1 = CallbackLater::addTo($app, ['urlTrigger' => 'm2_cb']); $modal2 = \atk4\ui\Modal::addTo($app, ['cb' => $cb1]); $modal2->set(function ($m) use ($modal2) { diff --git a/demos/_unit-test/post.php b/demos/_unit-test/post.php index b9c008dc5c..1703c29488 100644 --- a/demos/_unit-test/post.php +++ b/demos/_unit-test/post.php @@ -11,7 +11,7 @@ require_once __DIR__ . '/../init-app.php'; $form = Form::addTo($app); -$form->name = 'test_form'; +$form->cb->setUrlTrigger('test_submit'); $form->addControl('f1')->set('v1'); diff --git a/demos/_unit-test/reload.php b/demos/_unit-test/reload.php index a950cbfc67..83ffaf79f9 100644 --- a/demos/_unit-test/reload.php +++ b/demos/_unit-test/reload.php @@ -20,7 +20,7 @@ $b->on('click', new JsReload($v)); $cb = Callback::addTo($app); -$cb->urlTrigger = 'c_reload'; +$cb->setUrlTrigger('c_reload'); \atk4\ui\Loader::addTo($app, ['cb' => $cb])->set(function ($page) { $v = View::addTo($page, ['ui' => 'segment'])->set('loaded'); diff --git a/demos/_unit-test/sse.php b/demos/_unit-test/sse.php index 978b7b715e..9688d7edba 100644 --- a/demos/_unit-test/sse.php +++ b/demos/_unit-test/sse.php @@ -14,7 +14,7 @@ $sse = \atk4\ui\JsSse::addTo($app); // url trigger must match php_unit test in sse provider. -$sse->urlTrigger = 'see_test'; +$sse->setUrlTrigger('see_test'); $v->js(true, $sse->set(function () use ($sse) { $sse->send(new JsExpression('console.log("test")')); diff --git a/demos/form-control/checkbox.php b/demos/form-control/checkbox.php index 96f5c80bec..705d503e09 100644 --- a/demos/form-control/checkbox.php +++ b/demos/form-control/checkbox.php @@ -34,6 +34,10 @@ $form->addControl('test_checked', [Form\Control\Checkbox::class])->set(true); $form->addControl('also_checked', 'Hello World', 'boolean')->set(true); +$form->onSubmit(function ($f) use ($app) { + return new \atk4\ui\JsToast($app->encodeJson($f->model->get())); +}); + View::addTo($app, ['ui' => 'divider']); $c = new Form\Control\Checkbox('Selected checkbox by default'); $c->set(true); diff --git a/demos/form/form5.php b/demos/form/form5.php index b267b1db53..b4b77cfedd 100644 --- a/demos/form/form5.php +++ b/demos/form/form5.php @@ -5,6 +5,7 @@ namespace atk4\ui\demo; use atk4\ui\Form; +use atk4\ui\JsToast; /** @var \atk4\ui\App $app */ require_once __DIR__ . '/../init-app.php'; @@ -14,6 +15,10 @@ 'ui' => 'ignored warning message', ]); +$formSubmit = function ($f) use ($app) { + return new JsToast($app->encodeJson($f->model->get())); +}; + $cc = \atk4\ui\Columns::addTo($app); $form = Form::addTo($cc->addColumn()); @@ -35,6 +40,8 @@ // Objects still accept seed $form->addControl('six', new Form\Control\Checkbox(['caption' => 'Caption3'])); +$form->onSubmit($formSubmit); + $model = new \atk4\data\Model(new \atk4\data\Persistence\Array_()); // model field uses regular line form control by default @@ -57,6 +64,7 @@ $form = Form::addTo($cc->addColumn()); $form->setModel($model); +$form->onSubmit($formSubmit); // Next form won't initalize default fields, but we'll add them individually $form = Form::addTo($cc->addColumn()); @@ -79,3 +87,4 @@ // can add field that does not exist in a model $form->addControl('nine', new Form\Control\Checkbox(['caption' => 'Caption3'])); +$form->onSubmit($formSubmit); diff --git a/demos/init-db.php b/demos/init-db.php index 4711e56db0..df4cb2364a 100644 --- a/demos/init-db.php +++ b/demos/init-db.php @@ -66,18 +66,18 @@ public function validate($intent = null): array { $errors = parent::validate($intent); - if (mb_strlen($this['iso']) !== 2) { + if (mb_strlen($this->get('iso')) !== 2) { $errors['iso'] = 'Must be exactly 2 characters'; } - if (mb_strlen($this['iso3']) !== 3) { + if (mb_strlen($this->get('iso3')) !== 3) { $errors['iso3'] = 'Must be exactly 3 characters'; } // look if name is unique $c = clone $this; $c->unload(); - $c->tryLoadBy('name', $this['name']); + $c->tryLoadBy('name', $this->get('name')); if ($c->loaded() && $c->id !== $this->id) { $errors['name'] = 'Country name must be unique'; } diff --git a/demos/interactive/scroll-grid-container.php b/demos/interactive/scroll-grid-container.php index 9de49578ca..bcfb038947 100644 --- a/demos/interactive/scroll-grid-container.php +++ b/demos/interactive/scroll-grid-container.php @@ -17,7 +17,7 @@ $c1 = $c->addColumn(); $g1 = \atk4\ui\Crud::addTo($c1); -$m1 = $g1->setModel(new Country($app->db)); //, ['name', 'iso']); +$m1 = $g1->setModel(new CountryLock($app->db)); //, ['name', 'iso']); $g1->addQuickSearch(['name', 'iso']); // demo for additional action buttons in Crud + JsPaginator @@ -32,9 +32,9 @@ $c2 = $c->addColumn(); $g2 = \atk4\ui\Grid::addTo($c2, ['menu' => false]); -$m2 = $g2->setModel(new Country($app->db)); +$m2 = $g2->setModel(new CountryLock($app->db)); $g2->addJsPaginatorInContainer(20, 200); $g3 = \atk4\ui\Grid::addTo($c2, ['menu' => false]); -$m3 = $g3->setModel(new Country($app->db)); +$m3 = $g3->setModel(new CountryLock($app->db)); $g3->addJsPaginatorInContainer(10, 150); diff --git a/demos/interactive/scroll-grid.php b/demos/interactive/scroll-grid.php index d0f002afe9..d06c2c6f80 100644 --- a/demos/interactive/scroll-grid.php +++ b/demos/interactive/scroll-grid.php @@ -16,6 +16,6 @@ \atk4\ui\Header::addTo($app, ['Dynamic scroll in Grid']); $grid = \atk4\ui\Grid::addTo($app, ['menu' => false]); -$model = $grid->setModel(new Country($app->db)); +$model = $grid->setModel(new CountryLock($app->db)); $grid->addJsPaginator(30); diff --git a/demos/interactive/virtual.php b/demos/interactive/virtual.php index b56a3307f3..6260559598 100644 --- a/demos/interactive/virtual.php +++ b/demos/interactive/virtual.php @@ -10,8 +10,7 @@ // Demonstrate the use of Virtual Page. // define virtual page. -$virtualPage = \atk4\ui\VirtualPage::addTo($app->layout); -$virtualPage->cb->urlTrigger = 'in'; +$virtualPage = \atk4\ui\VirtualPage::addTo($app->layout, ['urlTrigger' => 'in']); // Add content to virtual page. if (isset($_GET['p_id'])) { diff --git a/demos/interactive/wizard.php b/demos/interactive/wizard.php index 724c094a7a..2d4df83275 100644 --- a/demos/interactive/wizard.php +++ b/demos/interactive/wizard.php @@ -30,7 +30,7 @@ $wizard->addStep(['Set DSN', 'icon' => 'configure', 'description' => 'Database Connection String'], function (Wizard $wizard) { $form = \atk4\ui\Form::addTo($wizard); // IMPORTANT - needed for php_unit Wizard test. - $form->name = 'w_form'; + $form->cb->setUrlTrigger('w_form_submit'); $form->addControl('dsn', 'Connect DSN', ['required' => true])->placeholder = 'mysql://user:pass@db-host.example.com/mydb'; $form->onSubmit(function (\atk4\ui\Form $form) use ($wizard) { diff --git a/demos/obsolete/notify.php b/demos/obsolete/notify.php index 849f571dd2..c7749752ba 100644 --- a/demos/obsolete/notify.php +++ b/demos/obsolete/notify.php @@ -16,7 +16,7 @@ $modal->set(function ($p) use ($modal) { $form = \atk4\ui\Form::addTo($p); - $form->addField('name', null, ['caption' => 'Add your name']); + $form->addControl('name', null, ['caption' => 'Add your name']); $form->onSubmit(function (\atk4\ui\Form $form) use ($modal) { if (empty($form->model->get('name'))) { diff --git a/demos/obsolete/notify2.php b/demos/obsolete/notify2.php index b3f05ca2e0..69c6f648a9 100644 --- a/demos/obsolete/notify2.php +++ b/demos/obsolete/notify2.php @@ -30,7 +30,7 @@ public function init(): void $form = \atk4\ui\Form::addTo($app, ['segment']); // Unit test only. -$form->name = 'notify'; +$form->cb->setUrlTrigger('test_notify'); \atk4\ui\Label::addTo($form, ['Some of notification options that can be set.', 'top attached'], ['AboveControls']); $form->buttonSave->set('Show'); diff --git a/demos/tutorial/actions.php b/demos/tutorial/actions.php index 1858cd6a4f..9fa2aed47a 100644 --- a/demos/tutorial/actions.php +++ b/demos/tutorial/actions.php @@ -203,16 +203,15 @@ functionality and more. Next example shows how you can disable user action (add) $country = new \atk4\ui\demo\CountryLock($app->db); $country->getUserAction('add')->enabled = false; $country->getUserAction('delete')->enabled = function() { return rand(1,2)>1; }; - $country->getUserAction('mail', [ - 'appliesTo' => \atk4\data\Model\UserAction:APPLIES_TO_SINGLE_RECORD, - 'preview' => function($model) { return 'here is email preview for '.$model->get('name'); }, + $country->addUserAction('mail', [ + 'appliesTo' => \atk4\data\Model\UserAction::APPLIES_TO_SINGLE_RECORD, + 'preview' => function($model) { return 'here is email preview for '.$model->get('name'); }, 'callback' => function($model) { return 'email sent to '.$model->get('name'); }, 'description' => 'Email testing', 'ui' => ['icon'=>'mail', 'button'=>[null, 'icon'=>'green mail']], ]); - \atk4\ui\Crud::addTo($app, ['ipp' => 5]) - ->setModel($country, ['name','iso']); + \atk4\ui\Crud::addTo($app, ['ipp' => 5])->setModel($country, ['name','iso']); CODE ); }); diff --git a/features/bootstrap/atk4/ui/behat/FeatureContextBasic.php b/features/bootstrap/atk4/ui/behat/FeatureContextBasic.php index d705818d2e..0cdb3c7816 100644 --- a/features/bootstrap/atk4/ui/behat/FeatureContextBasic.php +++ b/features/bootstrap/atk4/ui/behat/FeatureContextBasic.php @@ -136,6 +136,33 @@ public function iClickTabWithTitle($arg1) $this->getSession()->executeScript($script); } + /** + * @Then I click first card on page + */ + public function iClickFirstCardOnPage() + { + $script = '$(".atk-card")[0].click()'; + $this->getSession()->executeScript($script); + } + + /** + * @Then I click first element using class :arg1 + */ + public function iClickFirstElementUsingClass($arg1) + { + $script = '$("' . $arg1 . '")[0].click()'; + $this->getSession()->executeScript($script); + } + + /** + * @Then I click paginator page :arg1 + */ + public function iClickPaginatorPage($arg1) + { + $script = '$("a.item[data-page=' . $arg1 . ']").click()'; + $this->getSession()->executeScript($script); + } + /** * @Then I see button :arg1 */ diff --git a/features/callback.feature b/features/callback.feature new file mode 100644 index 0000000000..c4de100f44 --- /dev/null +++ b/features/callback.feature @@ -0,0 +1,34 @@ +Feature: Callback + Testing callbacks + + Scenario: + Given I am on "_unit-test/callback.php" + Then I press button "First" + And wait for callback + Then I sleep 500 ms + Then I should see "TestName" + And I press button "Save" + And wait for callback + Then Toast display should contains text "Save" + Then I sleep 500 ms + Then I should not see "TestName" + + Scenario: + Given I am on "_unit-test/callback_2.php" + Then I press button "Load1" + And wait for callback + Then I should see "Loader-1" + Then I press button "Load2" + Then wait for callback + # Loader 2 automatically trigger 3, so wait for it. + Then wait for callback + Then I should see "Loader-2" + Then I should see "Loader-3" + Then I click paginator page "2" + And wait for callback + Then I click first element using class ".ui.atk-test.button" + And wait for callback + Then Modal is open with text "Edit Country" + Then I press button "Save" + And wait for callback + Then Toast display should contains text "Form Submit" diff --git a/features/rightpanel.feature b/features/rightpanel.feature index 21d15d735c..9b9f3e4117 100644 --- a/features/rightpanel.feature +++ b/features/rightpanel.feature @@ -2,7 +2,7 @@ Feature: RightPanel Testing RightPanel - Scenario: + Scenario: PanelReload Given I am on "layout/layout-panel.php" And I press button "Button 1" And wait for callback @@ -12,3 +12,13 @@ Feature: RightPanel Then I press button "Complete" And wait for callback Then I should see "Complete using button #1" + + Scenario: PanelModelAction + Given I am on "layout/layout-panel.php" + Then I click first card on page + And wait for callback + And I press button "User Confirmation" + And wait for callback + And I press Modal button "Ok" + And wait for callback + Then Toast display should contains text "Confirm country" diff --git a/features/useraction.feature b/features/useraction.feature index 676fada05d..685b74dd4d 100644 --- a/features/useraction.feature +++ b/features/useraction.feature @@ -23,7 +23,7 @@ Feature: UserAction Given I am on "collection/jsactions2.php" And I press button "User Confirmation" And wait for callback - Then I press button "Ok" + And I press Modal button "Ok" And wait for callback Then Toast display should contains text "Confirm country" diff --git a/src/App.php b/src/App.php index d4ff0a3161..066ab79d51 100644 --- a/src/App.php +++ b/src/App.php @@ -45,7 +45,7 @@ class App ]; /** @var string Version of Agile UI */ - public $version = '2.1.0'; + public $version = '2.2.0'; /** @var string Name of application */ public $title = 'Agile UI - Untitled Application'; diff --git a/src/Callback.php b/src/Callback.php index 6e07eb1aae..9b1b6b17ac 100644 --- a/src/Callback.php +++ b/src/Callback.php @@ -14,6 +14,9 @@ * Add this object to your render tree and it will expose a unique URL which, when * executed directly will perform a PHP callback that you set(). * + * Callback function run when triggered, i.e. when it's urlTrigger param value is present in the $_GET request. + * The current callback will be set within the $_GET['__atk_callback'] and will be set to urlTrigger as well. + * * $button = Button::addTo($layout); * $button->set('Click to do something')->link( * Callback::addTo($button) @@ -35,33 +38,14 @@ class Callback } use StaticAddToTrait; - /** - * Will look for trigger in the POST data. Will not care about URL, but - * $_POST[$this->postTrigger] must be set. - * - * @var string|bool - */ - public $postTrigger = false; - - /** - * Contains either false if callback wasn't triggered or the value passed - * as an argument to a call-back. - * - * e.g. following URL of getUrl('test') will result in $triggered = 'test'; - * - * @var string|false - */ - public $triggered = false; + /** @var string Specify a custom GET trigger. */ + protected $urlTrigger; - /** - * Specify a custom GET trigger here. - * - * @var string|null - */ - public $urlTrigger; + /** @var bool Create app sticky trigger. */ + public $isSticky = true; - /** @var bool stick callback url argument to view or application. */ - public $appSticky = false; + /** @var bool Allow this callback to trigger during a reload. */ + public $triggerOnReload = true; /** * Initialize object and set default properties. @@ -81,18 +65,23 @@ public function init(): void $this->_init(); if (!$this->app) { - throw new Exception('Call-back must be part of a RenderTree'); + throw new Exception('Callback must be part of a render tree'); } - if (!$this->urlTrigger) { - $this->urlTrigger = $this->name; - } + $this->setUrlTrigger($this->urlTrigger); + } - if ($this->postTrigger === true) { - $this->postTrigger = $this->name; + public function setUrlTrigger(string $trigger = null) + { + $this->urlTrigger = $trigger ?: $this->name; + if ($this->isSticky) { + $this->app->stickyGet($this->urlTrigger); } + } - $this->appSticky ? $this->app->stickyGet($this->urlTrigger) : $this->owner->stickyGet($this->urlTrigger); + public function getUrlTrigger(): string + { + return $this->urlTrigger; } /** @@ -105,88 +94,84 @@ public function init(): void */ public function set($callback, $args = []) { - if ($this->postTrigger) { - if (isset($_POST[$this->postTrigger])) { - $this->app->catch_runaway_callbacks = false; - $this->triggered = $_POST[$this->postTrigger]; - - $t = $this->app->run_called; - $this->app->run_called = true; - $ret = $callback(...$args); - $this->app->run_called = $t; - - return $ret; - } - } else { - if (isset($_GET[$this->urlTrigger])) { - $this->app->catch_runaway_callbacks = false; - $this->triggered = $_GET[$this->urlTrigger]; - - $t = $this->app->run_called; - $this->app->run_called = true; - $this->owner->stickyGet($this->urlTrigger); - $ret = $callback(...$args); - //$this->app->stickyForget($this->name); - $this->app->run_called = $t; - - return $ret; - } + if ($this->isTriggered() && $this->canTrigger()) { + $this->app->catch_runaway_callbacks = false; + $t = $this->app->run_called; + $this->app->run_called = true; + $ret = $callback(...$args); + $this->app->run_called = $t; + + return $ret; } } /** * Terminate this callback - * by rendering the owner view. + * by rendering the owner view by default. */ - public function terminate() + public function terminateJson(View $view = null): void { if ($this->canTerminate()) { - $this->app->terminateJson($this->owner); + $this->app->terminateJson($view ?? $this->owner); } } /** - * Prevent callback from terminating during a reload. + * Return true if urlTrigger is part of the request. */ - protected function canTerminate(): bool + public function isTriggered() { - $reload = $_GET['__atk_reload'] ?? null; + return isset($_GET[$this->urlTrigger]); + } - return !$reload || $this->owner->name === $reload; + /** + * Return callback triggered value. + */ + public function getTriggeredValue(): string + { + return $_GET[$this->urlTrigger] ?? ''; } /** - * Is callback triggered? + * Only current callback can terminate. */ - public function triggered() + public function canTerminate(): bool { - return $_GET[$this->urlTrigger] ?? false; + return isset($_GET['__atk_callback']) && $_GET['__atk_callback'] === $this->urlTrigger; + } + + /** + * Allow callback to be triggered or not. + */ + public function canTrigger(): bool + { + return $this->triggerOnReload || empty($_GET['__atk_reload']); } /** * Return URL that will trigger action on this call-back. If you intend to request * the URL direcly in your browser (as iframe, new tab, or document location), you * should use getUrl instead. - * - * @param string $mode - * - * @return string */ - public function getJsUrl($mode = 'ajax') + public function getJsUrl(string $value = 'ajax'): string { - return $this->owner->jsUrl([$this->urlTrigger => $mode, '__atk_callback' => 1], (bool) $this->postTrigger); + return $this->owner->jsUrl($this->getUrlArguments($value)); } /** * Return URL that will trigger action on this call-back. If you intend to request * the URL loading from inside JavaScript, it's always advised to use getJsUrl instead. - * - * @param string $mode - * - * @return string */ - public function getUrl($mode = 'callback') + public function getUrl(string $value = 'callback'): string + { + return $this->owner->url($this->getUrlArguments($value)); + } + + /** + * Return proper url argument for this callback. + */ + private function getUrlArguments(string $value): array { - return $this->owner->url([$this->urlTrigger => $mode, '__atk_callback' => 1], (bool) $this->postTrigger); + return ['__atk_callback' => $this->urlTrigger, $this->urlTrigger => $value]; } } diff --git a/src/Component/InlineEdit.php b/src/Component/InlineEdit.php index a8bc6f5f2c..16b43f1940 100644 --- a/src/Component/InlineEdit.php +++ b/src/Component/InlineEdit.php @@ -108,23 +108,21 @@ public function setModel(\atk4\data\Model $model) parent::setModel($model); $this->field = $this->field ? $this->field : $this->model->title_field; if ($this->autoSave && $this->model->loaded()) { - if ($this->cb->triggered()) { - $value = $_POST['value'] ? $_POST['value'] : null; - $this->cb->set(function () use ($value) { - try { - $this->model->set($this->field, $this->app->ui_persistence->typecastLoadField($this->model->getField($this->field), $value)); - $this->model->save(); - - return $this->jsSuccess('Update successfully'); - } catch (ValidationException $e) { - $this->app->terminateJson([ - 'success' => true, - 'hasValidationError' => true, - 'atkjs' => $this->jsError(($this->formatErrorMsg)($e, $value))->jsRender(), - ]); - } - }); - } + $value = $_POST['value'] ?? null; + $this->cb->set(function () use ($value) { + try { + $this->model->set($this->field, $this->app->ui_persistence->typecastLoadField($this->model->getField($this->field), $value)); + $this->model->save(); + + return $this->jsSuccess('Update successfully'); + } catch (ValidationException $e) { + $this->app->terminateJson([ + 'success' => true, + 'hasValidationError' => true, + 'atkjs' => $this->jsError(($this->formatErrorMsg)($e, $value))->jsRender(), + ]); + } + }); } return $this->model; @@ -211,15 +209,5 @@ protected function renderView(): void 'url' => $this->cb->getJsUrl(), 'saveOnBlur' => $this->saveOnBlur, ]); - -// $this->js(true, (new JsVueService())->createAtkVue( -// '#'.$this->name, -// 'atk-inline-edit', -// [ -// 'initValue' => $initValue, -// 'url' => $this->cb->getJsUrl(), -// 'saveOnBlur' => $this->saveOnBlur, -// ] -// )); } } diff --git a/src/Dropdown.php b/src/Dropdown.php index 856fc34c3f..b7e3047a14 100644 --- a/src/Dropdown.php +++ b/src/Dropdown.php @@ -29,7 +29,7 @@ public function init(): void parent::init(); if (!$this->cb) { - $this->cb = JsCallback::addTo($this, ['postTrigger' => 'item']); + $this->cb = JsCallback::addTo($this, ['urlTrigger' => 'item']); } } diff --git a/src/Form.php b/src/Form.php index a654f4fe90..d8ed692f56 100644 --- a/src/Form.php +++ b/src/Form.php @@ -28,6 +28,9 @@ class Form extends View public $ui = 'form'; public $defaultTemplate = 'form.html'; + /** @var Callback Callback handling form submission. */ + public $cb; + /** * Set this to false in order to * prevent from leaving @@ -194,6 +197,8 @@ public function init(): void // set css loader for this form $this->setApiConfig(['stateContext' => '#' . $this->name]); + + $this->cb = $this->add(new JsCallback(), ['desired_name' => 'submit']); } /** @@ -306,6 +311,32 @@ public function onSubmit(\Closure $callback) { $this->onHook(self::HOOK_SUBMIT, $callback); + $this->cb->set(function () { + try { + $this->loadPost(); + $response = $this->hook(self::HOOK_SUBMIT); + + if (!$response) { + if (!$this->model instanceof \atk4\ui\Misc\ProxyModel) { + $this->model->save(); + + return $this->success('Form data has been saved'); + } + + return new JsExpression('console.log([])', ['Form submission is not handled']); + } + + return $response; + } catch (\atk4\data\ValidationException $val) { + $response = []; + foreach ($val->errors as $field => $error) { + $response[] = $this->error($field, $error); + } + + return $response; + } + }); + return $this; } @@ -681,43 +712,10 @@ public function setFormConfig($config) */ public function ajaxSubmit() { - $this->_add($cb = new JsCallback(), ['desired_name' => 'submit', 'postTrigger' => true]); - - View::addTo($this, ['element' => 'input']) - ->setAttr('name', $cb->postTrigger) - ->setAttr('value', 'submit') - ->setStyle(['display' => 'none']); - - $cb->set(function () { - try { - $this->loadPost(); - $response = $this->hook(self::HOOK_SUBMIT); - - if (!$response) { - if (!$this->model instanceof \atk4\ui\Misc\ProxyModel) { - $this->model->save(); - - return $this->success('Form data has been saved'); - } - - return new JsExpression('console.log([])', ['Form submission is not handled']); - } - - return $response; - } catch (\atk4\data\ValidationException $val) { - $response = []; - foreach ($val->errors as $field => $error) { - $response[] = $this->error($field, $error); - } - - return $response; - } - }); - $this->js(true)->form(array_merge(['inline' => true, 'on' => 'blur'], $this->formConfig)); $this->js(true, null, $this->formElement) - ->api(array_merge(['url' => $cb->getJSURL(), 'method' => 'POST', 'serializeForm' => true], $this->apiConfig)); + ->api(array_merge(['url' => $this->cb->getJsUrl(), 'method' => 'POST', 'serializeForm' => true], $this->apiConfig)); $this->on('change', 'input, textarea, select', $this->js()->form('remove prompt', new JsExpression('$(this).attr("name")'))); diff --git a/src/Form/Control/Multiline.php b/src/Form/Control/Multiline.php index e4fa46c954..9b722f54ab 100644 --- a/src/Form/Control/Multiline.php +++ b/src/Form/Control/Multiline.php @@ -675,17 +675,13 @@ protected function renderView(): void throw new Exception('Multiline field needs to have it\'s model setup.'); } - if ($this->cb->triggered()) { - $this->cb->set(function () { - try { - return $this->renderCallback(); - } catch (\atk4\Core\Exception $e) { - $this->app->terminateJson(['success' => false, 'error' => $e->getMessage()]); - } catch (\Error $e) { - $this->app->terminateJson(['success' => false, 'error' => $e->getMessage()]); - } - }); - } + $this->cb->set(function () { + try { + return $this->renderCallback(); + } catch (\atk4\Core\Exception | \Error $e) { + $this->app->terminateJson(['success' => false, 'error' => $e->getMessage()]); + } + }); $this->multiLine->template->trySetHtml('Input', $this->getInput()); parent::renderView(); diff --git a/src/Form/Control/Upload.php b/src/Form/Control/Upload.php index b1ebf8897c..270d0be841 100644 --- a/src/Form/Control/Upload.php +++ b/src/Form/Control/Upload.php @@ -80,6 +80,9 @@ class Upload extends Input public $jsActions = []; + public const UPLOAD_ACTION = 'upload'; + public const DELETE_ACTION = 'delete'; + /** @var bool check if callback is trigger by one of the action. */ private $_isCbRunning = false; @@ -166,10 +169,8 @@ public function addJsAction($action) public function onUpload(\Closure $fx) { $this->hasUploadCb = true; - if (($_POST['action'] ?? null) === 'upload') { + if (($_POST['action'] ?? null) === self::UPLOAD_ACTION) { $this->cb->set(function () use ($fx) { - $this->_isCbRunning = true; - $postFiles = []; for ($i = 0;; ++$i) { $k = 'file' . ($i > 0 ? '-' . $i : ''); @@ -211,10 +212,8 @@ public function onUpload(\Closure $fx) public function onDelete(\Closure $fx) { $this->hasDeleteCb = true; - if (($_POST['action'] ?? null) === 'delete') { + if (($_POST['action'] ?? null) === self::DELETE_ACTION) { $this->cb->set(function () use ($fx) { - $this->_isCbRunning = true; - $fileName = $_POST['f_name'] ?? null; $this->addJsAction($fx($fileName)); @@ -231,8 +230,13 @@ protected function renderView(): void } parent::renderView(); - if (!$this->_isCbRunning && (!$this->hasUploadCb || !$this->hasDeleteCb)) { - throw new Exception('onUpload and onDelete callback must be called to use file upload. Missing one or both of them.'); + if ($this->cb->canTerminate()) { + $action = $_POST['action'] ?? null; + if (!$this->hasUploadCb && ($action === self::UPLOAD_ACTION)) { + throw new Exception('Missing onUpload callback.'); + } elseif (!$this->hasDeleteCb && ($action === self::DELETE_ACTION)) { + throw new Exception('Missing onDelete callback.'); + } } if (!empty($this->accept)) { diff --git a/src/JsCallback.php b/src/JsCallback.php index 9bf78b5b15..e66fa5e3de 100644 --- a/src/JsCallback.php +++ b/src/JsCallback.php @@ -34,6 +34,16 @@ class JsCallback extends Callback implements JsExpressionable */ public $storeName; + /** + * Usually JsCallback should not allow to trigger during a reload. + * Consider reloading a form, if triggering is allowed during the reload process + * then $form->model could be saved during that reload which can lead to unexpected result + * if model id is not properly handled. + * + * @var bool + */ + public $triggerOnReload = false; + /** * When multiple JsExpressionable's are collected inside an array and may * have some degree of nesting, convert it into a one-dimensional array, @@ -167,7 +177,7 @@ public function getAjaxec($response, $chain = null): string return $ajaxec; } - public function getUrl($mode = 'callback') + public function getUrl(string $mode = 'callback'): string { throw new Exception('Do not use getUrl on JsCallback, use getJsUrl()'); } diff --git a/src/Loader.php b/src/Loader.php index c2f86d49b9..2a0e6155f3 100644 --- a/src/Loader.php +++ b/src/Loader.php @@ -34,9 +34,6 @@ class Loader extends View /** @var string defautl css class */ public $ui = 'ui segment'; - /** @var bool Make callback url argument stick to application or view. */ - public $appStickyCb = false; - /** @var Callback for triggering */ protected $cb; @@ -49,7 +46,7 @@ public function init(): void } if (!$this->cb) { - $this->cb = Callback::addTo($this, ['appSticky' => $this->appStickyCb]); + $this->cb = Callback::addTo($this); } } @@ -84,7 +81,7 @@ public function set($fx = [], $ignore = null) $this->cb->set(function () use ($fx) { $fx($this); - $this->app->terminateJson($this); + $this->cb->terminateJson(); }); return $this; @@ -96,7 +93,7 @@ public function set($fx = [], $ignore = null) */ protected function renderView(): void { - if (!$this->cb->triggered()) { + if (!$this->cb->isTriggered()) { if ($this->loadEvent) { $this->js($this->loadEvent, $this->jsLoad()); } @@ -116,7 +113,7 @@ protected function renderView(): void public function jsLoad($args = [], $apiConfig = [], $storeName = null) { return $this->js()->atkReloadView([ - 'uri' => $this->cb->getJsUrl(), + 'uri' => $this->cb->getUrl(), 'uri_options' => $args, 'apiConfig' => !empty($apiConfig) ? $apiConfig : null, 'storeName' => $storeName ? $storeName : null, diff --git a/src/Modal.php b/src/Modal.php index 3255eec12c..ec1b9aa82b 100644 --- a/src/Modal.php +++ b/src/Modal.php @@ -41,9 +41,6 @@ class Modal extends View public $cb_view; public $args = []; - /** @var bool Make callback url argument stick to application or view. */ - public $appStickyCb = false; - /** @var string Currently only "json" response type is supported. */ public $type = 'json'; @@ -64,6 +61,8 @@ class Modal extends View /** * Set callback function for this modal. + * $fx is set as an array in order to comply with View::set(). + * TODO Rename this function and break BC? * * @param \Closure $fx * @@ -94,17 +93,12 @@ public function enableCallback() $this->cb_view = View::addTo($this); $this->cb_view->stickyGet('__atk_m', $this->name); if (!$this->cb) { - $this->cb = CallbackLater::addTo($this->cb_view, ['appSticky' => $this->appStickyCb]); + $this->cb = CallbackLater::addTo($this->cb_view); } $this->cb->set(function () { - if ($this->cb->triggered() && $this->fx) { - $this->fx[0]($this->cb_view); - } - $modalName = $_GET['__atk_m'] ?? null; - if ($modalName === $this->name) { - $this->app->terminateJson($this->cb_view); - } + $this->fx[0]($this->cb_view); + $this->cb->terminateJson($this->cb_view); }); } diff --git a/src/Panel/Content.php b/src/Panel/Content.php index 94612e04bb..e9371764f2 100644 --- a/src/Panel/Content.php +++ b/src/Panel/Content.php @@ -19,7 +19,7 @@ public function init(): void { parent::init(); $this->addClass('atk-panel-content'); - $this->setCb(new Callback(['appSticky' => true])); + $this->setCb(new Callback()); } /** @@ -46,10 +46,8 @@ public function setCb(Callback $cb) public function onLoad(\Closure $fx) { $this->cb->set(function () use ($fx) { - if ($this->cb->triggered()) { - $fx($this); - $this->cb->terminate(); - } + $fx($this); + $this->cb->terminateJson(); }); } diff --git a/src/Popup.php b/src/Popup.php index 8368cf005d..298ec37325 100644 --- a/src/Popup.php +++ b/src/Popup.php @@ -153,7 +153,7 @@ public function init(): void /** * Set callback for loading content dynamically. - * Callback will reveive a view attach to this popup + * Callback will receive a view attach to this popup * for adding content to it. * * @param \Closure $fx @@ -176,14 +176,12 @@ public function set($fx = null, $ignore = null) $this->minHeight = '60px'; } - if ($this->cb->triggered()) { - //create content view to pass to callback. - $content = $this->add($this->dynamicContent); - $this->cb->set($fx, [$content]); - //only render our content view. - //PopupService will replace content with this one. - $this->app->terminateJson($content); - } + //create content view to pass to callback. + $content = $this->add($this->dynamicContent); + $this->cb->set($fx, [$content]); + //only render our content view. + //PopupService will replace content with this one. + $this->cb->terminateJson($content); } /** diff --git a/src/Table.php b/src/Table.php index b278f28dd4..d359aa7094 100644 --- a/src/Table.php +++ b/src/Table.php @@ -277,9 +277,6 @@ public function setFilterColumn($cols = null) if ($col) { $pop = $col->addPopup(new Table\Column\FilterPopup(['field' => $this->model->getField($colName), 'reload' => $this->reload, 'colTrigger' => '#' . $col->name . '_ac'])); $pop->isFilterOn() ? $col->setHeaderPopupIcon('table-filter-on') : null; - $pop->form->onSubmit(function (Form $form) use ($pop) { - return new JsReload($this->reload); - }); //apply condition according to popup form. $this->model = $pop->setFilterCondition($this->model); } diff --git a/src/Table/Column/FilterPopup.php b/src/Table/Column/FilterPopup.php index 8b1eb57e16..a90f09b159 100644 --- a/src/Table/Column/FilterPopup.php +++ b/src/Table/Column/FilterPopup.php @@ -71,9 +71,8 @@ public function init(): void $this->form->onSubmit(function (Form $form) { $form->model->save(); - //trigger click action in order to close popup. - //otherwise calling ->popup('hide') is not working as expected. - return (new Jquery($this->triggerBy))->trigger('click'); + + return new jsReload($this->reload); }); \atk4\ui\Button::addTo($this->form, ['Clear', 'clear '])->on('click', function ($f) use ($model) { diff --git a/src/VirtualPage.php b/src/VirtualPage.php index 88c179e688..783a2067d9 100644 --- a/src/VirtualPage.php +++ b/src/VirtualPage.php @@ -27,9 +27,6 @@ class VirtualPage extends View /** @var string UI container class */ public $ui = 'container'; - /** @var bool Make callback url argument stick to application or view. */ - public $appStickyCb = true; - /** * Initialization. */ @@ -37,8 +34,7 @@ public function init(): void { parent::init(); - $this->cb = $this->_add([Callback::class, 'urlTrigger' => $this->urlTrigger ?: $this->name, 'appSticky' => $this->appStickyCb]); - $this->app->stickyGet($this->cb->urlTrigger); + $this->cb = $this->add([Callback::class, 'urlTrigger' => $this->urlTrigger ?: $this->name]); } /** @@ -71,9 +67,9 @@ public function set($fx = [], $junk = null) /** * Is virtual page active? */ - public function triggered() + public function isTriggered(): bool { - return $this->cb->triggered(); + return $this->cb->isTriggered(); } /** @@ -110,14 +106,14 @@ public function getHtml() { $this->cb->set(function () { // if virtual page callback is triggered - if ($type = $this->cb->triggered()) { + if ($mode = $this->cb->getTriggeredValue()) { // process callback if ($this->fx) { ($this->fx)($this); } // special treatment for popup - if ($type === 'popup') { + if ($mode === 'popup') { $this->app->html->template->set('title', $this->app->title); $this->app->html->template->setHtml('Content', parent::getHtml()); $this->app->html->template->appendHtml('HEAD', $this->getJs()); @@ -135,7 +131,7 @@ public function getHtml() } // do not terminate if callback supplied (no cutting) - if ($type !== 'callback') { + if ($mode !== 'callback') { $this->app->terminateHtml($this); } } diff --git a/src/Wizard.php b/src/Wizard.php index 3cb0d32fd5..bab1c78774 100644 --- a/src/Wizard.php +++ b/src/Wizard.php @@ -67,7 +67,7 @@ public function init(): void $this->stepCallback = Callback::addTo($this, ['urlTrigger' => $this->name]); } - $this->currentStep = (int) $this->stepCallback->triggered() ?: 0; + $this->currentStep = (int) ($this->stepCallback->getTriggeredValue() ?: 0); $this->stepTemplate = $this->template->cloneRegion('Step'); $this->template->del('Step'); @@ -75,13 +75,13 @@ public function init(): void // add buttons if ($this->currentStep) { $this->buttonPrev = Button::addTo($this, ['Back', 'basic'], ['Left']); - $this->buttonPrev->link($this->stepCallback->getUrl($this->currentStep - 1)); + $this->buttonPrev->link($this->stepCallback->getUrl((string) ($this->currentStep - 1))); } $this->buttonNext = Button::addTo($this, ['Next', 'primary'], ['Right']); $this->buttonFinish = Button::addTo($this, ['Finish', 'primary'], ['Right']); - $this->buttonNext->link($this->stepCallback->getUrl($this->currentStep + 1)); + $this->buttonNext->link($this->stepCallback->getUrl((string) ($this->currentStep + 1))); } /** @@ -104,8 +104,8 @@ public function addStep($name, $callback) // add tabs menu item $this->steps[] = $this->add($step, 'Step'); - if (!$this->stepCallback->triggered()) { - $_GET[$this->stepCallback->urlTrigger] = '0'; + if (!$this->stepCallback->isTriggered()) { + $_GET[$this->stepCallback->getUrlTrigger()] = '0'; } if ($step->sequence === $this->currentStep) { @@ -132,7 +132,7 @@ public function addStep($name, $callback) public function addFinish(\Closure $callback) { if (count($this->steps) === $this->currentStep + 1) { - $this->buttonFinish->link($this->stepCallback->getUrl(count($this->steps))); + $this->buttonFinish->link($this->stepCallback->getUrl((string) (count($this->steps)))); } elseif ($this->currentStep === count($this->steps)) { $this->buttonPrev->destroy(); $this->buttonNext->addClass('disabled')->set('Completed'); @@ -167,7 +167,7 @@ public function add($seed, $region = null) */ public function urlNext() { - return $this->stepCallback->getUrl($this->currentStep + 1); + return $this->stepCallback->getUrl((string) ($this->currentStep + 1)); } /** diff --git a/tests/CallbackTest.php b/tests/CallbackTest.php index e45e8ea0b4..f85b256b27 100644 --- a/tests/CallbackTest.php +++ b/tests/CallbackTest.php @@ -77,24 +77,6 @@ public function testCallbackNotFiring() $this->assertNull($var); } - public function testCallbackPost() - { - $var = null; - - $app = $this->app; - - $cb = \atk4\ui\Callback::addTo($app, ['postTrigger' => 'go']); - - // simulate triggering - $_POST['go'] = true; - - $cb->set(function ($x) use (&$var) { - $var = $x; - }, [34]); - - $this->assertSame(34, $var); - } - public function testCallbackLater() { $var = null; @@ -174,11 +156,12 @@ public function testVirtualPage() $app = $this->app; $vp = \atk4\ui\VirtualPage::addTo($app); + // simulate triggering + $vp->set(function ($p) use (&$var) { $var = 25; }); - // simulate triggering $_GET[$vp->name] = '1'; $this->expectOutputRegex('/^..DOCTYPE/'); diff --git a/tests/DemosTest.php b/tests/DemosTest.php index 6bb831284d..e6970fd8d9 100644 --- a/tests/DemosTest.php +++ b/tests/DemosTest.php @@ -311,10 +311,9 @@ public function testWizard(): void } $response = $this->getResponseFromRequest( - 'interactive/wizard.php?demo_wizard=1&w_form_submit=ajax&__atk_callback=1', + 'interactive/wizard.php?demo_wizard=1&w_form_submit=ajax&__atk_callback=w_form_submit', ['form_params' => [ 'dsn' => 'mysql://root:root@db-host.example.com/atk4', - 'w_form_submit' => 'submit', ]] ); @@ -335,10 +334,10 @@ public function jsonResponseProvider(): array // simple reload $files[] = ['_unit-test/reload.php?__atk_reload=reload']; // loader callback reload - $files[] = ['_unit-test/reload.php?c_reload=ajax&__atk_callback=1']; + $files[] = ['_unit-test/reload.php?c_reload=ajax&__atk_callback=c_reload']; // test catch exceptions - $files[] = ['_unit-test/exception.php?m_cb=ajax&__atk_callback=1&__atk_json=1']; - $files[] = ['_unit-test/exception.php?m2_cb=ajax&__atk_callback=1&__atk_json=1']; + $files[] = ['_unit-test/exception.php?m_cb=ajax&__atk_callback=m_cb&__atk_json=1']; + $files[] = ['_unit-test/exception.php?m2_cb=ajax&__atk_callback=m2_cb&__atk_json=1']; return $files; } @@ -418,16 +417,15 @@ public function jsonResponsePostProvider(): array { $files = []; $files[] = [ - '_unit-test/post.php?test_form_submit=ajax&__atk_callback=1', + '_unit-test/post.php?test_submit=ajax&__atk_callback=test_submit', [ 'f1' => 'v1', - 'test_form_submit' => 'submit', ], ]; // for JsNotify coverage $files[] = [ - 'obsolete/notify2.php?notify_submit=ajax&__atk_callback=1', + 'obsolete/notify2.php?test_notify=ajax&__atk_callback=test_notify', [ 'text' => 'This text will appear in notification', 'icon' => 'warning sign', @@ -436,7 +434,6 @@ public function jsonResponsePostProvider(): array 'width' => '25%', 'position' => 'topRight', 'attach' => 'Body', - 'notify_submit' => 'submit', ], ]; diff --git a/tests/FormTest.php b/tests/FormTest.php index 64341e8037..aa72175c47 100644 --- a/tests/FormTest.php +++ b/tests/FormTest.php @@ -46,7 +46,9 @@ public function assertSubmit(array $post_data, \Closure $submit = null, \Closure { $submit_called = false; $_POST = $post_data; - $_POST['atk_submit'] = 'ajax'; + // trigger callback + $_GET['atk_submit'] = 'ajax'; + $_GET['__atk_callback'] = 'atk_submit'; $this->f->onSubmit(function (Form $form) use (&$submit_called, $submit) { $submit_called = true;