Form generation example

User
Assistant
Got it—here’s a Concrete CMS 9–style version using the core `$form` helper (and a CSRF token). It keeps your Bootstrap layout/cards, but swaps raw inputs for `$form` methods so it’s consistent with dashboard conventions.

<?php
defined('C5_EXECUTE') or die('Access Denied.');

use ConcreteCoreViewView;
use ConcreteCoreSupportFacadeApplication;
use ConcreteCoreUrlUrl;

$app   = Application::getFacadeApplication();
/** @var ConcreteCoreFormServiceForm $form */
$form  = $app->make('helper/form');
$token = $app->make('token');

$view = View::getInstance();
$view->requireAsset('javascript', 'sfm/admin');
$view->requireAsset('css', 'sfm/admin');

/** Generate unique slug */
function generateUniqueSlug()
{
    return 'feed-' . bin2hex(random_bytes(4));
}

$generatedUrl = generateUniqueSlug();

// Build options arrays for selects
$localeOptions = [];
foreach ($locales as $locale) {
    $localeOptions[$locale->getLocale()] = $locale->getLanguageText() . ' (' . $locale->getLocale() . ')';
}

$productOptions = [];
foreach ($products as $product) {
    $productOptions[$product['pID']] = $product['pName'];
}

$productGroupOptions = [];
foreach ($product_groups as $group) {
    $productGroupOptions[$group['gID']] = $group['groupName'];
}

$productTypeOptions = [];
foreach ($product_types as $type) {
    $productTypeOptions[$type['ptID']] = $type['ptName'];
}

$manufacturerOptions = [];
foreach ($manufacturers as $m) {
    $manufacturerOptions[$m['mID']] = $m['mName'];
}

// Derived URLs
$feedSlug = ltrim($generatedUrl, '/');
$baseFeedUrl = rtrim((string) Url::to('/google-merchant-feed/'), '/');
$fullFeedUrl = $baseFeedUrl . '/' . $feedSlug;
?>

<form method="post" action="<?= h($view->action('submit')) ?>" class="ccm-dashboard-form">

    <?= $token->output('submit_feed') ?>
    <?= $form->hidden('feedSlug', $feedSlug) ?>
    <?= $form->hidden('url', $fullFeedUrl) ?>

    <div class="card mb-4">
        <div class="card-header">
            <h5 class="mb-0"><?= t('Basic Information') ?></h5>
        </div>
        <div class="card-body">
            <div class="row g-3">
                <div class="col-md-6">
                    <div class="form-group">
                        <?= $form->label('name', t('Feed Name')) ?> <span class="text-danger">*</span>
                        <?= $form->text('name', '', ['class' => 'form-control ccm-input-text', 'required' => 'required', 'placeholder' => t('Human-readable feed title')]) ?>
                    </div>
                </div>

                <div class="col-md-6">
                    <div class="form-group">
                        <?= $form->label('url_display', t('Feed URL')) ?>
                        <div class="form-control-plaintext">
                            <?= h($baseFeedUrl) ?><strong>/<?= h($feedSlug) ?></strong>
                        </div>
                        <small class="form-text text-muted">
                            <?= t('This is the URL where your feed will be accessible.') ?>
                        </small>
                    </div>
                </div>
            </div>

            <div class="form-group mt-3">
                <?= $form->label('settings[language]', t('Language')) ?>
                <?= $form->select('settings[language]', $localeOptions, null, ['class' => 'form-control form-select']) ?>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header">
            <h5 class="mb-0"><?= t('Products & Options') ?></h5>
        </div>
        <div class="card-body">
            <div class="form-group mb-3">
                <div class="row g-3">
                    <div class="col">
                        <h3 class="h6 mb-2"><?= t('Products') ?></h3>
                        <?= $form->label('settings[pName][]', t('Product Name')) ?>
                        <?= $form->select('settings[pName][]', $productOptions, [], [
                            'class' => 'form-control form-select',
                            'size' => 5,
                            'multiple' => 'multiple'
                        ]) ?>
                        <small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
                    </div>

                    <div class="col">
                        <h3 class="h6 mb-2"><?= t('Options') ?></h3>
                        <?= $form->label('settings[records_per_page]', t('Max number of products to display')) ?>
                        <?= $form->number('settings[records_per_page]', '', [
                            'class' => 'form-control ccm-input-text',
                            'placeholder' => t('Leave blank or 0 to list all matching products'),
                            'min' => 0
                        ]) ?>
                    </div>
                </div>
            </div>

            <div class="form-group mb-0">
                <div class="row g-3">
                    <div class="col">
                        <?= $form->label('settings[sort_order]', t('Sort Order')) ?>
                        <?= $form->select('settings[sort_order]', [
                            'alphabetical' => t('Alphabetical'),
                            'price'        => t('Price'),
                            'date'         => t('Date'),
                        ], 'alphabetical', ['class' => 'form-control form-select']) ?>
                    </div>

                    <div class="col">
                        <?= $form->label('settings[product_groups][]', t('Product Groups')) ?>
                        <?= $form->select('settings[product_groups][]', $productGroupOptions, [], [
                            'class' => 'form-control form-select',
                            'size' => 5,
                            'multiple' => 'multiple'
                        ]) ?>
                        <small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header">
            <h5 class="mb-0"><?= t('Filtering') ?></h5>
        </div>
        <div class="card-body">
            <div class="form-group mb-3">
                <div class="row g-3">
                    <div class="col">
                        <?= $form->label('settings[product_types][]', t('Product Types')) ?>
                        <?= $form->select('settings[product_types][]', $productTypeOptions, [], [
                            'class' => 'form-control form-select',
                            'size' => 5,
                            'multiple' => 'multiple'
                        ]) ?>
                        <small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
                    </div>

                    <div class="col">
                        <?= $form->label('settings[brand][]', t('Brand/Manufacturer')) ?>
                        <?= $form->select('settings[brand][]', $manufacturerOptions, [], [
                            'class' => 'form-control form-select',
                            'size' => 5,
                            'multiple' => 'multiple'
                        ]) ?>
                        <small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
                    </div>
                </div>
            </div>

            <div class="form-group mb-3">
                <div class="row g-3">
                    <div class="col">
                        <?= $form->label('settings[pSku]', t('Product SKU')) ?>
                        <?= $form->text('settings[pSku]', '', ['class' => 'form-control ccm-input-text']) ?>
                    </div>

                    <div class="col">
                        <label class="form-label d-block"><?= t('Attributes') ?></label>

                        <div class="form-check">
                            <?= $form->checkbox('settings[pFeatured]', 1, false, ['class' => 'form-check-input', 'id' => 'pFeatured']) ?>
                            <label class="form-check-label" for="pFeatured"><?= t('Include Featured Only') ?></label>
                        </div>

                        <div class="form-check">
                            <?= $form->checkbox('settings[onsale]', 1, false, ['class' => 'form-check-input', 'id' => 'onsale']) ?>
                            <label class="form-check-label" for="onsale"><?= t('Include OnSale Only') ?></label>
                        </div>

                        <div class="form-check">
                            <?= $form->checkbox('settings[outofstock]', 1, false, ['class' => 'form-check-input', 'id' => 'outofstock']) ?>
                            <label class="form-check-label" for="outofstock"><?= t('Include Out Of Stock Products') ?></label>
                        </div>

                        <div class="form-check">
                            <?= $form->checkbox('settings[inactive]', 1, false, ['class' => 'form-check-input', 'id' => 'inactive']) ?>
                            <label class="form-check-label" for="inactive"><?= t('Include Inactive Products') ?></label>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header">
            <h5 class="mb-0"><?= t('Product Attributes') ?></h5>
        </div>
        <div class="card-body">
            <div class="row g-3">
                <div class="col">
                    <?= $form->label('settings[pTaxable]', t('Taxable')) ?>
                    <?= $form->select('settings[pTaxable]', ['0' => t('No'), '1' => t('Yes')], '0', ['class' => 'form-control form-select']) ?>
                </div>
                <div class="col">
                    <?= $form->label('settings[pShippable]', t('Shippable')) ?>
                    <?= $form->select('settings[pShippable]', ['0' => t('No'), '1' => t('Yes')], '0', ['class' => 'form-control form-select']) ?>
                </div>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header">
            <h5 class="mb-0"><?= t('Price Range') ?></h5>
        </div>
        <div class="card-body">
            <div class="form-group mb-3">
                <?= $form->label('settings[price_range]', t('Price Range')) ?>
                <div class="price-input d-flex align-items-center gap-2">
                    <div class="field">
                        <span><?= t('Min') ?></span>
                        <?= $form->text('settings[price_range][min]', $minPrice, ['class' => 'input-min form-control']) ?>
                    </div>
                    <div class="separator">-</div>
                    <div class="field">
                        <span><?= t('Max') ?></span>
                        <?= $form->text('settings[price_range][max]', $maxPrice, ['class' => 'input-max form-control']) ?>
                    </div>
                </div>

                <div class="slider mt-3">
                    <div class="progress"></div>
                </div>

                <div class="range-input mt-2">
                    <input type="range" class="range-min" min="<?= (float)$minPrice ?>" max="<?= (float)$maxPrice ?>" value="<?= (float)$minPrice ?>" step="0.1">
                    <input type="range" class="range-max" min="<?= (float)$minPrice ?>" max="<?= (float)$maxPrice ?>" value="<?= (float)$maxPrice ?>" step="0.1">
                </div>
            </div>

            <h3 class="h6"><?= t('Automation') ?></h3>
            <div class="form-group mb-3 form-check">
                <?= $form->checkbox('enable_cron', 1, false, ['class' => 'form-check-input', 'id' => 'enable_cron']) ?>
                <label class="form-check-label" for="enable_cron"><?= t('Enable Cron Run') ?></label>
            </div>

            <div class="ccm-dashboard-form-actions-wrapper">
                <div class="ccm-dashboard-form-actions">
                    <a href="<?= h(Url::to('/dashboard/shopping_feed')) ?>" class="btn btn-secondary float-start"><?= t('Cancel') ?></a>
                    <button class="float-end btn btn-primary" type="submit"><?= t('Save') ?></button>
                </div>
            </div>
        </div>
    </div>
