From redaxo-mform
Guides MForm advanced widgets for REDAXO modules/YForm: custom links (internal/external/media/mailto/tel/YForm tables), media pickers, imagelists/linklists/medialists, color swatches with data-* attributes.
npx claudepluginhub friendsofredaxo/claude-marketplace --plugin redaxo-mformThis skill uses the workspace's default tool permissions.
MForm provides advanced picker widgets beyond standard HTML inputs. All work inside modules and inside the Flex Repeater.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Processes PDFs: extracts text/tables/images, merges/splits/rotates pages, adds watermarks, creates/fills forms, encrypts/decrypts, OCRs scans. Activates on PDF mentions or output requests.
Share bugs, ideas, or general feedback.
MForm provides advanced picker widgets beyond standard HTML inputs. All work inside modules and inside the Flex Repeater.
All examples assume
use FriendsOfRedaxo\MForm;at the top of the file and$mform = MForm::factory();already called.
addCustomLinkField)A unified link picker supporting internal articles, external URLs, media files, mailto and tel links.
$mform->addCustomLinkField(1, [
'label' => 'Link',
'data-intern' => 'enable', // show "internal article" tab
'data-extern' => 'enable', // show "external URL" tab
'data-media' => 'enable', // show "media" tab
'data-mailto' => 'enable', // show "e-mail" tab
'data-tel' => 'enable', // show "telephone" tab
'data-extern-link-prefix' => 'https://',
'data-link-category' => 14, // open link chooser at this category
'data-media-category' => 1, // open media pool at this category
'data-media-type' => 'jpg,png,gif', // restrict media types
]);
Link to a row in any YForm-managed table:
$ylink = [['name' => 'Länder', 'table' => 'rex_ycountries', 'column' => 'de_de']];
$mform->addCustomLinkField(1, ['label' => 'Land', 'ylink' => $ylink, 'data-intern' => 'disable']);
The widget stores a single string:
redaxo://12filename.jpghttps://example.commailto:user@example.comtel:+4912345rex-tablename://42Reading in OUTPUT PHP:
use FriendsOfRedaxo\MForm\Utils\MFormOutputHelper;
$link = 'REX_VALUE[1]';
$url = MFormOutputHelper::getCustomUrl($link);
$data = MFormOutputHelper::prepareCustomLink(['link' => $link], true);
// $data['customlink_url'] → resolved URL (same as $url)
// $data['customlink_text'] → display text (article name / media title / URL)
// $data['customlink_target'] → ' target="_blank" rel="noopener noreferrer"' for external, else ''
if ($url) {
echo '<a href="' . rex_escape($url) . '"' . $data['customlink_target'] . '>'
. rex_escape($data['customlink_text']) . '</a>';
}
See the mform-output skill for the full
MFormOutputHelperAPI includingcreateLinkData(),normalizeLinkData(), andnormalizeRepeaterItems().
addCustomLinkMultipleField)Multiple links as a JSON array in one value slot.
$mform->addCustomLinkMultipleField(1, [
'label' => 'Links',
'btn_add' => 'Link hinzufügen',
'data-intern' => 'enable',
'data-extern' => 'enable',
'data-media' => 'enable',
]);
Reading in OUTPUT PHP:
Der Wert aus REX_VALUE enthält HTML-Entities – deshalb html_entity_decode() vor json_decode():
use FriendsOfRedaxo\MForm\Utils\MFormOutputHelper;
$raw = html_entity_decode('REX_VALUE[1]', ENT_QUOTES | ENT_HTML5, 'UTF-8');
$links = json_decode($raw, true) ?? [];
foreach ($links as $linkStr) {
$url = MFormOutputHelper::getCustomUrl($linkStr);
$data = MFormOutputHelper::prepareCustomLink(['link' => $linkStr], true);
echo '<a href="' . rex_escape($url) . '"' . $data['customlink_target'] . '>'
. rex_escape($data['customlink_text']) . '</a>';
}
When reading from a YForm dataset (
$dataset->getValue('links')), skiphtml_entity_decode()– the value is stored without entities. See the mform-yform skill.
addMFormLinkField)Internal-article-only wrapper around Custom-Link. data-* link-type flags go in $parameter (2nd); label goes in $attributes (4th).
$mform->addMFormLinkField(1, ['data-extern' => 'disable', 'data-media' => 'disable'], 5, ['label' => 'Artikel']);
addMediaField / addMFormMediaField)Single file from the media pool.
// All widget options (label, types, preview, category) go in the 2nd param ($parameter)
$mform->addMediaField(1, ['label' => 'Bild', 'preview' => 1]);
// With type restriction and category
$mform->addMediaField(2, ['label' => 'Grafik', 'preview' => 1, 'types' => 'jpg,png,gif,svg,webp', 'category' => 3]);
// MForm variant (mform-media, stores filename in REX_VALUE; label in $attributes, data-* in $parameter)
$mform->addMFormMediaField(1, ['types' => 'jpg,png,webp', 'preview' => '1'], null, ['label' => 'Datei']);
Reading in OUTPUT PHP:
$filename = 'REX_VALUE[1]';
$media = rex_media::get($filename);
if ($media) {
echo '<img src="' . rex_url::media($filename) . '" alt="' . rex_escape($media->getTitle()) . '">';
}
addMedialistField)Multiple media files; stores a comma-separated filename string.
$mform->addMedialistField(1, ['label' => 'Dateien', 'types' => 'pdf,doc,docx']);
// With view options:
$mform->addMedialistField(2, [
'label' => 'Galerie',
'types' => 'jpg,png,webp',
'view' => 'gallery', // start view: list | grid | gallery
'views' => 'gallery,grid,list',
'view_switch' => 1, // show view-toggle button (default: 1)
]);
Reading in OUTPUT PHP:
$filenames = array_filter(explode(',', 'REX_VALUE[1]'));
foreach ($filenames as $filename) {
$media = rex_media::get($filename);
if ($media) {
echo '<a href="' . rex_url::media($filename) . '">' . rex_escape($media->getTitle()) . '</a>';
}
}
addImagelistField)Image gallery with view-toggle (grid / list / gallery). Stores a comma-separated filename string.
$mform->addImagelistField(1, ['label' => 'Bildergalerie', 'types' => 'jpg,jpeg,png,webp,gif']);
Reading in OUTPUT PHP:
$filenames = array_filter(explode(',', 'REX_VALUE[1]'));
foreach ($filenames as $filename) {
echo '<img src="' . rex_url::media($filename) . '" alt="">';
}
addLinklistField)Multiple internal article links; stores a comma-separated list of article IDs.
$mform->addLinklistField(1, ['label' => 'Verwandte Artikel'], 5);
Reading in OUTPUT PHP:
$ids = array_filter(explode(',', 'REX_VALUE[1]'));
foreach ($ids as $id) {
$art = rex_article::get((int) $id);
if ($art) {
echo '<a href="' . rex_getUrl($art->getId()) . '">' . rex_escape($art->getName()) . '</a>';
}
}
addLinkField)Stores a single article ID as REX_LINK. Output uses the classic REX_LINK[id=n] placeholder.
// label and widget config go in 2nd param ($parameter); catId as 3rd param
$mform->addLinkField(1, ['label' => 'Artikel'], 5); // 3rd param = start category ID
Reading in OUTPUT PHP:
$id = (int) 'REX_LINK[id=1]';
$art = rex_article::get($id);
if ($art) {
echo '<a href="' . rex_getUrl($id) . '">' . rex_escape($art->getName()) . '</a>';
}
Use
addCustomLinkFieldwhen you need external, media or mailto support. UseaddLinkFieldwhen you only need internal articles and want the nativeREX_LINKplaceholder.
By default addMediaField() and addLinkField() use the classic REDAXO core widget (backward-compatible). You can globally switch them to the modern custom_link UI:
// In project/boot.php (affects the whole project):
MForm::useCustomLinkForClassicWidgets(true);
When enabled, both fields store their value in REX_VALUE (same format as addCustomLinkField/addMFormMediaField) instead of REX_MEDIA/REX_LINK. Read the output with MFormOutputHelper::getCustomUrl().
Check the current state:
MForm::isUsingCustomLinkForClassicWidgets(); // bool
addColorSwatchField)Text input + visual color/class picker. Stores either a hex color (#2f77bc) or a CSS class name (.bg-primary).
$mform->addColorSwatchField(1, [
'#ffffff' => 'Weiß',
'#000000' => 'Schwarz',
'#2f77bc' => 'Primär',
'.bg-warning' => ['label' => 'Warnung', 'preview' => '#f0ad4e'], // CSS class with preview color
], ['label' => 'Hintergrundfarbe'], '#ffffff');
Reading in OUTPUT PHP:
$color = 'REX_VALUE[1]';
if (str_starts_with($color, '.')) {
// CSS class
echo '<div class="' . rex_escape(ltrim($color, '.')) . '">';
} else {
// Hex value
echo '<div style="background-color:' . rex_escape($color) . '">';
}
::getWidget())// Medialist widget – standalone, no MForm::factory() needed
echo rex_var_custom_medialist::getWidget(
'settings_media', // unique widget ID
'settings[media]', // form field name (for POST)
$savedValue, // current value (comma-separated filenames)
['view' => 'gallery', 'views' => 'gallery,grid,list']
);
// Custom-Link-Multi widget
echo rex_var_custom_link_multi::getWidget(
'settings_links',
'settings[links]',
$savedValue,
['intern' => 1, 'external' => 1, 'media' => 1]
);
rex_formDedicated rex_form_widget_*_element classes are available:
// Medialist
$field = $form->addField('', 'media_list', null,
['internal::fieldClass' => 'rex_form_widget_mform_medialist_element'], true);
$field->setTypes('jpg,png,pdf');
$field->setView('gallery');
// Linklist
$links = $form->addField('', 'links', null,
['internal::fieldClass' => 'rex_form_widget_mform_linklist_element'], true);
$links->setCategoryId(0);
// Custom-Link-Multi
$custom = $form->addField('', 'custom_links', null,
['internal::fieldClass' => 'rex_form_widget_mform_custom_link_multi_element'], true);
$custom->setIntern(1);
$custom->setExternal(1);
$custom->setMedia(1);
Available rex_form field classes: rex_form_widget_mform_imglist_element, rex_form_widget_mform_medialist_element, rex_form_widget_mform_linklist_element, rex_form_widget_mform_customlink_element, rex_form_widget_mform_custom_link_multi_element.
Widget assets are auto-loaded in the backend via
boot.php– no manual asset include needed.
data-intern / data-extern / data-media default to enable unless explicitly set to disable. All tabs are shown unless you restrict them.explode(',', $val), not json_decode().redaxo:// format.addMediaField() (REDAXO native) vs addMFormMediaField() (MForm custom): both store a filename string. addMFormMediaField uses the Custom-Link widget internally and has slightly different UI.. – remember to strip the leading dot when using as an HTML class attribute: ltrim($val, '.').ylink on Custom-Link requires YForm – ensure YForm is available before adding such a field.