poc form SDK (extends to form)

This commit is contained in:
Benjamin Dalsass
2023-09-01 14:32:42 +02:00
parent 20ae64706a
commit 664aa3949d
9 changed files with 155 additions and 68 deletions

View File

@@ -130,7 +130,9 @@ const Collection = function(oForm, objectFormUrl){
.then((data) => {
const oModalBody = $('#object_modal .modal-body');
oModalBody.html(data.template);
oForm.handleElement(oModalBody[0]);
oModalBody[0].querySelectorAll('form').forEach((formEl) => {
oForm.handleElement(formEl);
});
})
.catch(function (error) {
console.error(error);

View File

@@ -52,62 +52,96 @@ const Form = function(oWidget){
}
/**
* changeOptions.
* updateField.
*
* @param oEvent
* @param sId
* @param oForm
* @param oElement
* @param aDependentAttCodes
* @returns {Promise<void>}
*/
async function changeOptions(oEvent, sId){
async function updateField(oEvent, oForm, oElement, aDependentAttCodes){
// retrieve field that's need to be updated
const oDependentField = document.getElementById(sId);
const sName = oDependentField.getAttribute('name');
const sAttCode = oDependentField.getAttribute('data-att-code');
const aDependenciesAttCodes = [];
// retrieve parent form
const oForm = oDependentField.closest('form');
/////////////////////////////////
// I - CONSTRUCT DEPENDENCIES (The original event target but also other required dependencies)
// retrieve field container
const oContainer = oDependentField.closest(aSelectors.dataBlockContainer);
// the value of the field change, sAttCodes needs to be updated
aDependentAttCodes.forEach((sAttCode) => {
// set field container loading state
oContainer.classList.add('loading');
// field to update
const oDependsOnElement = oElement.querySelector(String.format(aSelectors.dataAttCode, sAttCode));
// prepare request body
const oFormData = new FormData(oForm);
// retrieve field container
const oContainer = oDependsOnElement.closest(aSelectors.dataBlockContainer);
// set field container loading state
oContainer.classList.add('loading');
// retrieve dependency data
const sDependsOn = oDependsOnElement.dataset.dependsOn;
// may have multiple dependencies
let aDependsEls = sDependsOn.split(DEPENDS_ON_SEPARATOR);
aDependsEls.forEach((sAtt) => {
if(!aDependenciesAttCodes.includes(sAtt)){
aDependenciesAttCodes.push(sAtt);
}
});
});
/////////////////////////////////
// II - PREPARE RELOAD REQUEST
// prepare quest data
let sRequestBody = '';
function encode(s){ return encodeURIComponent(s).replace(/%20/g,'+'); }
for(let pair of oFormData.entries()){
if(typeof pair[1]=='string'){
sRequestBody += (sRequestBody?'&':'') + encode(pair[0])+'='+encode(pair[1]);
let $bFirst = true;
// iterate throw dependencies...
aDependenciesAttCodes.forEach(function(sAtt) {
const oDependsOnElement = oElement.querySelector(String.format(aSelectors.dataAttCode, sAtt));
if(!$bFirst){
sRequestBody += '&';
}
}
sRequestBody += '&att_code=' + oDependentField.dataset.attCode;
sRequestBody += '&dependency_att_code=' + oEvent.target.dataset.attCode;
sRequestBody += 'partial_object['+oDependsOnElement.dataset.attCode + ']=' + oDependsOnElement.value;
$bFirst = false;
});
sRequestBody += '&att_codes=' + aDependentAttCodes.join(',');
sRequestBody += '&dependency_att_codes=' + aDependenciesAttCodes.join(',');
/////////////////////////////////
// III - UPDATE THE FORM
// update fom
const sUpdateFormResponse = await updateForm(sRequestBody, oForm.dataset.reloadUrl, oForm.getAttribute('method'));
const oHtml = oToolkit.parseTextToHtml(sUpdateFormResponse);
let oSingle = oHtml.getElementById('object_single_attribute');
oContainer.replaceWith(oSingle.firstChild);
// remove loading state
oContainer.classList.remove('loading');
const sReloadResponse = await updateForm(sRequestBody, oForm.dataset.reloadUrl, oForm.getAttribute('method'));
// update new dependent field
const oNewDependentField = document.querySelector(`[id$="${sAttCode}"]`);
oNewDependentField.setAttribute('name', sName);
oNewDependentField.setAttribute('id', sId);
oNewDependentField.setAttribute('data-att-code', sAttCode);
const oReloadedElement = oToolkit.parseTextToHtml(sReloadResponse);
// init dynamics
initDependencies(oContainer);
initDynamicsInvisible(oContainer);
initDynamicsDisable(oContainer);
let oPartial = oReloadedElement.getElementById('partial_object');
aDependentAttCodes.forEach((sAtt) => {
// dependent element
const oDependentElement = oElement.querySelector(String.format(aSelectors.dataAttCode, sAtt));
const oContainer = oDependentElement.closest(aSelectors.dataBlockContainer);
const sId = oDependentElement.getAttribute('id');
const sName = oDependentElement.getAttribute('name');
// new element
const oNewElement = oPartial.querySelector(String.format(aSelectors.dataAttCode, sAtt));
const oNewContainer = oNewElement.closest(aSelectors.dataBlockContainer);
oNewElement.setAttribute('id', sId);
oNewElement.setAttribute('name', sName);
// replace element
oContainer.replaceWith(oNewContainer);
});
// init widgets
oWidget.handleElement(oContainer);
}
/**
@@ -117,7 +151,13 @@ const Form = function(oWidget){
*/
function initDependencies(oElement){
// get all dependent fields
// retrieve parent form
const oForm = oElement.closest('form');
// compute dependencies map
let aMapDependencies = {};
// get all field with dependencies
const aDependentsFields = oElement.querySelectorAll(aSelectors.dataDependsOn);
// iterate throw dependent fields...
@@ -129,18 +169,30 @@ const Form = function(oWidget){
// may have multiple dependencies
let aDependsEls = sDependsOn.split(DEPENDS_ON_SEPARATOR);
// iterate throw dependencies...
// iterate throw the dependencies...
aDependsEls.forEach(function(sEl){
// retrieve dependency
const oDependsOnElement = oElement.querySelector(`[id$="${sEl}"]`);
// listen for changes
if(oDependsOnElement != null){
oDependsOnElement.addEventListener('change', (event) => changeOptions(event, oDependentField.id));
// add dependency att code to map
if(!(sEl in aMapDependencies)){
aMapDependencies[sEl] = [];
}
aMapDependencies[sEl].push(oDependentField.dataset.attCode);
});
});
// iterate throw dependencies map...
for(let sAttCode in aMapDependencies){
// retrieve corresponding field
const oDependsOnElement = oElement.querySelector(String.format(aSelectors.dataAttCode, sAttCode));
// listen changes
if(oDependsOnElement !== null){
oDependsOnElement.addEventListener('change', (event) => updateField(event, oForm, oElement, aMapDependencies[sAttCode]));
}
}
}
/**

View File

@@ -3,7 +3,7 @@
namespace Combodo\iTop\DI\Controller;
use Combodo\iTop\DI\Form\Type\Compound\ConfigurationType;
use Combodo\iTop\DI\Form\Type\Compound\ObjectSingleAttributeType;
use Combodo\iTop\DI\Form\Type\Compound\PartialObjectType;
use Combodo\iTop\DI\Form\Type\Compound\ObjectType;
use Combodo\iTop\DI\Services\ObjectService;
use Exception;
@@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Stopwatch\Stopwatch;
class Controller extends AbstractController
{
@@ -209,8 +210,9 @@ class Controller extends AbstractController
}
// create form with request data (dependent field)
$oForm = $this->createForm(ObjectType::class, $oObject, [
$oForm = $this->createForm(PartialObjectType::class, $oObject, [
'object_class' => $class,
'att_codes' => explode(',', $request->get('dependency_att_codes'))
]);
// handle form data
@@ -220,9 +222,9 @@ class Controller extends AbstractController
$oObject->ComputeValues();
// create a new form for affected field with updated (but not persist) data
$oForm = $this->createForm(ObjectSingleAttributeType::class, $oObject, [
$oForm = $this->createForm(PartialObjectType::class, $oObject, [
'object_class' => $class,
'att_code' => $request->get('att_code')
'att_codes' => explode(',', $request->get('att_codes'))
]);
// return object form

View File

@@ -103,6 +103,7 @@ class AttributeBuilder
$aFormType['options']['allow_target_creation'] = $oAttributeDefinition->AllowTargetCreation();
$aFormType['options']['object_class'] = $oAttributeDefinition->GetTargetClass();
$aFormType['options']['att_code'] = $oAttributeDefinition->GetCode();
$aFormType['options']['is_locked'] = $bIsLocked;
try{
$oObjectsSet = MetaModel::GetAllowedValuesAsObjectSet($oAttributeDefinition->GetHostClass(), $oAttributeDefinition->GetCode(), []);
$aFormType['options']['choices'] = $this->oObjectService->ToChoices($oObjectsSet);
@@ -155,7 +156,7 @@ class AttributeBuilder
$aFormType['options']['is_abstract'] = MetaModel::IsAbstract(LinkSetModel::GetTargetClass($oAttributeDefinition));
$aFormType['options']['target_class'] = LinkSetModel::GetTargetClass($oAttributeDefinition);
if($aFormType['options']['is_abstract']){
$aFormType['options']['object_classes'] = MetaModel::EnumChildClasses(LinkSetModel::GetTargetClass($oAttributeDefinition));
$aFormType['options']['object_classes'] = $this->oObjectService->listConcreteChildClasses(LinkSetModel::GetTargetClass($oAttributeDefinition));
}
$aFormType['options']['entry_options'] = [
'object_class' => $oAttributeDefinition->GetLinkedClass(),

View File

@@ -35,6 +35,7 @@ class ExternalKeyType extends AbstractType implements IFormTypeOptionModifier
'allow_target_creation' => false,
'object_class' => null,
'att_code' => null,
'is_locked' => false,
]);
}
@@ -54,9 +55,16 @@ class ExternalKeyType extends AbstractType implements IFormTypeOptionModifier
public function getNewOptions(array $aInitialOptions, DBObject $oObject) : array
{
try{
// $iVal = $oObject->Get($aInitialOptions['att_code']); // because we can't list all items du to performance, we want to force current value to be present, even if it's not part of the result
$oObjectsSet = MetaModel::GetAllowedValuesAsObjectSet(get_class($oObject), $aInitialOptions['att_code'], ['this' => $oObject]/*, null, $iVal*/);
$iVal = $oObject->Get($aInitialOptions['att_code']); // because we can't list all items du to performance, we want to force current value to be present, even if it's not part of the result
$oObjectsSet = MetaModel::GetAllowedValuesAsObjectSet(get_class($oObject), $aInitialOptions['att_code'], ['this' => $oObject]);
$aInitialOptions['choices'] = $this->oObjectService->ToChoices($oObjectsSet);
if($aInitialOptions['is_locked']){
$aInitialOptions['choices']['Object in creation'] = 0;
}
// !!! ensure current value is part of result
$object = MetaModel::GetObject($aInitialOptions['object_class'], $iVal);
$aInitialOptions['choices'][$object->GetName()] = $iVal;
}
catch(Exception $e){

View File

@@ -12,7 +12,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
* Used to handle depends on fields.
*
*/
class ObjectSingleAttributeType extends AbstractType
class PartialObjectType extends AbstractType
{
/** @var ObjectFormListener object form modifier */
private ObjectFormListener $oObjectFormModifier;
@@ -37,7 +37,7 @@ class ObjectSingleAttributeType extends AbstractType
{
$resolver->setDefaults( [
'object_class' => null,
'att_code' => null
'att_codes' => null
]);
}
@@ -46,11 +46,15 @@ class ObjectSingleAttributeType extends AbstractType
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// build form from options
$sFormType = $this->oAttributeBuilder->createAttribute($options['object_class'], $options['att_code']);
// add requested attributes form types...
foreach ($options['att_codes'] as $sAttCode){
// add form field
$builder->add($options['att_code'], $sFormType['type'], $sFormType['options']);
// create attribute type
$sFormType = $this->oAttributeBuilder->createAttribute($options['object_class'], $sAttCode, null);
// build form type
$builder->add($sAttCode, $sFormType['type'], $sFormType['options']);
}
// dynamic form handling
$builder->addEventSubscriber($this->oObjectFormModifier);

View File

@@ -9,6 +9,7 @@ use DBObject;
use DBObjectSet;
use MetaModel;
use ormLinkSet;
use Symfony\Component\Stopwatch\Stopwatch;
class ObjectService
{
@@ -18,16 +19,20 @@ class ObjectService
/** @var string database name */
private string $sDbName;
/** @var \Symfony\Component\Stopwatch\Stopwatch */
private Stopwatch $oStopWatch;
/**
* Constructor.
*
* @param $sDbHost
* @param $sDbName
*/
public function __construct($sDbHost, $sDbName)
public function __construct($sDbHost, $sDbName, Stopwatch $oStopWatch)
{
$this->sDbHost = $sDbHost;
$this->sDbName = $sDbName;
$this->oStopWatch = $oStopWatch;
}
/**
@@ -61,6 +66,8 @@ class ObjectService
*/
public function ToChoices(DBObjectSet $oObjectsSet) : array
{
$this->oStopWatch->start('ToChoices');
$aChoices = [];
// Retrieve friendly name complementary specification
@@ -75,11 +82,13 @@ class ObjectService
$oObjectsSet->OptimizeColumnLoad([$oObjectsSet->GetClassAlias() => $aDefaultFieldsToLoad]);
$i = 0;
while ($i < 30 && $oObj = $oObjectsSet->Fetch()) {
while ($i < 10 && $oObj = $oObjectsSet->Fetch()) {
$aChoices[$oObj->GetName()] = $oObj->GetKey();
$i++;
}
$this->oStopWatch->stop('ToChoices');
return $aChoices;
}
@@ -143,4 +152,13 @@ class ObjectService
}
}
}
public function listConcreteChildClasses(string $sObjectClass)
{
$aChildClasses = MetaModel::EnumChildClasses($sObjectClass);
return array_filter($aChildClasses, function ($sChildClass){
return !MetaModel::IsAbstract($sChildClass);
});
}
}

View File

@@ -108,8 +108,6 @@
</table>
{% set buttonDataAttributes = 'data-object-class="' ~ object_class ~ '" data-ext-key-to-me="' ~ ext_key_to_me ~ '"' %}
{% if is_abstract %}
{# create item button #}
<div class="d-inline-block">
@@ -119,7 +117,7 @@
</button>
<ul class="dropdown-menu dropdown_scroll_300">
{% for object_class in object_classes %}
<li><a class="dropdown-item create_item_link" href="#" {{ buttonDataAttributes|raw }}>{{ object_class }}</a></li>
<li><a class="dropdown-item create_item_link" href="#" data-object-class="{{ object_class }}" data-ext-key-to-me="{{ ext_key_to_me }}">{{ object_class }}</a></li>
{% endfor %}
</ul>
</div>
@@ -129,7 +127,7 @@
<button type="button" class="add_item_link btn btn-secondary btn-sm" data-collection-holder-class="{{ form.vars.id }}">Add {{ target_class }}</button>
{% else %}
{# create item button #}
<button type="button" class="btn btn-secondary create_item_link btn-sm" {{ buttonDataAttributes|raw }}>Create {{ target_class }}</button>
<button type="button" class="btn btn-secondary create_item_link btn-sm" data-object-class="{{ target_class }}" data-ext-key-to-me="{{ ext_key_to_me }}">Create {{ target_class }}</button>
{% endif %}
</div>

View File

@@ -46,7 +46,9 @@
{# Form #}
const oForm = new Form(oWidget);
oForm.handleElement(document);
document.querySelectorAll('form').forEach((formEl) => {
oForm.handleElement(formEl);
});
{# Collection #}
const oCollection = new Collection(oForm, '{{ path('object_form', {'class': 'object_class', 'id' : 0, 'name': 'form_name'}) }}');