</form>

### Notes
– Multiple selects: Concrete’s `$form->select()` supports `multiple` by passing `['multiple' => 'multiple']` and naming with `[]`.
– CSRF: I used `$token->output('submit_feed')`; check in your controller with `$this->token->validate('submit_feed')`.
– URLs: `Url::to('/google-merchant-feed/')` is preferred in v9, but `$this->url()` also works in a view.
– Range sliders aren’t part of the helper; they’re left as native “ so your existing JS keeps working.

User
Attempted to call an undefined method named "to" of class "ConcreteCoreUrlUrl".
Did you mean to call e.g. "__toString" or "toArray"?
Assistant
You imported the wrong `Url` class. You grabbed the value object (`ConcreteCoreUrlUrl`), which **doesn’t** have a static `to()` method—hence the error. You want the **facade**.

### Quick fix
Replace your import:

use ConcreteCoreUrlUrl;

with:

use ConcreteCoreSupportFacadeUrl; // the facade

Then this works:

$baseFeedUrl = rtrim((string) Url::to('/google-merchant-feed'), '/');
$fullFeedUrl = $baseFeedUrl . '/' . $feedSlug;

### Alternatives (also valid)
– In views/controllers you can simply use the view helper you already used elsewhere:

$baseFeedUrl = rtrim($this->url('/google-merchant-feed'), '/');

– Or resolve via the URL resolver:

use ConcreteCoreUrlResolverManagerResolverManagerInterface;
  $resolver = $app->make(ResolverManagerInterface::class);
  $baseFeedUrl = rtrim((string) $resolver->resolve(['/google-merchant-feed']), '/');

But the smallest change is: use the facade `ConcreteCoreSupportFacadeUrl`.

User
Product Name are muliselect but its showing dropdown
Assistant
Ah, got it 👍

In Concrete’s `$form->select()` helper, if you want it to render as a **multi-select listbox** (not just a dropdown with multi-select enabled), you need to:

– Pass the `multiple` attribute,
– Give it a `size` greater than 1.

Otherwise Concrete will render it like a normal select (single-line dropdown), even if you pass `multiple`.

### Example fix for **Product Name**:

<?= $form->select(
    'settings[pName][]',
    $productOptions,
    [], // default selected
    [
        'class' => 'form-control',
        'multiple' => 'multiple',
        'size' => 5   // 👈 ensures it shows as a multi-row listbox
    ]
) ?>

### For all other multi-selects (Product Groups, Types, Brand, etc.)
Do the same:

<?= $form->select(
    'settings[product_groups][]',
    $productGroupOptions,
    [],
    ['class' => 'form-control', 'multiple' => 'multiple', 'size' => 5]
) ?>

🔑 **Key point:**
`multiple` just enables multi-selection.
`size` tells the browser to show it as a listbox with that many visible rows.

Do you want me to go through your whole form and rewrite all the multi-selects with the proper `size` so they render as listboxes instead of dropdowns?

User
not working
Assistant
You’re super close—the issue is styling/JS, not the helper.

### Likely causes
1) **Class conflict:** `form-control` + `form-select` together can force a single-line height.
2) **JS enhancement:** a plugin (Select2/Choices/etc.) in `sfm/admin` may be converting your “ into a dropdown.

