1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\Form;
6:
7: use Atk4\Data\Field;
8: use Atk4\Data\Model;
9: use Atk4\Data\Model\EntityFieldPair;
10: use Atk4\Ui\Exception;
11: use Atk4\Ui\Form;
12: use Atk4\Ui\Js\Jquery;
13: use Atk4\Ui\Js\JsExpression;
14: use Atk4\Ui\Js\JsExpressionable;
15: use Atk4\Ui\View;
16:
17: /**
18: * Provides generic functionality for a form control.
19: *
20: * @phpstan-type JsCallbackSetClosure \Closure(Jquery, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed): (JsExpressionable|View|string|void)
21: */
22: class Control extends View
23: {
24: /** @var Form|null to which this field belongs */
25: public $form;
26:
27: /** @var EntityFieldPair<Model, Field>|null */
28: public $entityField;
29:
30: /** @var string */
31: public $controlClass = '';
32:
33: /** @var bool Whether you need this field to be rendered wrap in a form layout or as his */
34: public bool $layoutWrap = true;
35:
36: /** @var bool rendered or not input label in generic Form\Layout template. */
37: public $renderLabel = true;
38:
39: /** @var string Specify width for Fomantic-UI grid. For "four wide" use 'four'. */
40: public $width;
41:
42: /**
43: * Caption is a text that must appear somewhere nearby the field. For a form with layout, this
44: * will typically place caption above the input field, but for checkbox this may appear next to the
45: * checkbox itself. If Form Layout does not have captions above the input field, then caption
46: * will appear as a placeholder of the input fields and it may also appear as a tooltip.
47: *
48: * Caption is usually specified by a model.
49: *
50: * @var string|null
51: */
52: public $caption;
53:
54: /**
55: * Placed as a pointing label below the field. This only works when Form\Control appears in a form. You can also
56: * set this to object, such as \Atk4\Ui\Text otherwise HTML characters are escaped.
57: *
58: * @var string|View|array
59: */
60: public $hint;
61:
62: /** Disabled field is not editable and will not be submitted. */
63: public bool $disabled = false;
64:
65: /** Read-only field is not editable, but will be submitted. */
66: public bool $readOnly = false;
67:
68: #[\Override]
69: protected function init(): void
70: {
71: parent::init();
72:
73: if ($this->form && $this->entityField) {
74: if (isset($this->form->controls[$this->entityField->getFieldName()])) {
75: throw (new Exception('Form field already exists'))
76: ->addMoreInfo('name', $this->entityField->getFieldName());
77: }
78: $this->form->controls[$this->entityField->getFieldName()] = $this;
79: }
80: }
81:
82: /**
83: * Sets the value of this field. If field is a part of the form and is associated with
84: * the model, then the model's value will also be affected.
85: *
86: * @param mixed $value
87: */
88: #[\Override]
89: public function set($value = null)
90: {
91: if ($this->entityField) {
92: $this->entityField->set($value);
93: } else {
94: $this->content = $value;
95: }
96:
97: return $this;
98: }
99:
100: #[\Override]
101: protected function renderView(): void
102: {
103: // it only makes sense to have "name" property inside a field if used inside a form
104: if ($this->form) {
105: $this->template->trySet('name', $this->shortName);
106: }
107:
108: parent::renderView();
109: }
110:
111: #[\Override]
112: protected function renderTemplateToHtml(): string
113: {
114: $output = parent::renderTemplateToHtml();
115:
116: $form = $this->getClosestOwner(Form::class);
117:
118: return $form !== null ? $form->fixOwningFormAttrInRenderedHtml($output) : $output;
119: }
120:
121: /**
122: * Shorthand method for on('change') event.
123: * Some input fields, like Calendar, could call this differently.
124: *
125: * If $expr is JsExpressionable, then it will execute it instantly.
126: * If $expr is callback method, then it'll make additional request to webserver.
127: *
128: * Examples:
129: * $control->onChange(new JsExpression('console.log(\'changed\')'));
130: * $control->onChange(new JsExpression('$(this).parents(\'.form\').form(\'submit\')'));
131: *
132: * @param JsExpressionable|JsCallbackSetClosure|array{JsCallbackSetClosure} $expr
133: * @param array|bool $defaults
134: */
135: public function onChange($expr, $defaults = []): void
136: {
137: if (is_bool($defaults)) {
138: $defaults = $defaults ? [] : ['preventDefault' => false, 'stopPropagation' => false];
139: }
140:
141: $this->on('change', '#' . $this->name . '_input', $expr, $defaults);
142: }
143:
144: /**
145: * Method similar to View::js() however will adjust selector
146: * to target the "input" element.
147: *
148: * $field->jsInput(true)->val(123);
149: *
150: * @param bool|string $when
151: * @param ($when is false ? null : JsExpressionable|null) $action
152: *
153: * @return ($action is null ? Jquery : null)
154: */
155: public function jsInput($when = false, $action = null): ?JsExpressionable
156: {
157: return $this->js($when, $action, '#' . $this->name . '_input');
158: }
159: }
160: