N°8772 - dynamic form

This commit is contained in:
Benjamin Dalsass
2025-11-12 17:09:40 +01:00
parent 3d2485a004
commit 5cacfcb754
18 changed files with 320 additions and 296 deletions

View File

@@ -7,18 +7,19 @@
namespace Combodo\iTop\Forms\Block;
use Combodo\iTop\Forms\Block\Base\FormBlock;
use Combodo\iTop\Forms\Block\IO\AbstractFormIO;
use Combodo\iTop\Forms\Block\IO\Converter\AbstractConverter;
use Combodo\iTop\Forms\Block\IO\Format\RawFormat;
use Combodo\iTop\Forms\Block\IO\FormInput;
use Combodo\iTop\Forms\Block\IO\FormOutput;
use Combodo\iTop\Forms\IFormBlock;
use Forms\BlockIO;
/**
* Abstract form block.
*
* A form block describe a form (complex or simple type).
* A complex form have sub blocks.
* It defines its inputs and outputs.
* Inputs / Outputs.
* Options.
*
*/
abstract class AbstractFormBlock implements IFormBlock
@@ -96,7 +97,12 @@ abstract class AbstractFormBlock implements IFormBlock
return $this->oParent;
}
public function HasParent(): bool
/**
* Return true if this block is root.
*
* @return bool
*/
public function IsRootBlock(): bool
{
return $this->oParent !== null;
}
@@ -111,28 +117,6 @@ abstract class AbstractFormBlock implements IFormBlock
return $this->sName;
}
public function GetIdentifier(): string
{
$sParentName = $this->GetParent()?->GetIdentifier();
if (is_null($sParentName)) {
return $this->GetName();
}
return $sParentName.'_'.$this->sName;
}
public function GetPath(): array
{
$aPath = [];
$oCurrent = $this;
do {
$aPath[] = $oCurrent->GetName();
$oCurrent = $oCurrent->getParent();
} while ($oCurrent->HasParent());
return array_reverse($aPath);
}
/**
* Return the form block options.
* Options will be passed to FormType for building.
@@ -170,7 +154,7 @@ abstract class AbstractFormBlock implements IFormBlock
* @param string $sName the input name
* @param string $sType the type of the input
*
* @return void
* @return AbstractFormBlock
*/
public function AddInput(string $sName, string $sType): AbstractFormBlock
{
@@ -180,6 +164,27 @@ abstract class AbstractFormBlock implements IFormBlock
return $this;
}
/**
* Add an input connected to another block.
*
* @param string $sName the input name
* @param string $sOutputBlockName
* @param string $sOutputName
*
* @return AbstractFormBlock
* @throws FormBlockException
*/
public function AddInputDependsOn(string $sName, string $sOutputBlockName, string $sOutputName): AbstractFormBlock
{
$oOutputBlock = $this->GetParent()->Get($sOutputBlockName);
$oBlockOutput = $oOutputBlock->GetOutput($sOutputName);
$this->AddInput($sName, $oBlockOutput->GetDataType());
$this->DependsOn($sName, $sOutputBlockName, $sOutputName);
return $this;
}
/**
* Get an input.
*
@@ -204,7 +209,7 @@ abstract class AbstractFormBlock implements IFormBlock
* @param string $sType
* @param AbstractConverter|null $oConverter
*
* @return void
* @return AbstractFormBlock
*/
public function AddOutput(string $sName, string $sType, AbstractConverter $oConverter = null): AbstractFormBlock
{
@@ -342,11 +347,11 @@ abstract class AbstractFormBlock implements IFormBlock
}
/**
* Get bindings on inputs.
* Get bound inputs bindings.
*
* @return array
*/
public function GetInputsBindings(): array
public function GetBoundInputsBindings(): array
{
$aBindings = [];
@@ -361,11 +366,11 @@ abstract class AbstractFormBlock implements IFormBlock
}
/**
* Get bindings on outputs.
* Get bound outputs bindings.
*
* @return array
*/
public function GetOutputBindings(): array
public function GetBoundOutputBindings(): array
{
$aBindings = [];
@@ -379,26 +384,6 @@ abstract class AbstractFormBlock implements IFormBlock
return $aBindings;
}
/**
* Inputs data ready.
*
* @param string|null $sType
*
* @return bool
*/
public function IsInputsDataReady(string $sType = null): bool
{
foreach ($this->aFormInputs as $oFormInput) {
if ($oFormInput->IsBound()) {
if (!$oFormInput->IsEventDataReady($sType)) {
return false;
}
}
}
return true;
}
/**
* The block has been added to its parent.
*
@@ -421,6 +406,26 @@ abstract class AbstractFormBlock implements IFormBlock
$this->bIsAddedToForm = $bIsAdded;
}
/**
* Inputs data ready.
*
* @param string|null $sType
*
* @return bool
*/
public function IsInputsDataReady(string $sType = null): bool
{
foreach ($this->aFormInputs as $oFormInput) {
if ($oFormInput->IsBound()) {
if (!$oFormInput->IsEventDataReady($sType)) {
return false;
}
}
}
return true;
}
/**
* Compute outputs values.
*
@@ -438,19 +443,9 @@ abstract class AbstractFormBlock implements IFormBlock
$oFormOutput->ComputeValue($sEventType, $oData);
}
}
/**
* Propagate inputs values.
*
* @return void
*/
public function PropagateInputsValues(): void
{
foreach ($this->aFormInputs as $oFormInput) {
$oFormInput->PropagateBindingsValues();
}
}
/**
* Initialize inputs.
@@ -471,7 +466,27 @@ abstract class AbstractFormBlock implements IFormBlock
$this->AddOutput(self::OUTPUT_VALUE, RawFormat::class);
}
public function InputHasChanged()
/**
* Called when a binding value has been transmitted.
*
* @param AbstractFormIO $oBlockIO
*
* @return void
*/
public function BindingReceivedEvent(AbstractFormIO $oBlockIO): void
{
if ($this->IsInputsDataReady()) {
$this->AllInputsReadyEvent();
}
}
/**
* Called when all inputs are ready.
*
* @return void
*/
public function AllInputsReadyEvent(): void
{
}
}

View File

@@ -35,12 +35,11 @@ abstract class AbstractTypeFormBlock extends AbstractFormBlock
* @param string|null $sEventType
*
* @return bool
* @throws \Combodo\iTop\Forms\Block\FormBlockException
* @throws FormBlockException
*/
public function IsVisible(string $sEventType = null): bool
{
$oInput = $this->GetInput(self::INPUT_VISIBLE);
if(!$oInput->IsBound()){
return true;
}

View File

@@ -8,6 +8,8 @@ namespace Combodo\iTop\Forms\Block\Base;
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
use Combodo\iTop\Forms\Block\FormType\ChoiceFormType;
use Combodo\iTop\Forms\Block\IO\Converter\ChoiceValueToLabelConverter;
use Combodo\iTop\Forms\Block\IO\Format\RawFormat;
/**
* Form block for choices.
@@ -15,10 +17,18 @@ use Combodo\iTop\Forms\Block\FormType\ChoiceFormType;
*/
class ChoiceFormBlock extends AbstractTypeFormBlock
{
// Outputs
public const OUTPUT_LABEL = 'label';
/** @inheritdoc */
public function GetFormType(): string
{
return ChoiceFormType::class;
}
public function InitOutputs(): void
{
parent::InitOutputs();
$this->AddOutput(self::OUTPUT_LABEL, RawFormat::class, new ChoiceValueToLabelConverter($this));
}
}

View File

@@ -11,6 +11,7 @@ use Combodo\iTop\Forms\Block\FormBlockException;
use Combodo\iTop\Forms\Block\IO\Converter\StringToAttributeConverter;
use Combodo\iTop\Forms\Block\IO\Format\AttributeIOFormat;
use Combodo\iTop\Forms\Block\IO\Format\ClassIOFormat;
use Combodo\iTop\Forms\Block\IO\Format\RawFormat;
use CoreException;
use MetaModel;
@@ -52,10 +53,17 @@ class AttributeChoiceFormBlock extends ChoiceFormBlock
/** @inheritdoc */
public function UpdateDynamicOptions(string $sEventType = null): void
{
$oValue = $this->GetInput(self::INPUT_CLASS_NAME)->GetValue($sEventType);
$oClass = $this->GetInput(self::INPUT_CLASS_NAME)->GetValue($sEventType);
$aAttributeCodes = MetaModel::GetAttributesList($oValue);
$this->aDynamicOptions['choices'] = array_combine($aAttributeCodes, $aAttributeCodes);
$aAttributeCodes = MetaModel::GetAttributesList($oClass);
$aAttributes = [];
foreach ($aAttributeCodes as $sAttributeCode){
$oAttribute = MetaModel::GetAttributeDef(strval($oClass), $sAttributeCode);
$aAttributes[$oAttribute->GetLabel()] = $sAttributeCode;
}
$this->aDynamicOptions['choices'] = $aAttributes;
}
}

View File