### Fix 1 — Use only `form-select` and boolean multiple

<?= $form->select(
    'settings[pName][]',
    $productOptions,
    [],
    [
        'id' => 'pNameList',
        'class' => 'form-select',   // ← remove 'form-control'
        'multiple' => true,         // ← boolean is fine
        'size' => 8                 // ← >1 forces listbox
    ]
) ?>

### Fix 2 — If your JS enhances selects, skip this one
In your `sfm/admin` JS, exclude this field from Select2/Choices init. For example:

// If you do something like: $('.form-select').select2();
$('.form-select').not('#pNameList').select2(); // exclude the multi listbox

### Fix 3 — CSS safety (if theme still forces height)

/* Dashboard-safe override */
.form-select[multiple], .form-select[size] { height: auto !important; }

### Quick sanity check
View source should show:

<select name="settings[pName][]" id="pNameList" class="form-select" multiple size="8">
  ...
</select>

If that’s what you see but it still renders as a dropdown, it’s **definitely** a JS plugin—exclude this ID from the enhancement and it will display as a proper multi-row listbox.

User
still not work
Assistant
Gotcha — let’s brute‑force isolate the cause and fix it.

### 1) Prove the helper is fine (no classes, no JS)
Render a **plain** multi select. If this shows as a listbox, the issue is CSS/JS.

<?= $form->select(
    'settings[pName][]',
    $productOptions,
    [],
    [
        'id' => 'pNameList',
        'multiple' => true,
        'size' => 8,
        // no classes on purpose
    ]
) ?>

– In DevTools → Elements, you should see:

<select id="pNameList" multiple size="8" name="settings[pName][]">…</select>

– If you see a “ or similar wrapping, a JS plugin is hijacking it.

### 2) If a JS plugin is hijacking it (Select2/Choices/etc.)
**Option A: Destroy it after init**

<script>
document.addEventListener('DOMContentLoaded', function () {
  if (window.jQuery && jQuery.fn.select2) {
    try { jQuery('#pNameList').select2('destroy'); } catch(e){}
  }
  if (window.Choices) {
    try {
      // If your code created Choices on .form-select, try to find/destroy by ref
      // Fallback: remove data attributes that Choices adds
      const el = document.getElementById('pNameList');
      el.removeAttribute('data-choice');
      el.classList.remove('choices__input');
      // Or just re-clone to strip enhancements:
      el.replaceWith(el.cloneNode(true));
    } catch(e){}
  }
});
</script>

**Option B: Exclude it during enhancement**
In your `sfm/admin` init code, change:

// BEFORE (example)
$('.form-select').select2();

// AFTER
$('.form-select').not('#pNameList,[multiple],[size]').select2(); // skip multi/size selects
// or
$('.form-select').not('#pNameList').select2();

### 3) If it’s CSS forcing dropdown height
Some themes set a fixed height that breaks listboxes.

Add this tiny CSS override (in `sfm/admin.css` or the view):

/* Ensure multi-selects render as listboxes */
#pNameList[multiple],
#pNameList[size],
.form-select[multiple],
.form-select[size] {
  height: auto !important;
  min-height: 12rem; /* shows ~8 options comfortably */
  overflow: auto;
}

### 4) Final recommended markup (once excluded from JS)

<?= $form->select(
    'settings[pName][]',
    $productOptions,
    [],
    [
        'id' => 'pNameList',
        'class' => 'form-select',   // you can keep Bootstrap styling
        'multiple' => true,
        'size' => 8
    ]
) ?>

### Quick checklist
– [ ] View Source shows “ (good).
– [ ] No `.select2`/`.choices` wrapper around it (if yes → exclude/destroy).
– [ ] Computed CSS for `#pNameList` shows `height: auto` (if not → add override).

If you paste your select-init snippet from `sfm/admin.js`, I’ll tailor the exact exclusion/destroy line for your setup.

User
same convert
Assistant
Perfect—here’s your **edit form** rewritten to Concrete CMS 9’s `$form` helper using **`$form->selectMultiple()`** for all multiselects (this adds `multiple` and the `[]` suffix automatically). I also added a CSRF token and kept your Bootstrap layout.

<?php
defined('C5_EXECUTE') or die('Access Denied.');

use ConcreteCoreViewView;
use ConcreteCoreSupportFacadeApplication;
use ConcreteCoreSupportFacadeUrl;

$view = View::getInstance();
$view->requireAsset('javascript', 'sfm/admin');
$view->requireAsset('css', 'sfm/admin');

$app   = Application::getFacadeApplication();
/** @var ConcreteCoreFormServiceForm $form */
$form  = $app->make('helper/form');
$token = $app->make('token');

$settings = is_array($feed['settings']) ? $feed['settings'] : json_decode((string)$feed['settings'], true) ?: [];

$selectedMin = $settings['price_range']['min'] ?? $minPrice;
$selectedMax = $settings['price_range']['max'] ?? $maxPrice;

$pNameSelected        = $settings['pName'] ?? [];
$productGroupsSelected= $settings['product_groups'] ?? [];
$productTypesSelected = $settings['product_types'] ?? [];
$brandSelected        = $settings['brand'] ?? [];

$pFeatured  = !empty($settings['pFeatured']);
$onSale     = !empty($settings['onsale']);
$outOfStock = !empty($settings['outofstock']);
$inactive   = !empty($settings['inactive']);

$pTaxable   = (string)($settings['pTaxable']   ?? '0');
$pShippable = (string)($settings['pShippable'] ?? '0');
$pLocale    = (string)($settings['language']   ?? '');

$enableCron = !empty($feed['enableCron']);

// Build options
$localeOptions = [];
foreach ($locales as $locale) {
    $localeOptions[$locale->getLocale()] = $locale->getLanguageText() . ' (' . $locale->getLocale() . ')';
}

$productOptions = [];
foreach ($products as $product) {
    $productOptions[$product['pID']] = $product['pName'];
}

$productGroupOptions = [];
foreach ($product_groups as $group) {
    $productGroupOptions[$group['gID']] = $group['groupName'];
}

$productTypeOptions = [];
foreach ($product_types as $pt) {
    $productTypeOptions[$pt['ptID']] = $pt['ptName'];
}

$manufacturerOptions = [];
foreach ($manufacturers as $m) {
    $manufacturerOptions[$m['mID']] = $m['mName'];
}

// Feed URL (display only)
$feedSlug = ltrim($feed['feedSlug'] ?? '', '/');
$baseFeedUrl = rtrim((string) Url::to('/google-merchant-feed'), '/');
$fullFeedUrl = $baseFeedUrl . '/' . $feedSlug;
?>

