1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui;
6:
7: use Atk4\Ui\Js\JsChain;
8: use Atk4\Ui\Js\JsExpressionable;
9:
10: /**
11: * Dynamically render it's content.
12: * To provide content for a loader, use set() callback.
13: */
14: class Loader extends View
15: {
16: public $ui = 'segment';
17:
18: /**
19: * Shim is a filler object that is displayed inside loader while the actual content is fetched
20: * from the server. You may supply an object here or a seed. This view will be replaced
21: * by an actual content when loading stops. Additionally there will be loading indicator
22: * on top of this content.
23: *
24: * @var View
25: */
26: public $shim;
27:
28: /**
29: * Specify which event will cause Loader to begin fetching it's actual data. In some cases
30: * you would want to wait. You can set a custom JavaScript event name then trigger() it.
31: *
32: * Default value is `true` which means loading will take place as soon as possible. Setting this
33: * to `false` will disable event entirely.
34: *
35: * @var bool|string
36: */
37: public $loadEvent = true;
38:
39: /** @var Callback for triggering */
40: public $cb;
41:
42: /** @var array URL arguments. */
43: public $urlArgs = [];
44:
45: #[\Override]
46: protected function init(): void
47: {
48: parent::init();
49:
50: if (!$this->shim) { // @phpstan-ignore-line
51: $this->shim = [View::class, 'class' => ['padded segment'], 'style' => ['min-height' => '5em']];
52: }
53:
54: if (!$this->cb) { // @phpstan-ignore-line
55: $this->cb = Callback::addTo($this);
56: }
57: }
58:
59: /**
60: * Set callback function for this loader.
61: *
62: * The loader view is pass as an argument to the loader callback function.
63: * This allow to easily update the loader view content within the callback.
64: * $l1 = Loader::addTo($layout);
65: * $l1->set(function (Loader $p) {
66: * do_long_processing_action();
67: * $p->set('new content');
68: * });
69: *
70: * @param \Closure($this): void $fx
71: */
72: #[\Override]
73: public function set($fx = null)
74: {
75: if (!$fx instanceof \Closure) {
76: throw new \TypeError('$fx must be of type Closure');
77: }
78:
79: $this->cb->set(function () use ($fx) {
80: $fx($this);
81: $this->cb->terminateJson($this);
82: });
83:
84: return $this;
85: }
86:
87: /**
88: * Automatically call the jsLoad on a supplied event unless it was already triggered
89: * or if user have invoked jsLoad manually.
90: */
91: #[\Override]
92: protected function renderView(): void
93: {
94: if (!$this->cb->isTriggered()) {
95: if ($this->loadEvent) {
96: $this->js($this->loadEvent, $this->jsLoad($this->urlArgs));
97: }
98: $this->add($this->shim);
99: }
100:
101: parent::renderView();
102: }
103:
104: /**
105: * Return a JS action that will trigger the loader to start.
106: *
107: * @param string $storeName
108: *
109: * @return JsChain
110: */
111: public function jsLoad(array $args = [], array $apiConfig = [], $storeName = null): JsExpressionable
112: {
113: return $this->js()->atkReloadView([
114: 'url' => $this->cb->getUrl(),
115: 'urlOptions' => $args,
116: 'apiConfig' => $apiConfig !== [] ? $apiConfig : null,
117: 'storeName' => $storeName,
118: ]);
119: }
120: }
121: