Open Vinai opened 3 years ago
Every form needs a load and a save declaration.
Repository/Data model:
<load method="\Magento\Customer\Api\CustomerRepositoryInterface::getById"
type="\Magento\Customer\Api\Data\CustomerInterface">
<bindArguments>
<argument name="customer_id" requestParam="id"/>
</bindArguments>
</load>
<save method="\Magento\Customer\Api\CustomerRepositoryInterface::save">
<bindArguments>
<argument name="customer" formData="true"/>
<argument name="passwordHash" method="\My\Module\Model\CustomerPassword::hash"/>
</bindArguments>
</save>
ORM Model/Resource model:
<load method="\Magento\Cms\Model\ResourceModel\Block::load" entity="\Magento\Cms\Model\Block">
<bindArguments>
<argument name="value" requestParam="id"/>
</bindArguments>
</load>
<save method="\Magento\Cms\Model\ResourceModel\Block::save"/>
Not all of the elements and attributes above are required. If the entity type can be determined based on the load method return type, it does not have to be declared in the XML.
When saving a form, the submitted data will be used to rehydrate an instance of form entity type, which will then be passed to the save method as the formData
argument if specified, or the first argument otherwise.
It is possible to configure additional argument bindings on the save and load methods.
By default, all fields with a getter and setter pair or, for EAV entities, all attributes, will be rendered as input fields. Any fields that have objects as values will be excluded.
This default behavior can then be customized in similar way to grid columns.
<fields>
<include keepAllSourceFields="true">
<field name="identifier"/>
<field name="title" template="My_Module::form/title-field.phtml" joinColumns="true"/>
<field name="content" type="wysiwyg"/>
<field name="creation_time" type="datetime"/>
<field name="is_active" type="boolean" sortOrder="10"/>
<field name="comment" enabled="false"/>
<field name="store_ids" type="select" source="\Magento\Eav\Model\Entity\Attribute\Source\Store"/>
<field name="admin" valueProcessor="\My\Module\Form\AdminLinkProcessor"/>
</include>
<exclude>
<field name="updated_at"/>
</exclude>
</fields>
The type determination will probably not be very reliable. Text input fields will be used as a lowest common denominator if no more specific input type can be automatically determined.
Non-scalar values can be handled with a valueProcessor
, which can prepare a field value for rendering in a form field, and also can be used to take a submitted value and prepare it for saving (for example, serializing and rehydrating objects).
there's also use-case for hidden fields, field comments, field grouping , separators , labels , radio-buttons , submit button labels , submit button actions , ease of providing css, js inclusion paths per form
@speedupmate can you give a little more info on what you would like to have for "js inclusion paths per form"?
The sections in magento2 admin are under single layout handler, I'm not sure how you are approaching this but giving each form or section its in a separate layout handler would allow those forms to be targeted with css and js bit more elegantly
@speedupmate Thanks for clarifying. Hyva Admin forms will be loaded using layout XML (or as a block in a htmlContent
uicomponent) which means they "have" specific layout handles already. They are different from the system.xml forms.
Currently I have the following thoughts on allowing JS integration.
A form can have a x-init
and a x-data
attributes or child nodes. The content would be placed in the matching attributes on a <div>
wrapping the form.
If a JS function is referenced in either of the attributes, it can be either specified inline in a separate .phtml template loaded on the page with layout XML, or it could be loaded from a .js file that is loaded in the page with a <script>
tag.
I'm not sure how to make this properly extensible, or make this work with nested forms (another thing I would like to make possible).
Probably most use cases could be covered with this kind of simple initialization though, so maybe it is good enough.
Forms also could have event bindings like obsubmit="..."
.
I would like to add event specific attributes, for example onclick
, ònchange
or onkeydown
.
These would be rendered in the matching attributes on the form fields.
For example: onchange="$dispatch('custom-event', { foo: 'bar' })"
. This would allow intuitive integration with the alpinejs view model.
It would be nice to have a generic event binding attribute. Something liek on="click: open = ! open
. (In this example the event and the expression are separated by a colon.)
The above would make it simple and quick to enrich a form with some JS. The downside is that would probably lead to conflicting customizations sooner or later. Thanks to JS dynamic nature it would be possible to resolve these, but still - some pub/sub based system where customizations can be registered without overwriting each other would be more compatible. That said, I do not want to make this a JavaScript framework :)
Any thoughts and insights are very welcome!
Data needs to flow from the server into the rendered form, and then back again to the server.
The shape of this data is determined by the form load
and save
elements, following a fallback approach.
Steps to determine the data shape for rendering the form:
If an entity
attribute is specified on the load
attribute, the available fields and their properties are determined via reflection. It is possible to use the generic data structures array
or \Magento\Framework\DataObject
. In these two cases no fields can be extracted from the entity, and all form fields will need to be declared in the XML.
If no entity
attribute is specified on the load
element, reflection on the load
method will be used to determine the entity type. Note: maybe array-of-objects type in the form of Some\Class[]
could be implemented to render as a list of forms (use case example: configuring slider content (adding a new slide to existing ones). OTOH, maybe overkill.).
If no entity type can be determined due to lack of type information on the method
and no entity
attribute is present either, all fields will have to be declared in the forms XML. In this case the load type array
will be used as a default.
The information from the fields declared in the forms XML will be merged with the fields determined through reflection in the steps 1 and 2 above.
An event will be dispatched with the form field definitions to allow further conditional customization.
Then the form will be rendered according to the configuration.
Note: different from Hyvä grids, no type information is taken form the loaded data to build the form, only type reflection on the load method and the entity (if present) will be used together with the configured fields. This is because forms also have to work if no data is present.
When a form is submitted, all the data is sent to a generic controller and serialized into a PHP array. Hooks and events will be available to provide validation, default values and custom deserialization.
The next step is to determine the target type. The following steps are used to determine the type:
entity
attribute is present on the save
node, it will be usedsave
method.entity
type is present on the load
element, it will be used.load
method return type to determine the target type.array
, the PHP array is usedThe target type is instantiated and populated with the submitted array
The save method is called.
Specific exceptions are used to communicate validation errors.
JS integration
I'm not so experienced with alpinejs jet but I think it is enough to leave your objects as x-data="{}" and provide all bindings to events in js implementation , you know like oldschool code and template separation. Otherwise its just like knockoutjs experience with bindings just all over the templates and a nightmare to debug
Since you are defining forms the onsubmit and other event handlers you plan could be implemented/suggested as interface for js implementation to follow (simply put, to implement it you need certain set of methods) ?
And def require js loaded from external file then it can be more easily debugged with developer tools. Adding to templates is easy and all but m2 is still a large project and diff teams need to provide diff features will end up with mixed practices and at some point the experience is not so cool any more.
Currently I think I'll use a similar strategy as is used in the system configuration.
In there we have Tabs, Sections and Groups.
However, for Hyva_Admin forms I think it will be better to limit this to two organizing levels: Sections and collapsable Groups.
Fist draft of how it could work:
<sections>
<section id="foo" label="Foos" sortOrder="10">
<group id="important-things" sortOrder="10"/>
<group id="details" sortOrder="20" label="Details"/>
</section>
<section id="bar" label="Bars" sortOrder="20">
<group id="whatever" sortOrder="10"/>
</section>
</sections>
<fields>
<include keepAllSourceFields="true">
<field name="identifier" group="important-things"/>
<field name="title" template="My_Module::form/title-field.phtml" group="important-things"/>
<field name="content" type="wysiwyg" group="details"/>
<field name="creation_time" type="datetime" group="whatever"/>
<field name="is_active" type="boolean" group="non-existent"/>
<field name="comment" enabled="false"/>
</include>
</fields>
For EAV entities, fields can be automatically assigned to groups based on the eav_entity_attribute
table.
The following scenarios have to be handled:
Given the above example, the fields would end up in the following hierarchy:
- section: foo
- group: important-things
- field: identifier
- field: title
- group: details
- field: content
- section: bar
- group: whatever
- field: creation_time
- section: other
- group: non-existent
- field: is_active
- group: other
- field: comment
EDIT: replaced tab
with section
because of the new idea to add generic tabs support to Hyva_Admin.
EDIT 2: Just realized that group IDs will have to be unique after merging so fields can be associated based on the group ID. To document the issue, take this config:
<sections>
<section id="foo">
<group id="group1"/>
</section>
<section id="bar">
<group id="group1"/>
</section>
</sections>
<fields>
<include>
<field name="field1" group="group1"/>
</include>
</fields>
Should this field be associated with foo/group1
or bar/group1
?
There are three different semi-sensible ways to respond to this problem:
sectionId/groupId
, but that introduces another set of issues on how to deal with inconsistencies I would like to avoid.I think the only sane way to handle this is to throw an exception. Assigning fields to the first or last group with a matching ID would cause surprising and unwanted behavior, that is, fields suddenly moving to another section when a new group is declared.
My current plan is to allow adding events to fields:
<field name="title">
<event on="change"/>
<event on="keydown"/>
</field>
The event name would be built from using the schema hyva_form_[form-name]_[field_name]_[event_name]
.
For example hyva_form_foo_title_change
.
The event name is not customizable so there is no chance of accidentally disabling event observers of other modules.
Subscribers receive an object containing the field element and the alpinejs view model along the lines of:
this.$dispatch(event, {field: e.target, viewModel: this});
Forms always dispatches a hyva_form_[form-name]_submit
event, it doesn't have to be configured.
Getting the subscribers onto the page is not the responsibility of the form. They can be added using layout XML. If needed this can be extended in future to allow adding JS code to the page directly from the form XML, but initially I think it would be good to keep that out of scope.
By default a form will have a single "Save" button, which posts the form to the build in save controller action. This button can be disabled in the configuration, and additional buttons can be added, too.
First draft:
<navigation>
<buttons>
<button id="save" label="Save" url="hyva_admin/form/save" enabled="false" />
<button id="only-visible-when-entity-was-loaded" label="Example" hiddenForNewEntity="true"/>
<button id="reset" label="Reset" url="*/*/*"/>
<button id="validate">
<event on="click"/>
</button>
</buttons>
</navigation>
(The JS integration would be whatever comes out of the discussion and prototyping related to issue #35.)
If the label is omitted, the id
attribute is used to generate a label.
If a url
is omitted, no link will be rendered an any action would have to be preformed via JavaScript.
Any comments and thoughts are welcome as always.
One of the most tricky things to model in regards to the forms are entity relations. For example, a way to select the simple products associated with a parent composite product type.
Relations could be handled through embedded grids, multiselects, and so on. I don't think there is a one-size-fits-all solution. Over time I hope that Hyva_Admin forms will offer a couple of options to model relations in a form.
However, in the first prototype release, I will intentionally not implement support for relations. There will be support for select, multiselect, checkbox and radio fields. The options for each of these will be provided by standard Magento source models.
Once the forms architecture settles a bit, then it will be time to revisit entity relation support.
it's been a while, any updates on the progress?
Hey @rbouma, thanks for asking! Actually, after a long hiatus where I didn't need to create any backend forms, I now have a need again so I started working on the forms again. I can't give an ETA. I'm still in the "getting back into the code" phase.
This is a high level issue for documenting design decisions in regards to form support in Hyva_Admin.
Design goals:
Hopefully some of the code and thought that went into the grid design can be reused for forms.