<form method="post" action="<?= h($view->action('submit', $feed['id'])) ?>" class="ccm-dashboard-form">
    <?= $token->output('update_feed') ?>
    <?= $form->hidden('feedSlug', $feedSlug) ?>
    <?= $form->hidden('url', $fullFeedUrl) ?>

    <div class="card mb-4">
        <div class="card-header">
            <h5 class="mb-0"><?= t('Basic Information') ?></h5>
        </div>
        <div class="card-body">
            <div class="row g-3">
                <div class="col-md-6">
                    <div class="form-group">
                        <?= $form->label('name', t('Feed Name')) ?> <span class="text-danger">*</span>
                        <?= $form->text('name', $feed['name'] ?? '', [
                            'class' => 'form-control ccm-input-text',
                            'required' => 'required',
                            'placeholder' => t('Human-readable feed title'),
                        ]) ?>
                    </div>
                </div>

                <div class="col-md-6">
                    <div class="form-group">
                        <?= $form->label('url_display', t('Feed URL')) ?>
                        <div class="form-control-plaintext">
                            <?= h($baseFeedUrl) ?><strong>/<?= h($feedSlug) ?></strong>
                        </div>
                        <small class="form-text text-muted">
                            <?= t('This is the URL where your feed will be accessible.') ?>
                        </small>
                    </div>
                </div>
            </div>

            <div class="form-group mt-3">
                <?= $form->label('settings[language]', t('Language')) ?>
                <?= $form->select('settings[language]', $localeOptions, $pLocale, ['class' => 'form-select']) ?>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header">
            <h5 class="mb-0"><?= t('Products & Options') ?></h5>
        </div>
        <div class="card-body">
            <div class="form-group mb-3">
                <div class="row g-3">
                    <div class="col">
                        <?= $form->label('settings[pName]', t('Product Name')) ?>
                        <?= $form->selectMultiple(
                            'settings[pName]',
                            $productOptions,
                            $pNameSelected,
                            ['class' => 'form-select', 'size' => 8, 'id' => 'pNameList']
                        ) ?>
                        <small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
                    </div>

                    <div class="col">
                        <?= $form->label('settings[records_per_page]', t('Max number of products to display')) ?>
                        <?= $form->number('settings[records_per_page]', $settings['records_per_page'] ?? '', [
                            'class' => 'form-control ccm-input-text',
                            'placeholder' => t('Leave blank or 0 to list all matching products'),
                            'min' => 0,
                        ]) ?>
                    </div>
                </div>
            </div>

            <div class="form-group mb-0">
                <div class="row g-3">
                    <div class="col">
                        <?= $form->label('settings[sort_order]', t('Sort Order')) ?>
                        <?= $form->select('settings[sort_order]', [
                            'alphabetical' => t('Alphabetical'),
                            'price'        => t('Price'),
                            'date'         => t('Date'),
                        ], $settings['sort_order'] ?? 'alphabetical', ['class' => 'form-select']) ?>
                    </div>

                    <div class="col">
                        <?= $form->label('settings[product_groups]', t('Product Groups')) ?>
                        <?= $form->selectMultiple(
                            'settings[product_groups]',
                            $productGroupOptions,
                            $productGroupsSelected,
                            ['class' => 'form-select', 'size' => 8, 'id' => 'product_groups']
                        ) ?>
                        <small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header">
            <h5 class="mb-0"><?= t('Filtering') ?></h5>
        </div>
        <div class="card-body">
            <div class="form-group mb-3">
                <div class="row g-3">
                    <div class="col">
                        <?= $form->label('settings[product_types]', t('Product Types')) ?>
                        <?= $form->selectMultiple(
                            'settings[product_types]',
                            $productTypeOptions,
                            $productTypesSelected,
                            ['class' => 'form-select', 'size' => 8, 'id' => 'product_types']
                        ) ?>
                        <small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
                    </div>

                    <div class="col">
                        <?= $form->label('settings[brand]', t('Brand/Manufacturer')) ?>
                        <?= $form->selectMultiple(
                            'settings[brand]',
                            $manufacturerOptions,
                            $brandSelected,
                            ['class' => 'form-select', 'size' => 8, 'id' => 'brand']
                        ) ?>
                        <small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
                    </div>
                </div>
            </div>

            <div class="form-group mb-3">
                <div class="row g-3">
                    <div class="col">
                        <?= $form->label('settings[pSku]', t('Product SKU')) ?>
                        <?= $form->text('settings[pSku]', $settings['pSku'] ?? '', [
                            'class' => 'form-control ccm-input-text',
                            'placeholder' => t('Filter by SKU'),
                        ]) ?>
                    </div>

                    <div class="col">
                        <label class="form-label d-block"><?= t('Attributes') ?></label>

                        <div class="form-check">
                            <?= $form->checkbox('settings[pFeatured]', 1, $pFeatured, ['class' => 'form-check-input', 'id' => 'pFeatured']) ?>
                            <label class="form-check-label" for="pFeatured"><?= t('Include Featured Only') ?></label>
                        </div>

                        <div class="form-check">
                            <?= $form->checkbox('settings[onsale]', 1, $onSale, ['class' => 'form-check-input', 'id' => 'onsale']) ?>
                            <label class="form-check-label" for="onsale"><?= t('Include OnSale Only') ?></label>
                        </div>

                        <div class="form-check">
                            <?= $form->checkbox('settings[outofstock]', 1, $outOfStock, ['class' => 'form-check-input', 'id' => 'outofstock']) ?>
                            <label class="form-check-label" for="outofstock"><?= t('Include Out Of Stock Products') ?></label>
                        </div>

                        <div class="form-check">
                            <?= $form->checkbox('settings[inactive]', 1, $inactive, ['class' => 'form-check-input', 'id' => 'inactive']) ?>
                            <label class="form-check-label" for="inactive"><?= t('Include Inactive Products') ?></label>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header">
            <h5 class="mb-0"><?= t('Product Attributes') ?></h5>
        </div>
        <div class="card-body">
            <div class="row g-3">
                <div class="col">
                    <?= $form->label('settings[pTaxable]', t('Taxable')) ?>
                    <?= $form->select('settings[pTaxable]', ['0' => t('No'), '1' => t('Yes')], $pTaxable, ['class' => 'form-select', 'id' => 'pTaxable']) ?>
                </div>
                <div class="col">
                    <?= $form->label('settings[pShippable]', t('Shippable')) ?>
                    <?= $form->select('settings[pShippable]', ['0' => t('No'), '1' => t('Yes')], $pShippable, ['class' => 'form-select', 'id' => 'pShippable']) ?>
                </div>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header">
            <h5 class="mb-0"><?= t('Price Range') ?></h5>
        </div>
        <div class="card-body">
            <div class="form-group mb-3">
                <div class="price-input d-flex align-items-center gap-2">
                    <div class="field">
                        <span><?= t('Min') ?></span>
                        <?= $form->text('settings[price_range][min]', $selectedMin, ['class' => 'input-min form-control']) ?>
                    </div>
                    <div class="separator">-</div>
                    <div class="field">
                        <span><?= t('Max') ?></span>
                        <?= $form->text('settings[price_range][max]', $selectedMax, ['class' => 'input-max form-control']) ?>
                    </div>
                </div>

                <div class="slider mt-3"><div class="progress"></div></div>

                <div class="range-input mt-2">
                    <input type="range" class="range-min" min="<?= (float)$minPrice ?>" max="<?= (float)$maxPrice ?>" value="<?= (float)$minPrice ?>" step="0.1">
                    <input type="range" class="range-max" min="<?= (float)$minPrice ?>" max="<?= (float)$maxPrice ?>" value="<?= (float)$maxPrice ?>" step="0.1">
                </div>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header">
            <h5 class="mb-0"><?= t('Automation') ?></h5>
        </div>
        <div class="card-body">
            <div class="form-check">
                <?= $form->checkbox('enable_cron', 1, $enableCron, ['class' => 'form-check-input', 'id' => 'enable_cron']) ?>
                <label class="form-check-label" for="enable_cron"><?= t('Enable Cron Run') ?></label>
            </div>
        </div>
    </div>

    <div class="ccm-dashboard-form-actions-wrapper">
        <div class="ccm-dashboard-form-actions">
            <a href="<?= h($view->action('')) ?>" class="btn btn-secondary float-start"><?= t('Cancel') ?></a>
            <button class="float-end btn btn-primary" type="submit"><?= t('Update') ?></button>
        </div>
    </div>
