1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\VueComponent;
6:
7: use Atk4\Data\Model;
8: use Atk4\Data\ValidationException;
9: use Atk4\Ui\Js\JsExpressionable;
10: use Atk4\Ui\Js\JsToast;
11: use Atk4\Ui\JsCallback;
12: use Atk4\Ui\View;
13:
14: /**
15: * A Simple inline editable text Vue component.
16: */
17: class InlineEdit extends View
18: {
19: public $defaultTemplate = 'inline-edit.html';
20:
21: /** @var JsCallback JsCallback for saving data. */
22: public $cb;
23:
24: /** @var mixed Input initial value. */
25: public $initValue;
26:
27: /**
28: * Whether callback should save value to db automatically or not.
29: * Default to using onChange handler.
30: * If set to true, then saving to db will be done when model get set
31: * and if model is loaded already.
32: *
33: * @var bool
34: */
35: public $autoSave = false;
36:
37: /**
38: * The actual db field name that need to be saved.
39: * Default to title field when model is set.
40: *
41: * @var string|null the name of the field
42: */
43: public $fieldName;
44:
45: /**
46: * Whether component should save it's value when input get blur.
47: * Using this option will trigger callback when user is moving out of the
48: * inline edit field, like pressing tab for example.
49: *
50: * Otherwise, callback is fire when pressing Enter key,
51: * while inside the inline input field, only.
52: *
53: * @var bool
54: */
55: public $saveOnBlur = true;
56:
57: /** @var string Default CSS for the input div. */
58: public $inputCss = 'ui right icon input';
59:
60: /**
61: * The validation error msg function.
62: * This function is call when a validation error occur and
63: * give you a chance to format the error msg display inside
64: * errorNotifier.
65: *
66: * A default one is supply if this is null.
67: * It receive the error ($e) as parameter.
68: *
69: * @var \Closure(ValidationException, string): string|null
70: */
71: public $formatErrorMsg;
72:
73: #[\Override]
74: protected function init(): void
75: {
76: parent::init();
77:
78: $this->cb = JsCallback::addTo($this);
79:
80: // set default validation error handler
81: if (!$this->formatErrorMsg) {
82: $this->formatErrorMsg = function (ValidationException $e, string $value) {
83: $caption = $this->model->getField($this->fieldName)->getCaption();
84:
85: return $caption . ' - ' . $e->getMessage() . '. <br>Trying to set this value: "' . $value . '"';
86: };
87: }
88: }
89:
90: #[\Override]
91: public function setModel(Model $entity): void
92: {
93: parent::setModel($entity);
94:
95: if ($this->fieldName === null) {
96: $this->fieldName = $this->model->titleField;
97: }
98:
99: if ($this->autoSave && $this->model->isLoaded()) {
100: $this->cb->set(function () {
101: $postValue = $this->getApp()->getRequestPostParam('value');
102: try {
103: $this->model->set($this->fieldName, $this->getApp()->uiPersistence->typecastLoadField($this->model->getField($this->fieldName), $postValue));
104: $this->model->save();
105:
106: return $this->jsSuccess('Update saved');
107: } catch (ValidationException $e) {
108: $this->getApp()->terminateJson([
109: 'success' => true,
110: 'hasValidationError' => true,
111: 'atkjs' => $this->jsError(($this->formatErrorMsg)($e, $postValue))->jsRender(),
112: ]);
113: }
114: });
115: }
116: }
117:
118: /**
119: * You may supply your own function to handle update.
120: * The function will receive one param:
121: * value: the new input value.
122: *
123: * @param \Closure(mixed): (JsExpressionable|View|string|void) $fx
124: */
125: public function onChange(\Closure $fx): void
126: {
127: if (!$this->autoSave) {
128: $value = $this->getApp()->uiPersistence->typecastLoadField(
129: $this->model->getField($this->fieldName),
130: $this->getApp()->tryGetRequestPostParam('value')
131: );
132: $this->cb->set(static function () use ($fx, $value) {
133: return $fx($value);
134: });
135: }
136: }
137:
138: public function jsSuccess(string $message): JsExpressionable
139: {
140: return new JsToast([
141: 'title' => 'Success',
142: 'message' => $message,
143: 'class' => 'success',
144: ]);
145: }
146:
147: /**
148: * @param string $message
149: */
150: public function jsError($message): JsExpressionable
151: {
152: return new JsToast([
153: 'title' => 'Validation error:',
154: 'displayTime' => 8000,
155: 'showIcon' => 'exclamation',
156: 'message' => $message,
157: 'class' => 'error',
158: ]);
159: }
160:
161: #[\Override]
162: protected function renderView(): void
163: {
164: parent::renderView();
165:
166: if ($this->model && $this->model->isLoaded()) {
167: $initValue = $this->model->get($this->fieldName);
168: } else {
169: $initValue = $this->initValue;
170: }
171:
172: $fieldName = $this->fieldName ?? 'name';
173:
174: $this->vue('AtkInlineEdit', [
175: 'initValue' => $initValue,
176: 'url' => $this->cb->getJsUrl(),
177: 'saveOnBlur' => $this->saveOnBlur,
178: 'options' => ['fieldName' => $fieldName, 'inputCss' => $this->inputCss],
179: ]);
180: }
181: }
182: