form as custom element

This commit is contained in:
Benjamin Dalsass
2025-12-23 09:03:01 +01:00
parent 7713a113cc
commit a627f8a471
11 changed files with 164 additions and 38 deletions

View File

@@ -47,7 +47,7 @@
align-items: center;
}
.form[aria-busy="true"] {
.turbo-refreshing{
opacity: .5;
}

View File

@@ -1,4 +1,10 @@
class ChoicesElement extends HTMLSelectElement {
// register the custom element
static {
customElements.define('choices-element', ChoicesElement, {extends: 'select'});
}
plugins = [];
connectedCallback() {
@@ -36,4 +42,4 @@ class ChoicesElement extends HTMLSelectElement {
}
}
customElements.define('choices-element', ChoicesElement, {extends: 'select'});

View File

@@ -0,0 +1,90 @@
class FormElement extends HTMLFormElement
{
static #TURBO_REFRESHING_CLASS = 'turbo-refreshing';
static #TURBO_TRIGGER_FIELD = '_turbo_trigger';
#aFormBlockDataTransmittedData = {};
// register the custom element
static {
customElements.define('itop-form-element', FormElement, {extends: 'form'});
}
TriggerTurbo(oElement) {
// Get the name and id of the element triggering turbo
const sName = oElement.getAttribute('name');
const sId = oElement.getAttribute('id');
if(FormElement.IsCheckbox(oElement) || this.#aFormBlockDataTransmittedData[sName] !== oElement.value) {
// Refresh UI
this.#StartRefreshingUI(sId);
// Pre Submit
this.#PreSubmitTurboForm(sName);
// Submit
oElement.form.requestSubmit();
// Post Submit
this.#PostSubmitTurboForm(sName)
this.#aFormBlockDataTransmittedData[sName] = oElement.value;
}
}
/**
* Start refreshing UI.
*
* @param sId
* @constructor
*/
#StartRefreshingUI(sId)
{
Array.from(this.querySelectorAll(`.ibo-content-block[data-impacted-by*="${sId}"]`)).forEach(block => {
block.classList.add(FormElement.#TURBO_REFRESHING_CLASS);
});
}
/**
* Pre submit the form.
* Set the turbo trigger field in the form and disable validation
*
* @param sName
* @constructor
*/
#PreSubmitTurboForm(sName)
{
this.querySelector(`[name="${this.getAttribute("name")}[${FormElement.#TURBO_TRIGGER_FIELD}]"]`).value = sName;
this.setAttribute('novalidate', true);
}
/**
* Post submit the form.
* Reset the turbo trigger field and restore form validation.
*
* @param sName
* @constructor
*/
#PostSubmitTurboForm(sName)
{
this.querySelector(`[name="${this.getAttribute("name")}[${FormElement.#TURBO_TRIGGER_FIELD}]"]`).value = null;
this.removeAttribute('novalidate');
}
/**
*
* @param oElement
* @returns {boolean}
*/
static IsCheckbox (oElement)
{
return oElement instanceof HTMLInputElement
&& oElement.getAttribute('type') === 'checkbox'
}
}

View File

@@ -1,27 +0,0 @@
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
$aFormBlockDataTransmittedData = {};
function triggerTurbo(el) {
const name = el.getAttribute('name');
if(isCheckbox(el) || $aFormBlockDataTransmittedData[name] !== el.value) {
let sFormName = el.form.getAttribute("name");
el.form.querySelector(`[name="${sFormName}[_turbo_trigger]"]`).value = el.getAttribute('name');
el.form.setAttribute('novalidate', true);
el.form.requestSubmit();
el.form.querySelector(`[name="${sFormName}[_turbo_trigger]"]`).value = null;
el.form.removeAttribute('novalidate');
$aFormBlockDataTransmittedData[name] = el.value;
}
}
function isCheckbox (element) {
return element instanceof HTMLInputElement
&& element.getAttribute('type') === 'checkbox'
}

View File

@@ -165,8 +165,6 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
{
parent::InitializeLinkedScripts();
$this->LinkScriptFromAppRoot('js/forms/forms.js');
// Used by forms
$this->LinkScriptFromAppRoot('js/leave_handler.js');
@@ -182,6 +180,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
$this->LinkScriptFromAppRoot('node_modules/selectize-plugin-a11y/selectize-plugin-a11y.js');
$this->LinkScriptFromAppRoot('js/jquery.multiselect.js');
$this->LinkScriptFromAppRoot('node_modules/tom-select/dist/js/tom-select.complete.min.js');
$this->LinkScriptFromAppRoot('js/forms/custom-elements/form.js');
$this->LinkScriptFromAppRoot('js/forms/custom-elements/choices.js');
$this->LinkScriptFromAppRoot('js/forms/custom-elements/oql.js');
$this->LinkScriptFromAppRoot('js/forms/custom-elements/collection.js');

View File

@@ -391,9 +391,19 @@ abstract class AbstractFormBlock implements IFormBlock
*
* @return bool
*/
public function ImpactDependentsBlocks(): bool
public function IsImpactingBlocks(): bool
{
return $this->oIORegister->ImpactDependentsBlocks();
return $this->oIORegister->IsImpactingBlocks();
}
/**
* Return the dependencies blocks.
*
* @return array
*/
public function GetImpactedBlocks(): array
{
return $this->oIORegister->GetImpactedBlocks();
}
/**

View File

@@ -100,6 +100,7 @@ class FormBuilder implements FormBuilderInterface, IteratorAggregate
$this->builder->add('_turbo_trigger', HiddenType::class, [
'prevent_form_build' => true,
'mapped' => false,
'priority' => 1
]);
}
}

View File

@@ -59,7 +59,8 @@ class FormTypeExtension extends AbstractTypeExtension
$view->vars['form_block_class'] = $options['form_block_class'];
$oFormBlock = $options['form_block'];
$view->vars['trigger_form_submit_on_modify'] = $oFormBlock->ImpactDependentsBlocks();
$view->vars['trigger_form_submit_on_modify'] = $oFormBlock->IsImpactingBlocks();
$view->vars['impacted_by'] = array_keys( $oFormBlock->GetImpactedBlocks());
}
}
}

View File

@@ -49,7 +49,6 @@ class FormOutput extends AbstractFormIO
*/
public function ConvertValue(mixed $oData): mixed
{
IssueLog::Error($this->GetName().' array:'.$this->IsArray());
if ($this->IsArray()) {
return $this->ConvertArrayValue($oData);
} else {

View File

@@ -261,7 +261,7 @@ class IORegister
*
* @return bool
*/
public function ImpactDependentsBlocks(): bool
public function IsImpactingBlocks(): bool
{
/** @var FormOutput $oFormOutput */
foreach ($this->aOutputs as $oFormOutput) {
@@ -273,6 +273,27 @@ class IORegister
return false;
}
/**
* Return the dependencies blocks.
*
* @return array
*/
public function GetImpactedBlocks(): array
{
$aBlocks = [];
/** @var FormInput $oFormInput */
foreach ($this->aInputs as $oFormInput) {
if ($oFormInput->IsBound()) {
$oBlock = $oFormInput->GetBinding()->oSourceIO->GetOwnerBlock();
$sId = FormBlockHelper::GetFormId($oBlock);
$aBlocks[$sId] = $oBlock;
}
}
return $aBlocks;
}
/**
* Get bound inputs bindings.
*

View File

@@ -1,14 +1,40 @@
{% use "application/forms/itop_base_layout.html.twig" %}
{# Widgets #}
{%- block form_start -%}
{%- do form.setMethodRendered() -%}
{% set method = method|upper %}
{%- if method in ["GET", "POST"] -%}
{% set form_method = method %}
{%- else -%}
{% set form_method = "POST" %}
{%- endif -%}
<form is="itop-form-element" {% if name != '' %} name="{{ name }}"{% endif %} method="{{ form_method|lower }}"{% if action != '' %} action="{{ action }}"{% endif %}{{ block('attributes') }}{% if multipart %} enctype="multipart/form-data"{% endif %}>
{%- if form_method != method -%}
<input type="hidden" name="_method" value="{{ method }}" />
{%- endif -%}
{%- endblock form_start -%}
{%- block form_end -%}
{%- if not render_rest is defined or render_rest -%}
{{ form_rest(form) }}
{%- endif -%}
</form>
{%- endblock form_end -%}
{%- block widget_attributes -%}
{{- parent() -}}
{% if trigger_form_submit_on_modify %}
onChange="triggerTurbo(this);"
onChange="this.form.TriggerTurbo(this);"
{% endif %}
{%- endblock widget_attributes -%}
{%- block attributes -%}
{{- parent() -}}
{% if impacted_by is not empty %}
data-impacted-by="{{ impacted_by|join(',') }}"
{% endif %}
{%- endblock attributes -%}
{%- block form_widget_simple -%}
{% if type == 'text' %}{% set ibo_class='ibo-input-string' %}{% else %}{% set ibo_class='ibo-input-' ~ type %}{% endif %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' ibo-input ' ~ ibo_class)|trim}) %}