</form>

### Notes
– **`selectMultiple`**: Use names **without** `[]` (e.g. `'settings[pName]'`). The helper outputs `multiple` and `name="…[]"` for you.
– Added `size => 8` so it renders as a **listbox**.
– If your `sfm/admin` JS enhances `.form-select` with Select2/Choices, **exclude** these IDs (`#pNameList`, `#product_groups`, `#product_types`, `#brand`) from enhancement so they stay as listboxes.

User
Compile Error: Unparenthesized `a ? b : c ?: d` is not supported. Use either `(a ? b : c) ?: d` or `a ? b : (c ?: d)`
Assistant
That error is from a **nested ternary without parentheses** (PHP 8+). In my snippet this line is the culprit:

$settings = is_array($feed['settings']) ? $feed['settings'] : json_decode((string)$feed['settings'], true) ?: [];

Fix it by either adding parentheses **or** (cleaner) switching to `??`. Here’s a safe, drop‑in replacement for your setup block:

<?php
defined('C5_EXECUTE') or die('Access Denied.');

use ConcreteCoreViewView;
use ConcreteCoreSupportFacadeApplication;
use ConcreteCoreSupportFacadeUrl;

$view = View::getInstance();
$view->requireAsset('javascript', 'sfm/admin');
$view->requireAsset('css', 'sfm/admin');

$app   = Application::getFacadeApplication();
/** @var ConcreteCoreFormServiceForm $form */
$form  = $app->make('helper/form');
$token = $app->make('token');

// ✅ avoid nested ternaries
$rawSettings = $feed['settings'] ?? '[]';
$settings = is_array($rawSettings)
    ? $rawSettings
    : (json_decode((string)$rawSettings, true) ?? []);

$selectedMin = $settings['price_range']['min'] ?? $minPrice;
$selectedMax = $settings['price_range']['max'] ?? $maxPrice;

$pNameSelected          = $settings['pName']           ?? [];
$productGroupsSelected  = $settings['product_groups']  ?? [];
$productTypesSelected   = $settings['product_types']   ?? [];
$brandSelected          = $settings['brand']           ?? [];

$pFeatured  = !empty($settings['pFeatured']);
$onSale     = !empty($settings['onsale']);
$outOfStock = !empty($settings['outofstock']);
$inactive   = !empty($settings['inactive']);

$pTaxable   = (string)($settings['pTaxable']   ?? '0');
$pShippable = (string)($settings['pShippable'] ?? '0');
$pLocale    = (string)($settings['language']   ?? '');

$enableCron = !empty($feed['enableCron']);

$feedSlug     = ltrim($feed['feedSlug'] ?? '', '/');
$baseFeedUrl  = rtrim((string) Url::to('/google-merchant-feed'), '/');
$fullFeedUrl  = $baseFeedUrl . '/' . $feedSlug;

If you really want to keep the one‑liner, parenthesize it:

$settings = is_array($feed['settings'] ?? null)
    ? $feed['settings']
    : (json_decode((string)($feed['settings'] ?? ''), true) ?: []);

Scan for any other patterns like `a ? b : c ?: d` and either add parentheses or replace the final `?: d` with `?? d`.

User
Now we have both form
Assistant
Great idea—move the shared markup into a single **element** and load it from both `add.php` and `edit.php`.

Below are three drop‑in snippets:

# 1) Element file (common form)

Create this at:
`packages/shopping_feed_manager/elements/shopping_feed/form.php`

<?php
defined('C5_EXECUTE') or die('Access Denied.');

use ConcreteCoreSupportFacadeApplication;
use ConcreteCoreSupportFacadeUrl;

/** expected vars:
 * $locales, $products, $product_groups, $product_types, $manufacturers
 * $minPrice, $maxPrice
 * $feed (assoc array for edit) OR null for add
 * $actionUrl (string)
 * $tokenName (string) e.g. 'submit_feed' or 'update_feed'
 */

$app   = Application::getFacadeApplication();
/** @var ConcreteCoreFormServiceForm $form */
$form  = $app->make('helper/form');
$token = $app->make('token');

$isEdit = !empty($feed);
$settingsRaw = $isEdit ? ($feed['settings'] ?? '[]') : '[]';
$settings = is_array($settingsRaw) ? $settingsRaw : (json_decode((string)$settingsRaw, true) ?? []);

$selectedMin = $settings['price_range']['min'] ?? $minPrice;
$selectedMax = $settings['price_range']['max'] ?? $maxPrice;

$pNameSelected          = $settings['pName']          ?? [];
$productGroupsSelected  = $settings['product_groups'] ?? [];
$productTypesSelected   = $settings['product_types']  ?? [];
$brandSelected          = $settings['brand']          ?? [];

$pFeatured  = !empty($settings['pFeatured']);
$onSale     = !empty($settings['onsale']);
$outOfStock = !empty($settings['outofstock']);
$inactive   = !empty($settings['inactive']);

