Skip to content

Commit

Permalink
Add loadable typecast for JsCallback arguments (#2225)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek authored Nov 23, 2024
1 parent aa8a745 commit c480f98
Show file tree
Hide file tree
Showing 33 changed files with 367 additions and 98 deletions.
2 changes: 1 addition & 1 deletion demos/_unit-test/grid-rowclick.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
return new JsToast(['message' => 'Clicked Action MenuItem']);
});

$grid->addModalAction('Action Modal', 'Details', static function (View $p, $id) use ($model) {
$grid->addModalAction('Action Modal', 'Details', static function (View $p, WrappedId $id) use ($model) {
Message::addTo($p, ['Clicked Action Modal: ' . $model->load($id)->name]);
});

Expand Down
44 changes: 44 additions & 0 deletions demos/_unit-test/useraction-input-callback.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Atk4\Ui\Demos;

use Atk4\Data\Model\UserAction;
use Atk4\Ui\App;
use Atk4\Ui\Form;

/** @var App $app */
require_once __DIR__ . '/../init-app.php';

$country = new Country($app->db);

$country->addUserAction('greetInteger', [
'appliesTo' => UserAction::APPLIES_TO_NO_RECORD,
'args' => [
'foo' => [
'type' => 'integer',
'required' => true,
],
],
'callback' => static function (Country $entity, int $foo) {
return 'Hello II ' . $foo;
},
]);

Form\Control\Line::addTo($app, ['action' => $country->getUserAction('greetInteger')]);

$country->addUserAction('greetWrappedId', [
'appliesTo' => UserAction::APPLIES_TO_NO_RECORD,
'args' => [
'foo' => [
'type' => WrappedIdType::NAME,
'required' => true,
],
],
'callback' => static function (Country $entity, WrappedId $foo) {
return 'Hello III ' . $foo->getId();
},
]);

Form\Control\Line::addTo($app, ['action' => $country->getUserAction('greetWrappedId')]);
3 changes: 2 additions & 1 deletion demos/basic/menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Atk4\Ui\Dropdown as UiDropdown;
use Atk4\Ui\Form;
use Atk4\Ui\Header;
use Atk4\Ui\Js\JsToast;
use Atk4\Ui\Menu;
use Atk4\Ui\View;

Expand All @@ -21,7 +22,7 @@
$dropdown = UiDropdown::addTo($menu, ['With Callback', 'dropdownOptions' => ['on' => 'hover']]);
$dropdown->setSource(['a', 'b', 'c']);
$dropdown->onChange(static function (string $itemId) {
return 'New selected item ID: ' . $itemId;
return new JsToast('New selected item ID: ' . $itemId);
});

$submenu = $menu->addMenu('Sub-menu');
Expand Down
8 changes: 3 additions & 5 deletions demos/collection/grid.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,14 @@
// creating a button for executing model test user action
$grid->addExecutorButton($grid->getExecutorFactory()->createExecutor($model->getUserAction('test'), $grid));

$grid->addActionButton('Say HI', static function (Jquery $j, $id) use ($grid) {
$grid->addActionButton('Say HI', static function (Jquery $j, WrappedId $id) use ($grid) {
$model = Country::assertInstanceOf($grid->model);

$id = $grid->getApp()->uiPersistence->typecastAttributeLoadField($grid->model->getIdField(), $id); // TODO fix asap, this line must not be needed!

return new JsToast('Loaded "' . $model->load($id)->name . '" from ID=' . $id->getId());
});

$grid->addModalAction(['icon' => 'external'], 'Modal Test', static function (View $p, $id) {
Message::addTo($p, ['Clicked on ID=' . $id]);
$grid->addModalAction(['icon' => 'external'], 'Modal Test', static function (View $p, WrappedId $id) {
Message::addTo($p, ['Clicked on ID=' . $id->getId()]);
});

// creating an executor for delete action
Expand Down
6 changes: 3 additions & 3 deletions demos/interactive/scroll-grid-container.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@
$g1->addQuickSearch([Country::hinting()->fieldName()->name, Country::hinting()->fieldName()->iso]);

// demo for additional action buttons in Crud + JsPaginator
$g1->addModalAction(['icon' => 'cogs'], 'Details', static function (View $p, $id) use ($g1) {
$g1->addModalAction(['icon' => 'cogs'], 'Details', static function (View $p, WrappedId $id) use ($g1) {
Card::addTo($p)->setModel($g1->model->load($id));
});
$g1->addActionButton('red', static function (Jquery $js, $id) {
return $js->find('tr[data-id=' . $id . ']')->css('color', 'red');
$g1->addActionButton('red', static function (Jquery $js, WrappedId $id) use ($app, $m1) {
return $js->find('tr[data-id=' . $app->uiPersistence->typecastAttributeSaveField($m1->getIdField(), $id) . ']')->css('color', 'red');
});
// THIS SHOULD GO AFTER YOU CALL Grid::addActionButton()
$g1->addJsPaginatorInContainer(30, 350);
Expand Down
9 changes: 6 additions & 3 deletions demos/javascript/js.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Atk4\Ui\Header;
use Atk4\Ui\Js\Jquery;
use Atk4\Ui\Js\JsBlock;
use Atk4\Ui\Js\JsCallbackLoadableValue;
use Atk4\Ui\Js\JsExpression;
use Atk4\Ui\Label;

Expand Down Expand Up @@ -75,6 +76,8 @@

$label = Label::addTo($app->layout, ['Test']);

$label->on('click', null, static function (Jquery $j, $arg1) {
return 'width is ' . $arg1;
}, [new JsExpression('$(window).width()')]);
$label->on('click', null, static function (Jquery $j, int $windowWidth) {
return 'width is ' . $windowWidth;
}, [
new JsCallbackLoadableValue(new JsExpression('$(window).width()'), static fn ($v) => (int) $v),
]);
30 changes: 18 additions & 12 deletions docs/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,34 +312,40 @@ will send browser screen width back to the callback:
$label = \Atk4\Ui\Label::addTo($app);
$cb = \Atk4\Ui\JsCallback::addTo($label);
$cb->set(function (\Atk4\Ui\Js\Jquery $j, $arg1) {
return 'width is ' . $arg1;
}, [new \Atk4\Ui\Js\JsExpression('$(window).width()')]);
$cb->set(function (\Atk4\Ui\Js\Jquery $j, int $windowWidth) {
return 'width is ' . $windowWidth;
}, [
new \Atk4\Ui\Js\JsCallbackLoadableValue('$(window).width()', static fn ($v) => (int) $v),
]);
$label->detail = $cb->getUrl();
$label->on('click', $cb);
```

In here you see that I'm using a 2nd argument to $cb->set() to specify arguments, which, I'd like to fetch from the
browser. Those arguments are passed to the callback and eventually arrive as $arg1 inside my callback. The {php:meth}`View::on()`
also supports argument passing:
browser. Those arguments are passed to the callback and eventually arrive as $windowWidth inside my callback.
The {php:meth}`View::on()` also supports argument passing:

```
$label = \Atk4\Ui\Label::addTo($app, ['Callback test']);
$label->on('click', function (Jquery $j, $arg1) {
return 'width is ' . $arg1;
}, ['confirm' => 'sure?', 'args' => [new \Atk4\Ui\Js\JsExpression('$(window).width()')]]);
$label->on('click', function (Jquery $j, int $windowWidth) {
return 'width is ' . $windowWidth;
}, ['confirm' => 'sure?', 'args' => [
new \Atk4\Ui\Js\JsCallbackLoadableValue('$(window).width()', static fn ($v) => (int) $v),
]]);
```

If you do not need to specify confirm, you can actually pass arguments in a key-less array too:

```
$label = \Atk4\Ui\Label::addTo($app, ['Callback test']);
$label->on('click', function (Jquery $j, $arg1) {
return 'width is ' . $arg1;
}, [new \Atk4\Ui\Js\JsExpression('$(window).width()')]);
$label->on('click', function (Jquery $j, int $windowWidth) {
return 'width is ' . $windowWidth;
}, [
new \Atk4\Ui\Js\JsCallbackLoadableValue('$(window).width()', static fn ($v) => (int) $v),
]);
```

## Referring to event origin
Expand All @@ -348,7 +354,7 @@ You might have noticed that JsCallback now passes first argument ($j) which so f
jQuery chain for the element which received the event. We can change the response to do something with this element like:

```
return $j->text('width is ' . $arg1);
return $j->text('width is ' . $windowWidth);
```

Now instead of showing an alert box, label content will be changed to display window width.
Expand Down
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ parameters:
-
path: 'src/JsCallback.php'
identifier: method.childParameterType
message: '~^Parameter #2 \$args \(array<int\|string, Atk4\\Ui\\Js\\JsExpressionable\|string>\) of method Atk4\\Ui\\JsCallback::set\(\) should be contravariant with parameter \$fxArgs \(list<mixed>\) of method Atk4\\Ui\\Callback::set\(\)$~'
message: '~^Parameter #2 \$args \(array<int\|string, Atk4\\Ui\\Js\\JsCallbackLoadableValue>\) of method Atk4\\Ui\\JsCallback::set\(\) should be contravariant with parameter \$fxArgs \(list<mixed>\) of method Atk4\\Ui\\Callback::set\(\)$~'
count: 1
-
path: 'src/Loader.php'
Expand Down
8 changes: 7 additions & 1 deletion src/Dropdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Atk4\Ui;

use Atk4\Ui\Js\Jquery;
use Atk4\Ui\Js\JsCallbackLoadableValue;
use Atk4\Ui\Js\JsExpression;
use Atk4\Ui\Js\JsExpressionable;
use Atk4\Ui\Js\JsFunction;
Expand Down Expand Up @@ -55,7 +56,12 @@ public function onChange(\Closure $fx): void

$this->cb->set(static function (Jquery $j, string $value) use ($fx) {
return $fx($value);
}, ['item' => 'value']);
}, ['item' => new JsCallbackLoadableValue(null, function ($v) {
return $this->getApp()->uiPersistence->typecastLoadField(
$this->model->getField('id'),
$v
);
})]);
}

#[\Override]
Expand Down
4 changes: 3 additions & 1 deletion src/Form/Control/DropdownCascade.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ function (Jquery $j) use ($cascadeFromValue) {
$this->jsDropdown()->addClass('loading'),
];

$this->cascadeFrom->onChange($expr, ['args' => [$this->cascadeFrom->name => $this->cascadeFrom->jsInput()->val()]]);
$this->cascadeFrom->onChange($expr, ['args' => [
$this->cascadeFrom->name => $this->cascadeFrom->jsInput()->val(),
]]);
}

#[\Override]
Expand Down
21 changes: 18 additions & 3 deletions src/Form/Control/Input.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

namespace Atk4\Ui\Form\Control;

use Atk4\Data\Field;
use Atk4\Data\Model\UserAction;
use Atk4\Ui\AbstractView;
use Atk4\Ui\Button;
use Atk4\Ui\Exception;
use Atk4\Ui\Form;
use Atk4\Ui\Icon;
use Atk4\Ui\Js\JsCallbackLoadableValue;
use Atk4\Ui\Label;
use Atk4\Ui\UserAction\ExecutorFactory;
use Atk4\Ui\UserAction\ExecutorInterface;
Expand Down Expand Up @@ -157,12 +160,24 @@ protected function prepareRenderButton($button, $spot)
? $this->getExecutorFactory()->createExecutor($button, $this, ExecutorFactory::JS_EXECUTOR)
: $button;
$button = $this->add($this->getExecutorFactory()->createTrigger($executor->getAction()), $spot);
if ($executor->getAction()->args) {

if (count($executor->getAction()->args) === 0) {
$button->on('click', $executor);
} elseif (count($executor->getAction()->args) === 1) {
$actionArgName = array_key_first($executor->getAction()->args);
$actionArgType = $executor->getAction()->args[$actionArgName]['type'];

$button->on('click', $executor, ['args' => [
array_key_first($executor->getAction()->args) => $this->jsInput()->val(),
$actionArgName => new JsCallbackLoadableValue($this->jsInput()->val(), function ($v) use ($actionArgType) {
return $this->getApp()->uiPersistence->typecastLoadField(
new Field(['type' => $actionArgType]),
$v
);
}),
]]);
} else {
$button->on('click', $executor);
throw (new Exception('Input form control supports user action with zero or one argument only'))
->addMoreInfo('arguments', array_keys($executor->getAction()->args));
}
}
if (!$button->isInitialized()) { // TODO if should be replaced with new method like View::addOrAssertRegion() which will add the element and otherwise assert the owner and region
Expand Down
11 changes: 7 additions & 4 deletions src/Form/Control/TreeItemSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Atk4\Ui\Form;
use Atk4\Ui\HtmlTemplate;
use Atk4\Ui\Js\Jquery;
use Atk4\Ui\Js\JsCallbackLoadableValue;
use Atk4\Ui\Js\JsExpressionable;
use Atk4\Ui\JsCallback;
use Atk4\Ui\View;
Expand Down Expand Up @@ -93,14 +94,16 @@ protected function init(): void
*/
public function onItem(\Closure $fx): void
{
$this->cb = JsCallback::addTo($this)->set(function (Jquery $j, $data) use ($fx) {
$value = $this->getApp()->decodeJson($data);
$this->cb = JsCallback::addTo($this)->set(static function (Jquery $j, $value) use ($fx) {
return $fx($value);
}, ['data' => new JsCallbackLoadableValue(null, function ($v) {
$value = $this->getApp()->decodeJson($v);
if (!$this->allowMultiple) {
$value = $value[0];
}

return $fx($value);
}, ['data' => 'value']);
return $value;
})]);
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/Form/Control/Upload.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

use Atk4\Ui\Button;
use Atk4\Ui\Exception;
use Atk4\Ui\Js\Jquery;
use Atk4\Ui\Js\JsBlock;
use Atk4\Ui\Js\JsCallbackLoadableValue;
use Atk4\Ui\Js\JsExpressionable;
use Atk4\Ui\JsCallback;

Expand Down Expand Up @@ -190,16 +192,14 @@ public function onDelete(\Closure $fx): void
{
$this->hasDeleteCb = true;
if ($this->getApp()->tryGetRequestPostParam('fUploadAction') === self::DELETE_ACTION) {
$this->cb->set(function () use ($fx) {
$fileId = $this->getApp()->getRequestPostParam('fUploadId');

$this->cb->set(function (Jquery $j, string $fileId) use ($fx) {
$jsRes = $fx($fileId);
if ($jsRes !== null) { // @phpstan-ignore notIdentical.alwaysTrue (https://github.com/phpstan/phpstan/issues/9388)
$this->addJsAction($jsRes);
}

return new JsBlock($this->jsActions);
});
}, ['fUploadId' => new JsCallbackLoadableValue(null, static fn ($v) => $v)]);
}
}

Expand Down
11 changes: 8 additions & 3 deletions src/Grid.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Atk4\Data\Field;
use Atk4\Data\Model;
use Atk4\Ui\Js\Jquery;
use Atk4\Ui\Js\JsCallbackLoadableValue;
use Atk4\Ui\Js\JsExpressionable;
use Atk4\Ui\Js\JsReload;
use Atk4\Ui\UserAction\ConfirmationExecutor;
Expand Down Expand Up @@ -535,9 +536,13 @@ private function explodeSelectionValue(string $value): array
public function addBulkAction($item, \Closure $callback, $args = [])
{
$menuItem = $this->menu->addItem($item);
$menuItem->on('click', function (Jquery $j, string $value) use ($callback) {
return $callback($j, $this->explodeSelectionValue($value));
}, [$this->selection->jsChecked()]);
$menuItem->on('click', static function (Jquery $j, array $ids) use ($callback) {
return $callback($j, $ids);
}, [
new JsCallbackLoadableValue($this->selection->jsChecked(), function ($v) {
return $this->explodeSelectionValue($v);
}),
]);

return $menuItem;
}
Expand Down
42 changes: 42 additions & 0 deletions src/Js/JsCallbackLoadableValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace Atk4\Ui\Js;

use Atk4\Core\WarnDynamicPropertyTrait;

class JsCallbackLoadableValue implements JsExpressionable
{
use WarnDynamicPropertyTrait;

private JsExpressionable $jsValue;

/** @var \Closure(string): mixed */
private \Closure $loadTypecastFx;

/**
* @param \Closure(string): mixed $loadTypecastFx
*/
public function __construct(?JsExpressionable $jsValue, \Closure $loadTypecastFx)
{
if ($jsValue !== null) {
$this->jsValue = $jsValue;
}
$this->loadTypecastFx = $loadTypecastFx;
}

#[\Override]
public function jsRender(): string
{
return $this->jsValue->jsRender();
}

/**
* @return mixed
*/
public function typecastLoadValue(string $value)
{
return ($this->loadTypecastFx)($value);
}
}
Loading

0 comments on commit c480f98

Please sign in to comment.