@@ -13,66 +13,47 @@ use Combodo\iTop\Forms\FormsException;
use IssueLog;
use Symfony\Component\Form\FormEvents;
/**
*
*/
class ExpressionFormBlock extends AbstractFormBlock
{
public const EXPRESSION_PATTERN = "/\[\[(?<input>[^\]]+)]]/";
// Outputs
const OUTPUT_RESULT = "result";
const OUTPUT_RESULT_INVERT = "result_invert";
public function InitBlockOptions(array &$aUserOptions): void
{
parent::InitBlockOptions($aUserOptions);
}
/** @inheritdoc */
public function InitOutputs(): void
{
parent::InitOutputs();
$this->AddOutput(self::OUTPUT_RESULT, BooleanIOFormat::class);
// $this->AddOutput(self::OUTPUT_RAW, RawFormat::class);
$this->AddOutput(self::OUTPUT_RESULT_INVERT, BooleanIOFormat::class);
}
// public function InputHasChanged()
// {
// if (!$this->IsInputsDataReady()) {
// return;
// }
// $sExpression = $this->GetOptions()['expression'];
// $sValue = preg_replace_callback(
// "/\[\[(?<input>[^\]]+)]]/",
// function(array $aMatches): string {
// return $this->GetInput($aMatches['input'])->GetValue();
// },
// $sExpression);
//
// foreach ($this->GetInputs() as $oFormInput) {
// IssueLog::Info($oFormInput->GetName().' = '.$oFormInput->GetValue());
// }
// IssueLog::Info("Result of [$sExpression] is [$sValue]");
//
// $result = '';
// eval('$result = '.$sValue.';');
// IssueLog::Info("Result of [$sExpression] is eval to [$result]");
//
// $this->GetOutput(self::OUTPUT_RESULT)->SetValue(FormEvents::POST_SUBMIT, new BooleanIOFormat($result));
// $this->GetOutput(self::OUTPUT_VALUE)->SetValue(FormEvents::POST_SUBMIT, new RawFormat($result));
// }
public function InputHasChanged()
/** @inheritdoc */
public function AllInputsReadyEvent(): void
{
if (!$this->IsInputsDataReady()) {
return;
}
$sExpression = $this->GetOptions()['expression'];
$this->Compute($sExpression, FormEvents::POST_SET_DATA);
$this->Compute($sExpression, FormEvents::POST_SUBMIT);
$this->ComputeExpression(FormEvents::POST_SET_DATA);
$this->ComputeExpression(FormEvents::POST_SUBMIT);
}
public function Compute(string $sExpression, string $sEventType): void
/**
* Compute the expression and set the output values.
*
* @param string $sEventType
*
* @return void
*/
public function ComputeExpression(string $sEventType): void
{
try{
$sExpression = $this->GetOptions()['expression'];
$sValue = preg_replace_callback(
"/\[\[(?<input>[^\]]+)]]/",
self::EXPRESSION_PATTERN,
function(array $aMatches) use ($sEventType): ?string {
$oInput = $this->GetInput($aMatches['input']);
if(!$oInput->HasEventValue($sEventType)){
@@ -86,6 +67,7 @@ class ExpressionFormBlock extends AbstractFormBlock
eval('$result = '.$sValue.';');
$this->GetOutput(self::OUTPUT_RESULT)->SetValue($sEventType, new BooleanIOFormat($result));
$this->GetOutput(self::OUTPUT_RESULT_INVERT)->SetValue($sEventType, new BooleanIOFormat(!$result));
$this->GetOutput(self::OUTPUT_VALUE)->SetValue($sEventType, new RawFormat($result));
}
catch(\Exception $e){

View File

@@ -33,13 +33,16 @@ class ChoiceFormType extends AbstractType
{
parent::configureOptions($resolver);
// options to control the inline display of choices
$resolver->setDefault('inline_display', true);
}
/** @inheritdoc */
public function buildView(FormView $view, FormInterface $form, array $options): void
{
parent::buildView($view, $form, $options);
// pass options to the view
$view->vars['inline_display'] = $options['inline_display'];
}
@@ -47,46 +50,51 @@ class ChoiceFormType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// on preset data
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) use ($options) {
if ($options['multiple'] === false && $options['required'] === true) {
if ($event->getData() === null) {
$FirstElement = array_shift($options['choices']);
if ($FirstElement !== null) {
$event->setData($FirstElement);
}
}
}
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $oEvent) use ($options) {
$this->InitializeValue($oEvent, $options);
});
// on pre submit
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) use ($options) {
if($options['multiple'] === false && $options['required'] === true) {
if ($event->getData() === null) {
$FirstElement = array_shift($options['choices']);
if($FirstElement !== null){
$event->setData($FirstElement);
}
}
}
});
// on pre submit
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) use ($options){
// on pre submit (prior)
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $oEvent) use ($options){
// reset value if not in available choices
if (!empty($event->getData()) && !$this->CheckValue($event->getData(), $options)) {
$event->getForm()->addError(new FormError("The value has been reset because it is not part of the available choices anymore."));
$event->setData(null);
if (!empty($oEvent->getData()) && !$this->CheckValue($oEvent->getData(), $options)) {
$oEvent->getForm()->addError(new FormError("The value has been reset because it is not part of the available choices anymore."));
$oEvent->setData(null);
}
}, 1);
}, 1); // priority 1 to be executed before the default validation (priority 0)
// on pre submit
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $oEvent) use ($options) {
$this->InitializeValue($oEvent, $options);
});
}
/**
* Initialize the value of the choice field.
*
* @param PreSetDataEvent|PreSubmitEvent $oEvent
* @param array $options
*
* @return void
*/
private function InitializeValue(PreSetDataEvent|PreSubmitEvent $oEvent, array $options): void
{
if ($options['multiple'] === false && $options['required'] === true) {
if ($oEvent->getData() === null) {
$oFirstElement = array_shift($options['choices']);
if ($oFirstElement !== null) {
$oEvent->setData($oFirstElement);
}
}
}
}
/**
* Check if the value(s) are part of the available choices.
*
* @param $oValue
* @param $options
*

View File

@@ -1,4 +1,8 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Forms\Block\FormType;
@@ -8,14 +12,18 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
/**
*
*/
class FormType extends AbstractType
{
/** @inheritdoc */
public function getParent(): string
{
return \Symfony\Component\Form\Extension\Core\Type\FormType::class;
}
/** @inheritdoc */
public function buildView(FormView $view, FormInterface $form, array $options)
{
parent::buildView($view, $form, $options);

View File

@@ -1,10 +1,17 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Forms\Block\FormType;
use Combodo\iTop\Forms\Block\Base\FormBlock;
use Symfony\Component\Form\FormInterface;
/**
*
*/
class FormTypeHelper
{
@@ -51,7 +58,7 @@ class FormTypeHelper
$aBlocksToRedraw[$sBlockTurboTriggerId] = $oFormTurboTrigger->createView();
// Add impacted blocks
$aImpacted = $oMap->GetImpacted($oBlockTurboTrigger->GetName());
$aImpacted = $oMap->GetBlocksImpactedBy($oBlockTurboTrigger->GetName());
foreach ($aImpacted as $oImpactedBlock) {
$sName = $sParentName.'_'.$oImpactedBlock->GetName();
if($oParent->has($oImpactedBlock->GetName())){

View File

@@ -0,0 +1,35 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Forms\Block\IO\Converter;
use Combodo\iTop\Forms\Block\Base\ChoiceFormBlock;
use Combodo\iTop\Forms\Block\IO\Format\RawFormat;
/**
*
*/
class ChoiceValueToLabelConverter extends AbstractConverter
{
private ChoiceFormBlock $oChoiceBlock;
public function __construct(ChoiceFormBlock $oChoiceBlock)
{
$this->oChoiceBlock = $oChoiceBlock;
}
/** @inheritdoc */
public function Convert(mixed $oData): ?RawFormat
{
if(is_null($oData) || is_array($oData)){
return null;
}
$aOptions = array_flip($this->oChoiceBlock->GetOptionsMergedWithDynamic()['choices']);
return new RawFormat($aOptions[$oData]);
}
}

View File

@@ -22,7 +22,6 @@ class FormBinding
{
$this->oDestinationIO = $oDestinationIO;
$this->oSourceIO = $oSourceIO;
}
/**
@@ -33,6 +32,6 @@ class FormBinding
public function PropagateValues(): void
{
$this->oDestinationIO->SetValues($this->oSourceIO->GetValues());
$this->oDestinationIO->GetOwnerBlock()->InputHasChanged();
$this->oDestinationIO->GetOwnerBlock()->BindingReceivedEvent($this->oDestinationIO);
}
}

View File

@@ -4,16 +4,16 @@ namespace Combodo\iTop\Forms\Block\IO\Format;
class RawFormat
{
public string $oValue;
public string $sValue;
public function __construct(string $oValue)
public function __construct(string $sValue)
{
$this->oValue = $oValue;
$this->sValue = $sValue;
// validation du format sinon exception
}
public function __toString(): string
{
return strval($this->oValue);
return strval($this->sValue);
}
}