$pTaxable   = (string)($settings['pTaxable']   ?? '0');
$pShippable = (string)($settings['pShippable'] ?? '0');
$pLocale    = (string)($settings['language']   ?? '');

$enableCron = $isEdit ? !empty($feed['enableCron']) : false;

// Options builders
$localeOptions = [];
foreach ($locales as $locale) {
    $localeOptions[$locale->getLocale()] = $locale->getLanguageText() . ' (' . $locale->getLocale() . ')';
}
$productOptions = [];
foreach ($products as $p) { $productOptions[$p['pID']] = $p['pName']; }
$productGroupOptions = [];
foreach ($product_groups as $g) { $productGroupOptions[$g['gID']] = $g['groupName']; }
$productTypeOptions = [];
foreach ($product_types as $pt) { $productTypeOptions[$pt['ptID']] = $pt['ptName']; }
$manufacturerOptions = [];
foreach ($manufacturers as $m) { $manufacturerOptions[$m['mID']] = $m['mName']; }

// Feed URL / slug
if ($isEdit) {
    $feedSlug = ltrim((string)($feed['feedSlug'] ?? ''), '/');
    $feedName = (string)($feed['name'] ?? '');
} else {
    $feedSlug = 'feed-' . bin2hex(random_bytes(4));
    $feedName = '';
}
$baseFeedUrl = rtrim((string) Url::to('/google-merchant-feed'), '/');
$fullFeedUrl = $baseFeedUrl . '/' . $feedSlug;
?>

<form method="post" action="<?= h($actionUrl) ?>" class="ccm-dashboard-form">
    <?= $token->output($tokenName) ?>
    <?= $form->hidden('feedSlug', $feedSlug) ?>
    <?= $form->hidden('url', $fullFeedUrl) ?>

    <div class="card mb-4">
        <div class="card-header"><h5 class="mb-0"><?= t('Basic Information') ?></h5></div>
        <div class="card-body">
            <div class="row g-3">
                <div class="col-md-6">
                    <?= $form->label('name', t('Feed Name')) ?> <span class="text-danger">*</span>
                    <?= $form->text('name', $feedName, [
                        'class' => 'form-control ccm-input-text',
                        'required' => 'required',
                        'placeholder' => t('Human-readable feed title'),
                    ]) ?>
                </div>

                <div class="col-md-6">
                    <?= $form->label('url_display', t('Feed URL')) ?>
                    <div class="form-control-plaintext">
                        <?= h($baseFeedUrl) ?><strong>/<?= h($feedSlug) ?></strong>
                    </div>
                    <small class="form-text text-muted"><?= t('This is the URL where your feed will be accessible.') ?></small>
                </div>
            </div>

            <div class="form-group mt-3">
                <?= $form->label('settings[language]', t('Language')) ?>
                <?= $form->select('settings[language]', $localeOptions, $pLocale, ['class' => 'form-select']) ?>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header"><h5 class="mb-0"><?= t('Products & Options') ?></h5></div>
        <div class="card-body">
            <div class="row g-3 mb-3">
                <div class="col">
                    <?= $form->label('settings[pName]', t('Product Name')) ?>
                    <?= $form->selectMultiple('settings[pName]', $productOptions, $pNameSelected, [
                        'class' => 'form-select', 'size' => 8, 'id' => 'pNameList'
                    ]) ?>
                    <small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
                </div>

                <div class="col">
                    <?= $form->label('settings[records_per_page]', t('Max number of products to display')) ?>
                    <?= $form->number('settings[records_per_page]', $settings['records_per_page'] ?? '', [
                        'class' => 'form-control ccm-input-text',
                        'placeholder' => t('Leave blank or 0 to list all matching products'),
                        'min' => 0
                    ]) ?>
                </div>
            </div>

            <div class="row g-3">
                <div class="col">
                    <?= $form->label('settings[sort_order]', t('Sort Order')) ?>
                    <?= $form->select('settings[sort_order]', [
                        'alphabetical' => t('Alphabetical'),
                        'price'        => t('Price'),
                        'date'         => t('Date'),
                    ], $settings['sort_order'] ?? 'alphabetical', ['class' => 'form-select']) ?>
                </div>

                <div class="col">
                    <?= $form->label('settings[product_groups]', t('Product Groups')) ?>
                    <?= $form->selectMultiple('settings[product_groups]', $productGroupOptions, $productGroupsSelected, [
                        'class' => 'form-select', 'size' => 8, 'id' => 'product_groups'
                    ]) ?>
                    <small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
                </div>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header"><h5 class="mb-0"><?= t('Filtering') ?></h5></div>
        <div class="card-body">
            <div class="row g-3 mb-3">
                <div class="col">
                    <?= $form->label('settings[product_types]', t('Product Types')) ?>
                    <?= $form->selectMultiple('settings[product_types]', $productTypeOptions, $productTypesSelected, [
                        'class' => 'form-select', 'size' => 8, 'id' => 'product_types'
                    ]) ?>
                    <small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
                </div>

                <div class="col">
                    <?= $form->label('settings[brand]', t('Brand/Manufacturer')) ?>
                    <?= $form->selectMultiple('settings[brand]', $manufacturerOptions, $brandSelected, [
                        'class' => 'form-select', 'size' => 8, 'id' => 'brand'
                    ]) ?>
                    <small class="text-muted"><?= t('Hold Ctrl/Cmd to select multiple.') ?></small>
                </div>
            </div>

            <div class="row g-3">
                <div class="col">
                    <?= $form->label('settings[pSku]', t('Product SKU')) ?>
                    <?= $form->text('settings[pSku]', $settings['pSku'] ?? '', ['class' => 'form-control ccm-input-text']) ?>
                </div>

                <div class="col">
                    <label class="form-label d-block"><?= t('Attributes') ?></label>

                    <div class="form-check">
                        <?= $form->checkbox('settings[pFeatured]', 1, $pFeatured, ['class' => 'form-check-input', 'id' => 'pFeatured']) ?>
                        <label class="form-check-label" for="pFeatured"><?= t('Include Featured Only') ?></label>
                    </div>
                    <div class="form-check">
                        <?= $form->checkbox('settings[onsale]', 1, $onSale, ['class' => 'form-check-input', 'id' => 'onsale']) ?>
                        <label class="form-check-label" for="onsale"><?= t('Include OnSale Only') ?></label>
                    </div>
                    <div class="form-check">
                        <?= $form->checkbox('settings[outofstock]', 1, $outOfStock, ['class' => 'form-check-input', 'id' => 'outofstock']) ?>
                        <label class="form-check-label" for="outofstock"><?= t('Include Out Of Stock Products') ?></label>
                    </div>
                    <div class="form-check">
                        <?= $form->checkbox('settings[inactive]', 1, $inactive, ['class' => 'form-check-input', 'id' => 'inactive']) ?>
                        <label class="form-check-label" for="inactive"><?= t('Include Inactive Products') ?></label>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header"><h5 class="mb-0"><?= t('Product Attributes') ?></h5></div>
        <div class="card-body">
            <div class="row g-3">
                <div class="col">
                    <?= $form->label('settings[pTaxable]', t('Taxable')) ?>
                    <?= $form->select('settings[pTaxable]', ['0' => t('No'), '1' => t('Yes')], $pTaxable, ['class' => 'form-select', 'id' => 'pTaxable']) ?>
                </div>
                <div class="col">
                    <?= $form->label('settings[pShippable]', t('Shippable')) ?>
                    <?= $form->select('settings[pShippable]', ['0' => t('No'), '1' => t('Yes')], $pShippable, ['class' => 'form-select', 'id' => 'pShippable']) ?>
                </div>
            </div>
        </div>
    </div>

    <div class="card mb-4">
        <div class="card-header"><h5 class="mb-0"><?= t('Price Range') ?></h5></div>
        <div class="card-body">
            <div class="form-group mb-3">
                <div class="price-input d-flex align-items-center gap-2">
                    <div class="field">
                        <span><?= t('Min') ?></span>
                        <?= $form->text('settings[price_range][min]', $selectedMin, ['class' => 'input-min form-control']) ?>
                    </div>
                    <div class="separator">-</div>
                    <div class="field">
                        <span><?= t('Max') ?></span>
                        <?= $form->text('settings[price_range][max]', $selectedMax, ['class' => 'input-max form-control']) ?>
                    </div>
                </div>

                <div class="slider mt-3"><div class="progress"></div></div>

                <div class="range-input mt-2">
                    <input type="range" class="range-min" min="<?= (float)$minPrice ?>" max="<?= (float)$maxPrice ?>" value="<?= (float)$minPrice ?>" step="0.1">
                    <input type="range" class="range-max" min="<?= (float)$minPrice ?>" max="<?= (float)$maxPrice ?>" value="<?= (float)$maxPrice ?>" step="0.1">
                </div>
            </div>

            <div class="form-check">
                <?= $form->checkbox('enable_cron', 1, $enableCron, ['class' => 'form-check-input', 'id' => 'enable_cron']) ?>
                <label class="form-check-label" for="enable_cron"><?= t('Enable Cron Run') ?></label>
            </div>

            <div class="ccm-dashboard-form-actions-wrapper mt-3">
                <div class="ccm-dashboard-form-actions">
                    <a href="<?= h(Url::to('/dashboard/shopping_feed')) ?>" class="btn btn-secondary float-start"><?= t('Cancel') ?></a>
                    <button class="float-end btn btn-primary" type="submit"><?= $isEdit ? t('Update') : t('Save') ?></button>
                </div>
            </div>
        </div>
    </div>
