Light API 0.4.0 is a public beta: a minor release such as 0.5.0 may still break. What that means for your module
Light
Developer guideRecipes

Fields on a Light editor

A card with your fields on a Light editor, checked on the server and in the browser.

A module adds fields to a Light editor with a card: the product, category, CMS page, CMS block, discount, new-customer and customer contact editors. The card's values load with the entity, are checked before the save, go onto the entity, and save inside the editor's transaction. JS hooks check and extend the save in the browser.

When to use it

  • Your module stores a property of the entity that the merchant sets while editing it, such as a product's delivery note.
  • Keep a card to four fields or fewer, and put it after the core content; more belongs in the advanced view (Built for Light, rule 2).

The forms

Form codeEntity interfaceContainers
productMagento\Catalog\Api\Data\ProductInterfacemrx.product.form.main.after_pricing, mrx.product.form.main.bottom, mrx.product.form.aside.bottom
categoryMagento\Catalog\Api\Data\CategoryInterfacemrx.category.form.main.bottom, mrx.category.form.aside.bottom
cms_pageMagento\Cms\Api\Data\PageInterfacemrx.cms_page.form.main.bottom, mrx.cms_page.form.aside.bottom
cms_blockMagento\Cms\Api\Data\BlockInterfacemrx.cms_block.form.main.bottom
discountMagento\SalesRule\Api\Data\RuleInterfacemrx.discount.form.main.bottom, mrx.discount.form.aside.bottom
customer_newMagento\Customer\Api\Data\CustomerInterfacemrx.customer.new.main.bottom
customerMagento\Customer\Api\Data\CustomerInterfacemrx.customer.contact.form.bottom

The generated reference lists the same with their versions. Order views have no form pipeline: a card on an order has its own controller (Order and customer pages).

Steps

1. The extension class

One class implements the form interfaces:

  • FormExtensionInterface: load() gives the card its values, validate() returns messages per field, save() writes your own tables inside the transaction and returns what the page gets back as ext.<code>.
  • DeclaresFieldsInterface: getFields() declares the fields, so ExtensionCard draws them without a template.
  • AppliesToEntityInterface: apply() sets the values on the entity before the editor saves it.
app/code/Mrx/Light/Api/Form/AppliesToEntityInterface.php
public function apply(FormContextInterface $context, array $input, object $entity): void;

FormContextInterface tells your class which form, entity and store view it works on, and CurrentFormInterface::get() gives the same to any block on the page.

The golden example's product field is an admin-only product attribute, set in apply(); save() returns null because the product's own save stores it:

docs/magerex-light/examples/Acme_LightExample/Model/Form/NoteField.php
public function save(FormContextInterface $context, array $input): ?array

The fields getFields() returns:

Prop

Type

2. Register it

The pool form_extensions is keyed by form code, then by your extension code, with the class and the ACL resource that may see and save the card:

app/code/Acme/LightProof/etc/adminhtml/di.xml
<item name="acme_proof" xsi:type="array">
    <item name="extension" xsi:type="object">Acme\LightProof\Model\ProofField\Proxy</item>
    <item name="resource" xsi:type="string">Acme_LightProof::manage</item>
</item>

3. The card

Mrx\Light\Block\ExtensionCard in one of the form's containers draws the declared fields. The card renders inside the editor's <form>: a card of your own never prints a <form>, and gives every button type="button", or a click saves the whole editor. Its inputs are named ext[<code>][<field>], so the save bar tracks them and the editor posts them to your class:

app/code/Acme/LightProof/view/adminhtml/layout/light_products_form.xml
<referenceContainer name="mrx.product.form.main.after_pricing">

4. JS hooks

The card's js argument loads a RequireJS module that registers hooks for the form and your code with Mrx_Light/js/form-hooks: validate checks in the browser before anything is posted, payload adds values, saved runs after the save. Acme_LightProof refuses the value "jsfail" in the browser, before any request, and adds js to every save:

app/code/Acme/LightProof/view/adminhtml/web/js/proof-hooks.js
hooks.register('product', 'acme_proof', {

After a save the form fires mrx:form-saved with formCode, entityId and response (Events, JS modules and #mrx-config keys).

How a save runs

The browser checks first, then the server runs every extension of the form inside the editor's database transaction. This is the product editor; the other six forms run the same steps:

Where validate() runs differs per form: the product save runs it inside the transaction, the category and discount editors in their controller before the save, and the CMS and customer editors in their service before the transaction. A message from validate() always stops the save.

What the admin sees

The product editor with the proof module's card after the pricing

  • Simple mode. The card on the editor, after the core content. A message from validate() shows under its field, next to the editor's own messages, and nothing is saved.
  • Advanced mode. The stock form, without the card. A value set through apply() is an attribute, so the stock form, REST, import and "Duplicate" see it.

ACL

The card, its values and its save need the item's resource. A posted ext[<code>] without it is ignored and logged.

Check it

  • bin/magento mrx:light:doctor --module=<your module>: unknown_reference with kind form catches a misspelled form code, unknown_container a wrong container.
  • ?mrx_slots=1 in developer mode outlines the containers on the page.
  • tests/playwright/light-proof.spec.ts test 4 is the test to copy.

Pitfalls

  • Where the data lives. A value of the entity is an attribute set in apply(), so REST, import, "Duplicate" and the stock form see it. An own table is only for data that isn't a property of the entity, with a foreign key onDelete CASCADE and its own service. A storefront-visible or filterable user-defined attribute already shows in the Details card. The pipeline runs only in the Light editors, so a rule that must always hold belongs in your service or the attribute's backend model.
  • The module's own product-form logic (ui_component modifiers, plugins on the stock Product\Save controller) doesn't run on a Light save.
  • save() only writes the database. Emails and calls to other systems belong in an observer on <entity>_save_commit_after, which runs only after the commit and is dropped on a rollback.
  • Set variant values in the advanced view. An AdvancedHint in mrx.product.form.main.after_pricing can say so.
  • When a field becomes required later, fill it for existing entities in a data patch.

Last updated on

On this page