</form>

# 2) `add.php` (calls the element)

`packages/shopping_feed_manager/single_pages/dashboard/shopping_feed/add.php`

<?php
defined('C5_EXECUTE') or die('Access Denied.');

use ConcreteCoreViewView;

$view = View::getInstance();
$view->requireAsset('javascript', 'sfm/admin');
$view->requireAsset('css', 'sfm/admin');

View::element(
    'shopping_feed/form',
    [
        'locales'          => $locales,
        'products'         => $products,
        'product_groups'   => $product_groups,
        'product_types'    => $product_types,
        'manufacturers'    => $manufacturers,
        'minPrice'         => $minPrice,
        'maxPrice'         => $maxPrice,
        'feed'             => null, // add mode
        'actionUrl'        => $view->action('submit'),
        'tokenName'        => 'submit_feed',
    ],
    'shopping_feed_manager'
);

# 3) `edit.php` (calls the element)

`packages/shopping_feed_manager/single_pages/dashboard/shopping_feed/edit.php`

<?php
defined('C5_EXECUTE') or die('Access Denied.');

use ConcreteCoreViewView;

$view = View::getInstance();
$view->requireAsset('javascript', 'sfm/admin');
$view->requireAsset('css', 'sfm/admin');

View::element(
    'shopping_feed/form',
    [
        'locales'          => $locales,
        'products'         => $products,
        'product_groups'   => $product_groups,
        'product_types'    => $product_types,
        'manufacturers'    => $manufacturers,
        'minPrice'         => $minPrice,
        'maxPrice'         => $maxPrice,
        'feed'             => $feed, // edit mode data from controller
        'actionUrl'        => $view->action('submit', $feed['id']),
        'tokenName'        => 'update_feed',
    ],
    'shopping_feed_manager'
);

## Controller token checks (quick heads‑up)
In your single page controller, validate the token names accordingly:

if (!$this->token->validate('submit_feed')) { /* handle error */ }
// or
if (!$this->token->validate('update_feed')) { /* handle error */ }

This keeps **one source of truth** for the form, works for both add & edit, and preserves your multi‑select listboxes (`selectMultiple` + `size`).

User
form not showjg in add.php
Assistant
Got you. If the form isn’t rendering in `add.php`, it’s almost always one of these:

1) wrong element path/handle,
2) not actually calling the element,
3) missing vars (fatal error hidden by cache),
4) asset/URL import error stopping the view.

Here’s a bullet‑proof setup.

# 1) Put the element here

packages/shopping_feed_manager/elements/shopping_feed/form.php

(Exactly this path. `shopping_feed_manager` must match your **package folder name** / `$pkgHandle`.)

# 2) In `add.php`, call the element like this
Using the `$this->element()` helper is the simplest in single page views:

<?php
defined('C5_EXECUTE') or die('Access Denied.');

/** @var ConcreteCoreViewView $this */
$this->requireAsset('javascript', 'sfm/admin');
$this->requireAsset('css', 'sfm/admin');

$this->element(
    'shopping_feed/form',
    [
        'locales'          => $locales,
        'products'         => $products,
        'product_groups'   => $product_groups,
        'product_types'    => $product_types,
        'manufacturers'    => $manufacturers,
        'minPrice'         => $minPrice,
        'maxPrice'         => $maxPrice,
        'feed'             => null, // add mode
        'actionUrl'        => $this->action('submit'),
        'tokenName'        => 'submit_feed',
    ],
    'shopping_feed_manager' // ← your package handle
);

If you prefer `View::element`, this also works:

use ConcreteCoreViewView;
View::element('shopping_feed/form', [...], 'shopping_feed_manager');

# 3) In `edit.php`, same pattern

<?php
defined('C5_EXECUTE') or die('Access Denied.');

$this->requireAsset('javascript', 'sfm/admin');
$this->requireAsset('css', 'sfm/admin');

$this->element(
    'shopping_feed/form',
    [
        'locales'          => $locales,
        'products'         => $products,
        'product_groups'   => $product_groups,
        'product_types'    => $product_types,
        'manufacturers'    => $manufacturers,
        'minPrice'         => $minPrice,
        'maxPrice'         => $maxPrice,
        'feed'             => $feed, // from controller
        'actionUrl'        => $this->action('submit', $feed['id']),
        'tokenName'        => 'update_feed',
    ],
    'shopping_feed_manager'
);

# 4) Common gotchas checklist
– **Handle/path:** The 3rd param **must** be your package handle string (`shopping_feed_manager`). The element path must be `/elements/shopping_feed/form.php`.
– **Imports in the element:** In `form.php` use `use ConcreteCoreSupportFacadeUrl;` (the **facade**, not `ConcreteCoreUrlUrl`).
– **Variables exist:** Ensure your controller sets `$locales, $products, $product_groups, $product_types, $manufacturers, $minPrice, $maxPrice` for both add and edit actions.
– **Errors hidden by cache:** Clear cache: Dashboard → System & Settings → Optimization → **Clear Cache**. Check logs at `application/files/logs/` if nothing shows.
– **Minimal sanity test:** Add a temporary `echo 'FORM ELEMENT LOADED';` at the top of `form.php`. If you don’t see it, the path/handle is wrong.

If you paste your current `add()` controller method (where you set the variables), I’ll align it so the element definitely gets everything it needs.

User
still not work
Assistant
Alright—let’s pin this down methodically. If the form doesn’t show in `add.php`, it’s either:

– the element **path/handle** is wrong,
– the **element is throwing a fatal** before output,
– the **variables aren’t set** in the controller,
– or the **call** to render the element is wrong.

Here’s a tight checklist + minimal test code so we can see exactly where it fails.

# 1) Verify file locations (exact)
– Single page view:

packages/shopping_feed_manager/single_pages/dashboard/shopping_feed/add.php

– Common element:

packages/shopping_feed_manager/elements/shopping_feed/form.php

– Package handle in package controller:

protected $pkgHandle = 'shopping_feed_manager';

# 2) Minimal smoke test (element actually gets called)
Temporarily put this at the **very top** of `packages/shopping_feed_manager/elements/shopping_feed/form.php`:

<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>
<?php echo '<div style="padding:8px;background:#fffae6;border:1px solid #f0c36d;margin-bottom:8px;">FORM ELEMENT LOADED</div>'; ?>

If you **don’t** see “FORM ELEMENT LOADED”, the element isn’t being found/called → go to step 3.
If you **do** see it, the element is loading, and a **fatal later** is killing the rest → go to step 4.

# 3) Call the element in add.php the “safe” way
Use the `$this->element()` form inside a single page **and** pass the package handle (3rd param):

<?php
defined('C5_EXECUTE') or die('Access Denied.');

/** @var ConcreteCoreViewView $this */
$this->requireAsset('javascript', 'sfm/admin');
$this->requireAsset('css', 'sfm/admin');

/* Sanity: show we're in add.php */
echo '<div style="padding:6px;background:#eef;">ADD VIEW LOADED</div>';

$this->element(
    'shopping_feed/form',
    [
        'locales'          => $locales ?? [],
        'products'         => $products ?? [],
        'product_groups'   => $product_groups ?? [],
        'product_types'    => $product_types ?? [],
        'manufacturers'    => $manufacturers ?? [],
        'minPrice'         => $minPrice ?? 0,
        'maxPrice'         => $maxPrice ?? 0,
        'feed'             => null,
        'actionUrl'        => $this->action('submit'),
        'tokenName'        => 'submit_feed',
    ],
    'shopping_feed_manager' // <-- must match package handle exactly
);

If still nothing shows from the element, **try without the package handle** by temporarily copying the element to `application/elements/shopping_feed/form.php`, then call:

$this->element('shopping_feed/form', [/* same vars */]);

If it renders from `application/…` but not from `packages/…`, your 3rd param handle or element path under the package is wrong.

# 4) Make sure the controller sets variables
Your single page controller must set the vars that the element expects.

// File: packages/shopping_feed_manager/controllers/single_page/dashboard/shopping_feed.php
namespace ConcretePackageShoppingFeedManagerControllerSinglePageDashboard;

use ConcreteCorePageControllerDashboardPageController;

class ShoppingFeed extends DashboardPageController
{
    public function add()
    {
        // TODO: fetch real data; placeholders here
        $this->set('locales', $this->getLocales());
        $this->set('products', $this->getProducts());
        $this->set('product_groups', $this->getProductGroups());
        $this->set('product_types', $this->getProductTypes());
        $this->set('manufacturers', $this->getManufacturers());
        $this->set('minPrice', 0);
        $this->set('maxPrice', 1000);
    }

    // … helper methods …
}

If these aren’t set, PHP notices/fatals inside the element can short‑circuit output.

# 5) Show PHP errors to catch fatals
Turn on error display temporarily (locally):

– Dashboard → System & Settings → Debug → **Display Errors** → On
or in `application/config/generated_overrides/concrete.php`:

'debug' => [
      'display_errors' => true,
  ],

Also check logs: `application/files/logs/` after hitting the page.

# 6) Element guard + early error surfacing
Wrap the element internals with **existence checks** so missing vars don’t fatal:

<?php
// In the element form.php, right after the "FORM ELEMENT LOADED" test echo, add:
$required = ['locales','products','product_groups','product_types','manufacturers','minPrice','maxPrice','actionUrl','tokenName'];
$missing = array_filter($required, fn($k) => !isset($$k));
if ($missing) {
    echo '<pre style="color:#b00;background:#fee;padding:8px;">Missing vars: '.implode(', ', $missing).'</pre>';
    return;
}

If you see “Missing vars…”, then your controller isn’t setting them for `add()`.

# 7) Common gotchas
– **Wrong Url import in element**: use the **facade**

use ConcreteCoreSupportFacadeUrl;

not `ConcreteCoreUrlUrl`.
– **Cache**: Clear cache: Dashboard → System & Settings → Optimization → **Clear Cache**.
– **Case/typo in folders**: `elements/shopping_feed/form.php` (not `shopping-feed` or `shoppingfeed`).
– **Calling `$view = View::getInstance()` everywhere**: not needed for `$this->element()`; can skip to reduce risk.

If you try the smoke echos and the missing‑vars guard, you’ll see exactly which step is failing. Send me:
– what you see on the page (the two test boxes / missing vars),
– and any log/error message you get,

and I’ll pinpoint the exact line to fix.

Leave a Comment

Your email address will not be published. Required fields are marked *