Searchbox for Concrete CMS

User
How to add search searchbox for concrete cms 9 cusotm package single page listing
getHeaderSearch using concrete cms way
Assistant
Here’s the “Concrete CMS way” to get the nice Dashboard-style header search on your own single page list in v9.

## 1) Controller: wire up the search plumbing

Make your Dashboard single page controller extend `DashboardPageController` and implement the same helpers core uses:

<?php
namespace ConcretePackageYourPkgControllerSinglePageDashboardYourpkg;

use ConcreteCorePageControllerDashboardPageController;
use ConcreteCoreFilesystemElementManager;
use ConcreteCoreSearchQueryQueryFactory;
use ConcreteCoreSearchResultResult;
use ConcreteCoreSearchResultResultFactory;
use ConcreteCoreSearchQueryModifierQueryModifier;
use ConcreteCoreSearchQueryModifierAutoSortColumnRequestModifier;
use ConcreteCoreSearchQueryModifierItemsPerPageRequestModifier;
use ConcreteCoreSearchFieldFieldKeywordsField;
use SymfonyComponentHttpFoundationRequest;
use ConcreteCoreEntitySearchQuery as QueryEntity;

class Items extends DashboardPageController
{
    protected $headerMenu;
    protected $headerSearch;

    // (A) your custom provider (see step 2)
    protected function getSearchProvider()
    {
        return $this->app->make(ApplicationSearchItemsSearchProvider::class);
    }

    // (B) boilerplate helpers
    protected function getQueryFactory()
    {
        return $this->app->make(QueryFactory::class);
    }

    protected function getHeaderMenu()
    {
        if (!isset($this->headerMenu)) {
            $this->headerMenu = $this->app->make(ElementManager::class)->get('items/search/menu');
        }
        return $this->headerMenu;
    }

    protected function getHeaderSearch()
    {
        if (!isset($this->headerSearch)) {
            $this->headerSearch = $this->app->make(ElementManager::class)->get('items/search/search');
        }
        return $this->headerSearch;
    }

    protected function createSearchResult(QueryEntity $query): Result
    {
        $provider      = $this->getSearchProvider();
        $resultFactory = $this->app->make(ResultFactory::class);
        $modifier      = $this->app->make(QueryModifier::class);

        $modifier->addModifier(new AutoSortColumnRequestModifier($provider, $this->request, Request::METHOD_GET));
        $modifier->addModifier(new ItemsPerPageRequestModifier($provider, $this->request, Request::METHOD_GET));

        return $resultFactory->createFromQuery($provider, $modifier->process($query));
    }

    protected function getSearchKeywordsField()
    {
        $kw = $this->request->query->get('keywords');
        return new KeywordsField($kw);
    }

    // routes:
    public function view()
    {
        // simple keyword search by default
        $query = $this->getQueryFactory()->createQuery(
            $this->getSearchProvider(),
            [$this->getSearchKeywordsField()]
        );
        $this->renderSearchResult($this->createSearchResult($query));
    }

    public function advanced_search()
    {
        $query  = $this->getQueryFactory()->createFromAdvancedSearchRequest($this->getSearchProvider(), $this->request, Request::METHOD_GET);
        $result = $this->createSearchResult($query);
        $this->renderSearchResult($result);
    }

    protected function renderSearchResult(Result $result)
    {
        $headerMenu   = $this->getHeaderMenu();
        $headerSearch = $this->getHeaderSearch();

        // pass the built query back into the header controls (so fields prefill)
        $headerMenu->getElementController()->setQuery($result->getQuery());
        $headerSearch->getElementController()->setQuery($result->getQuery());

        $this->set('result', $result);
        $this->set('headerMenu', $headerMenu);
        $this->set('headerSearch', $headerSearch);

        // use the wide “full.php” Dashboard template like core search pages
        $this->setThemeViewTemplate('full.php');
    }
}

This mirrors how core does it for Users/Pages/Files (note the `getHeaderSearch()` + `ElementManager::get('…/search')` pattern and the `QueryFactory`/`ResultFactory` flow). citeturn6view0turn7search4

## 2) Provide a SearchProvider for your items

Create a provider class (e.g., `application/src/Search/Items/SearchProvider.php`) that implements the Concrete search provider interface and returns:
– a `FieldManager` (your searchable fields),
– a `ColumnSet` (columns to show),
– an `ItemList` (DB/Entity list to query),
– base URL, default columns, items per page, etc.

This is the same abstraction core uses behind Users/Pages/Files search.

## 3) Header elements (your search box + menu)

Add two element controllers & views in your package (or in `application/`):

packages/your_pkg/controllers/element/items/search/search.php
packages/your_pkg/elements/items/search/search.php
packages/your_pkg/controllers/element/items/search/menu.php
packages/your_pkg/elements/items/search/menu.php

A minimal `controllers/element/items/search/search.php`:

<?php
namespace ConcretePackageYourPkgControllerElementItemsSearch;

use ConcreteCoreControllerElementController;
use ConcreteCoreEntitySearchQuery;

class Search extends ElementController
{
    protected $query;

    public function getElement()
    {
        // path under your package's /elements folder
        return 'items/search/search';
    }

    public function setQuery(?Query $query = null)
    {
        $this->query = $query;
    }

    public function view()
    {
        $this->set('query', $this->query);
        // optionally: $this->set('headerSearchAction', $this->action('advanced_search'));
    }
}

Then in `elements/items/search/search.php` render the search UI. Easiest path: copy the core markup from Users/Pages search elements and adapt fields to your provider (so your advanced filters, presets, save search, etc., appear like the rest of the Dashboard). Core wires the header search element exactly this way (controller sets the `Query`, view outputs fields and submits to `advanced_search`). citeturn7search6turn7search7

## 4) Your single page view

In your SP view (e.g., `packages/your_pkg/single_pages/dashboard/yourpkg/items.php`), output the two header elements and the results table (the controller already sets them):

<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>
<div class="ccm-dashboard-header-buttons">
    <?php /* header menu (bulk actions, export, etc.) */ echo $headerMenu->render(); ?>
</div>

<div class="ccm-dashboard-content-inner">
    <?php /* header search (keyword + advanced fields) */ echo $headerSearch->render(); ?>

    <?php
    // results table (the Result object renders a bedrock/Bootstrap 5 table like core)
    $list = $result->getItemListObject();     // your ItemList
    $pagination = $list->getPagination();
    echo $result->getSearchResultObject()->render(); // standard search result table
    echo $pagination->renderDefaultView();
    ?>
</div>

### References to core patterns
– Users search controller shows `getHeaderSearch()` implemented via `ElementManager::get('users/search/search')`, passing the built `Query` back into the element so the form is prefilled. Use the exact same pattern. citeturn6view0
– Pages/Sitemap search does the same with `pages/search/search`. citeturn7search4

If you want, tell me the fields/columns you need and I’ll sketch the `SearchProvider`, `FieldManager`, and `ColumnSet` too, so your header search can do keywords + advanced filters exactly like core.

User
we have custom package name
ConcretePackageShoppingFeedManager
controllers/single_page/dashboard/shopping_feed.php
single_page/dashboard/shopping_feed/view.php

now want to show concrete cms 9 search header as on default pagges

Assistant
Got it. Here’s a drop-in scaffold to make your ShoppingFeedManager single page show the **standard Concrete CMS v9 header search** (like Users/Pages/Files) using the core search plumbing.

I’ll use your paths:

– `packages/shopping_feed_manager/controllers/single_page/dashboard/shopping_feed.php`
– `packages/shopping_feed_manager/single_pages/dashboard/shopping_feed/view.php`

…and add a tiny search provider + two header elements inside your package.

# 1) Controller: wire up header menu/search + results

`packages/shopping_feed_manager/controllers/single_page/dashboard/shopping_feed.php`

<?php
namespace ConcretePackageShoppingFeedManagerControllerSinglePageDashboard;

use ConcreteCorePageControllerDashboardPageController;
use ConcreteCoreFilesystemElementManager;
use ConcreteCoreSearchQueryQueryFactory;
use ConcreteCoreSearchResultResultFactory;
use ConcreteCoreSearchResultResult;
use ConcreteCoreSearchQueryModifierQueryModifier;
use ConcreteCoreSearchQueryModifierAutoSortColumnRequestModifier;
use ConcreteCoreSearchQueryModifierItemsPerPageRequestModifier;
use ConcreteCoreSearchFieldFieldKeywordsField;
use SymfonyComponentHttpFoundationRequest;
use ConcreteCoreEntitySearchQuery as QueryEntity;

class ShoppingFeed extends DashboardPageController
{
    protected $headerMenu;
    protected $headerSearch;

    /** (A) Your custom provider (see step 2) */
    protected function getSearchProvider()
    {
        return $this->app->make(ConcretePackageShoppingFeedManagerSearchFeedSearchProvider::class);
    }

    /** (B) Core helpers used by search pages */
    protected function getQueryFactory()
    {
        return $this->app->make(QueryFactory::class);
    }

    protected function getHeaderMenu()
    {
        if (!isset($this->headerMenu)) {
            $this->headerMenu = $this->app->make(ElementManager::class)->get('shopping_feed/search/menu');
        }
        return $this->headerMenu;
    }

    protected function getHeaderSearch()
    {
        if (!isset($this->headerSearch)) {
            $this->headerSearch = $this->app->make(ElementManager::class)->get('shopping_feed/search/search');
        }
        return $this->headerSearch;
    }

    protected function createSearchResult(QueryEntity $query): Result
    {
        $provider      = $this->getSearchProvider();
        $resultFactory = $this->app->make(ResultFactory::class);
        $modifier      = $this->app->make(QueryModifier::class);

        // Respect ?ccm_order_by / ?ccm_order_dir / ?itemsPerPage in the URL (like core)
        $modifier->addModifier(new AutoSortColumnRequestModifier($provider, $this->request, Request::METHOD_GET));
        $modifier->addModifier(new ItemsPerPageRequestModifier($provider, $this->request, Request::METHOD_GET));

        return $resultFactory->createFromQuery($provider, $modifier->process($query));
    }

    protected function getSearchKeywordsField()
    {
        $kw = (string) $this->request->query->get('keywords', '');
        return new KeywordsField($kw);
    }

    /** Routes */

    public function view()
    {
        // basic keyword search by default (so the box shows & works immediately)
        $query  = $this->getQueryFactory()->createQuery($this->getSearchProvider(), [$this->getSearchKeywordsField()]);
        $result = $this->createSearchResult($query);
        $this->renderSearchResult($result);
    }

    public function advanced_search()
    {
        // allows advanced fields & presets if you add more fields later
        $query  = $this->getQueryFactory()->createFromAdvancedSearchRequest($this->getSearchProvider(), $this->request, Request::METHOD_GET);
        $result = $this->createSearchResult($query);
        $this->renderSearchResult($result);
    }

    protected function renderSearchResult(Result $result)
    {
        $headerMenu   = $this->getHeaderMenu();
        $headerSearch = $this->getHeaderSearch();

        // prefill the header form with the current query (core does the same)
        $headerMenu->getElementController()->setQuery($result->getQuery());
        $headerSearch->getElementController()->setQuery($result->getQuery());

        $this->set('result', $result);
        $this->set('headerMenu', $headerMenu);
        $this->set('headerSearch', $headerSearch);

        // full-width dashboard template like core search pages
        $this->setThemeViewTemplate('full.php');
    }
}

# 2) Minimal SearchProvider for your “feeds”

Create a simple provider and a list that your result table can render. You can flesh this out later with columns/fields.

`packages/shopping_feed_manager/src/Search/Feed/SearchProvider.php`

<?php
namespace ConcretePackageShoppingFeedManagerSearchFeed;

use ConcreteCoreSearchProviderInterface;
use ConcreteCoreSearchFieldManager as FieldManager;
use ConcreteCoreSearchItemListPagerManagerPagerManagerInterface;
use ConcreteCoreSearchColumnSetDefaultSet as ColumnSet;
use ConcreteCoreSearchResultResultFactory;
use ConcreteCoreUrlResolverManagerResolverManagerInterface;

class SearchProvider implements ProviderInterface
{
    protected $fields;
    protected $columns;
    protected $list;
    protected $resolver;

    public function __construct(
        FieldManager $fields,
        ColumnSet $columns,
        FeedList $list,
        ResolverManagerInterface $resolver
    ) {
        $this->fields   = $fields;
        $this->columns  = $columns;
        $this->list     = $list;
        $this->resolver = $resolver;
    }

    public function getFieldManager()
    {
        return $this->fields;
    }

    public function getBaseColumnSet()
    {
        return $this->columns;
    }

    public function getDefaultColumnSet()
    {
        return $this->columns;
    }

    public function getAvailableColumnSet()
    {
        return $this->columns;
    }

    public function getItemList()
    {
        return clone $this->list;
    }

    public function getSearchResultFromQuery($query)
    {
        $factory = app(ResultFactory::class);
        return $factory->createFromQuery($this, $query);
    }

    public function getBaseURL()
    {
        // URL back to your single page
        return (string) $this->resolver->resolve(['/dashboard/shopping_feed/view']);
    }

    public function getItemsPerPage()
    {
        return 20; // default, can be overridden by request via ItemsPerPageRequestModifier
    }

    public function getPagerManager(): ?PagerManagerInterface
    {
        return null; // optional, default pager works fine
    }
}

`packages/shopping_feed_manager/src/Search/Feed/FeedList.php` (your data source; make it extend a DB ItemList or an in-memory one as you prefer)

<?php
namespace ConcretePackageShoppingFeedManagerSearchFeed;

use ConcreteCoreSearchItemListDatabaseItemList;

class FeedList extends ItemList
{
    protected function createQuery()
    {
        // Adjust to your table/joins
        $this->query->select('f.id, f.title, f.createdAt')
            ->from('Feeds', 'f');
    }

    public function filterByKeywords($kw)
    {
        if ($kw !== '') {
            $this->query->andWhere('f.title LIKE :kw');
            $this->query->setParameter('kw', '%' . $kw . '%');
        }
    }

    public function getResult($queryRow)
    {
        // Return a simple array row; core’s default table renderer can work with arrays
        return $queryRow;
    }

    public function getTotalResults()
    {
        $qb = clone $this->query;
        $qb->resetQueryParts(['select', 'orderBy'])
           ->select('COUNT(*)');
        return (int) $qb->execute()->fetchOne();
    }

    protected function finalizeQuery(DoctrineDBALQueryQueryBuilder $query)
    {
        // no-op
    }
}

`packages/shopping_feed_manager/src/Search/Feed/Columns.php` (very basic columns set)

<?php
namespace ConcretePackageShoppingFeedManagerSearchFeed;

use ConcreteCoreSearchColumnColumn;
use ConcreteCoreSearchColumnSetDefaultSet;

class Columns extends DefaultSet
{
    public function __construct()
    {
        $this->addColumn(new Column('id', t('ID'), 'f.id'));
        $this->addColumn(new Column('title', t('Title'), 'f.title'));
        $this->addColumn(new Column('createdAt', t('Created'), 'f.createdAt'));
        $this->setDefaultSortColumn($this->getColumnByKey('createdAt'), 'desc');
    }
}

`packages/shopping_feed_manager/src/Search/Feed/Fields.php` (only keywords for now)

<?php
namespace ConcretePackageShoppingFeedManagerSearchFeed;

use ConcreteCoreSearchFieldManager as FieldManager;
use ConcreteCoreSearchFieldFieldKeywordsField;

class Fields extends FieldManager
{
    public function __construct()
    {
        $this->addGroup(t('Basic'), [
            new KeywordsField(),
        ]);
    }
}

> **Autoload**: In your package controller (`controller.php`), make sure PSR-4 autoload includes `ConcretePackageShoppingFeedManagerSearchFeed` → `src/Search/Feed/`.

# 3) Header elements (menu + search)

(These mirror core’s pattern: element controller sets/receives `Query`, view outputs the form.)

**Element controllers**

`packages/shopping_feed_manager/controllers/element/shopping_feed/search/search.php`

<?php
namespace ConcretePackageShoppingFeedManagerControllerElementShoppingFeedSearch;

use ConcreteCoreControllerElementController;
use ConcreteCoreEntitySearchQuery;

class Search extends ElementController
{
    protected $query;

    public function getElement()
    {
        return 'shopping_feed/search/search';
    }

    public function setQuery(?Query $query = null)
    {
        $this->query = $query;
    }

    public function view()
    {
        $this->set('query', $this->query);
        $this->set('action', $this->controller->action('advanced_search'));
    }
}

`packages/shopping_feed_manager/controllers/element/shopping_feed/search/menu.php`

<?php
namespace ConcretePackageShoppingFeedManagerControllerElementShoppingFeedSearch;

use ConcreteCoreControllerElementController;
use ConcreteCoreEntitySearchQuery;

class Menu extends ElementController
{
    protected $query;

    public function getElement()
    {
        return 'shopping_feed/search/menu';
    }

    public function setQuery(?Query $query = null)
    {
        $this->query = $query;
    }

    public function view()
    {
        $this->set('query', $this->query);
        // add bulk actions/export buttons later if needed
    }
}

**Element views**

`packages/shopping_feed_manager/elements/shopping_feed/search/search.php`

<?php defined('C5_EXECUTE') or die('Access Denied.');
/** @var string $action */
$keywords = '';
if (isset($query)) {
    $fields = $query->getFields();
    foreach ($fields as $f) {
        if (method_exists($f, 'getData') && isset($f->getData()['keywords'])) {
            $keywords = (string) $f->getData()['keywords'];
        }
    }
}
?>
<form class="ccm-search-fields" method="get" action="<?= h($action) ?>">
  <div class="row g-2 align-items-center">
    <div class="col-auto">
      <input type="text" class="form-control" name="keywords" value="<?= h($keywords) ?>" placeholder="<?= t('Search') ?>">
    </div>
    <div class="col-auto">
      <button type="submit" class="btn btn-primary"><?= t('Search') ?></button>
    </div>
  </div>
</form>
<hr class="my-3">

`packages/shopping_feed_manager/elements/shopping_feed/search/menu.php`

<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>
<div class="btn-group">
  <!-- Put bulk/excel/export buttons here if/when you add them -->
</div>

# 4) Single page view: render header + results

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

<?php defined('C5_EXECUTE') or die('Access Denied.');
/** @var ConcreteCoreSearchResultResult $result */
/** @var ConcreteCoreFilesystemElement $headerMenu */
/** @var ConcreteCoreFilesystemElement $headerSearch */
?>

<div class="ccm-dashboard-header-buttons">
  <?= $headerMenu->render(); ?>
</div>

<div class="ccm-dashboard-content-inner">
  <?= $headerSearch->render(); ?>

  <?php
    // Standard search result table (uses your Columns + FeedList)
    $searchResultObject = $result->getSearchResultObject();
    echo $searchResultObject->render();

    // Standard pagination
    $list = $result->getItemListObject();
    $pagination = $list->getPagination();
    echo $pagination->renderDefaultView();
  ?>
</div>

# 5) Package autoloader (important)

In your `packages/shopping_feed_manager/controller.php`:

public function getPackageAutoloaderRegistries()
{
    return [
        'src' => 'ConcretePackageShoppingFeedManager',
    ];
}

…and ensure your `src/Search/Feed/*` classes match the namespace paths shown above.

## What you get

– A **Dashboard header search box** (styled/positioned like core lists).
– A **menu area** for actions (export/bulk, optional).
– Keyword search via `?keywords=…` out of the box.
– Core’s **sorting** (`ccm_order_by`, `ccm_order_dir`) and **itemsPerPage** supported automatically.
– A **standard Bootstrap table + pagination** rendered by the core search result object.

> From here you can add more search fields (dates, status, etc.) by extending `Fields` and updating `FeedList::filterBy…()` methods; they’ll appear in the advanced flow automatically.

If you paste this in and hit `/dashboard/shopping_feed`, you should see the header search and a results table. If your DB table/columns differ, just tweak `FeedList::createQuery()` and the `Columns` class.

User
we only want search
Assistant
Cool — here’s the **minimal “search-only” setup** for your Concrete CMS 9 single page. No custom provider/columns, just a header search box that sends `?keywords=` and you filter your list with it.

# 1) Controller (add keywords + headerSearch element)

`packages/shopping_feed_manager/controllers/single_page/dashboard/shopping_feed.php`

<?php
namespace ConcretePackageShoppingFeedManagerControllerSinglePageDashboard;

use ConcreteCoreFilesystemElementManager;
use ConcreteCorePageControllerDashboardPageController;

class ShoppingFeed extends DashboardPageController
{
    protected $headerSearch;

    protected function getHeaderSearch()
    {
        if (!isset($this->headerSearch)) {
            // points to packages/shopping_feed_manager/elements/shopping_feed/search/search.php
            $this->headerSearch = $this->app->make(ElementManager::class)->get('shopping_feed/search/search');
        }
        return $this->headerSearch;
    }

    public function view()
    {
        // read keywords from querystring
        $keywords = (string) $this->request->query->get('keywords', '');

        // pass to the header search element to prefill the input
        $headerSearch = $this->getHeaderSearch();
        $headerSearch->getElementController()->setKeywords($keywords);

        // TODO: filter YOUR list with $keywords (example below)
        $items = $this->getFilteredItems($keywords);

        $this->set('headerSearch', $headerSearch);
        $this->set('keywords', $keywords);
        $this->set('items', $items);

        // optional: makes it look like other dashboard search pages
        $this->setThemeViewTemplate('full.php');
    }

    /** Replace this with your actual query/list */
    protected function getFilteredItems(string $keywords): array
    {
        $conn = $this->app->make('database')->connection();
        $qb = $conn->createQueryBuilder();
        $qb->select('f.id', 'f.title', 'f.createdAt')
           ->from('Feeds', 'f');

        if ($keywords !== '') {
            $qb->andWhere('f.title LIKE :kw OR f.id LIKE :kw')
               ->setParameter('kw', '%' . $keywords . '%');
        }

        $qb->orderBy('f.createdAt', 'DESC');

        return $qb->executeQuery()->fetchAllAssociative();
    }
}

# 2) Tiny element controller for the header search

`packages/shopping_feed_manager/controllers/element/shopping_feed/search/search.php`

<?php
namespace ConcretePackageShoppingFeedManagerControllerElementShoppingFeedSearch;

use ConcreteCoreControllerElementController;

class Search extends ElementController
{
    protected $keywords = '';

    public function getElement()
    {
        // tells Concrete which element view to render
        return 'shopping_feed/search/search';
    }

    public function setKeywords(string $keywords = '')
    {
        $this->keywords = $keywords;
    }

    public function view()
    {
        $this->set('keywords', $this->keywords);
        // submit back to /dashboard/shopping_feed (view action)
        $this->set('action', $this->controller->action('view'));
    }
}

# 3) The element view (the actual search box)

`packages/shopping_feed_manager/elements/shopping_feed/search/search.php`

<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>
<form class="ccm-search-fields" method="get" action="<?= h($action) ?>">
  <div class="row g-2 align-items-center">
    <div class="col-auto">
      <input type="text"
             class="form-control"
             name="keywords"
             value="<?= h($keywords ?? '') ?>"
             placeholder="<?= t('Search') ?>">
    </div>
    <div class="col-auto">
      <button type="submit" class="btn btn-primary"><?= t('Search') ?></button>
    </div>
    <?php if (!empty($keywords)) { ?>
      <div class="col-auto">
        <a class="btn btn-outline-secondary" href="<?= h($action) ?>"><?= t('Clear') ?></a>
      </div>
    <?php } ?>
  </div>
</form>
<hr class="my-3">

# 4) Your single page view: render the header search, then your list

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

<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>
<div class="ccm-dashboard-content-inner">

  <?= $headerSearch->render(); ?>

  <?php if ($keywords): ?>
    <p class="text-muted mb-2"><?= t('Showing results for: "%s"', h($keywords)); ?></p>
  <?php endif; ?>

  <div class="table-responsive">
    <table class="table table-striped align-middle">
      <thead>
        <tr>
          <th><?= t('ID') ?></th>
          <th><?= t('Title') ?></th>
          <th><?= t('Created') ?></th>
        </tr>
      </thead>
      <tbody>
      <?php if (!empty($items)) { ?>
        <?php foreach ($items as $row) { ?>
          <tr>
            <td><?= h($row['id']) ?></td>
            <td><?= h($row['title']) ?></td>
            <td><?= h($row['createdAt']) ?></td>
          </tr>
        <?php } ?>
      <?php } else { ?>
          <tr><td colspan="3" class="text-muted"><?= t('No results found.') ?></td></tr>
      <?php } ?>
      </tbody>
    </table>
  </div>

</div>

## Notes

– This gives you the **dashboard-styled search header** only. No advanced fields, no presets.
– You control filtering in `getFilteredItems()` — swap in your ORM/ItemList if you already have one.
– If you later want pagination, just add `->setMaxResults()`/`->setFirstResult()` on the query (or switch to an `ItemList` and call `$list->getPagination()`).

That’s it — paste these in and you’ll have a clean Concrete CMS v9 search bar at the top of your page that filters your current listing.

User
Call to a member function setKeywords() on null
Assistant
Ah 👍 — that error means your `$headerSearch->getElementController()` is **returning null**, because the element didn’t have a controller bound yet.

In Concrete CMS, when you do

$headerSearch = $this->app->make(ElementManager::class)->get('shopping_feed/search/search');

you get an **Element** object. To talk to its controller, you need to call `$headerSearch->getElementController()` — but that controller will only exist if you actually created one under
`controllers/element/shopping_feed/search/search.php`.

### Quick Fix
If you don’t want to mess with a custom element controller at all, just pass the keyword directly as a variable:

**Controller**

public function view()
{
    $keywords = (string) $this->request->query->get('keywords', '');

    $headerSearch = $this->app->make(ConcreteCoreFilesystemElementManager::class)
        ->get('shopping_feed/search/search');

    // set view vars (no element controller needed)
    $headerSearch->set('keywords', $keywords);
    $headerSearch->set('action', $this->action('view'));

    $items = $this->getFilteredItems($keywords);

    $this->set('headerSearch', $headerSearch);
    $this->set('keywords', $keywords);
    $this->set('items', $items);

    $this->setThemeViewTemplate('full.php');
}

**Element view** (`elements/shopping_feed/search/search.php`) stays the same:

<form class="ccm-search-fields" method="get" action="<?= h($action) ?>">
  <div class="row g-2 align-items-center">
    <div class="col-auto">
      <input type="text" class="form-control"
             name="keywords"
             value="<?= h($keywords ?? '') ?>"
             placeholder="<?= t('Search') ?>">
    </div>
    <div class="col-auto">
      <button type="submit" class="btn btn-primary"><?= t('Search') ?></button>
    </div>
    <?php if (!empty($keywords)) { ?>
      <div class="col-auto">
        <a class="btn btn-outline-secondary" href="<?= h($action) ?>"><?= t('Clear') ?></a>
      </div>
    <?php } ?>
  </div>
</form>
<hr class="my-3">

### Why this works
– When you use `$element->set('var', $value)`, the variable is available in the element view directly.
– You don’t need a separate `controllers/element/…` class unless you want heavier logic.

👉 So, just remove the `$headerSearch->getElementController()->setKeywords()` line and replace with `$headerSearch->set('keywords', $keywords)` as above. That’ll fix the `Call to a member function setKeywords() on null` error.

Do you want me to show the **super minimal version** (without even an element controller file) so you just have controller + element view?

User
File /var/www/html/concrete/elements/shopping_feed/search/search.php does not exist
Assistant
You’re loading the element without telling Concrete which package it’s in, so it’s looking in `/concrete/elements/…` (core) and failing.

## Fix

1) **Put the element file here (inside your package):**

packages/shopping_feed_manager/elements/shopping_feed/search/search.php

2) **Load it with the package handle.** In your controller’s `view()`:

use ConcreteCoreFilesystemElementManager;

public function view()
{
    $keywords = (string) $this->request->query->get('keywords', '');

    $em = $this->app->make(ElementManager::class);

    // IMPORTANT: pass the package handle as the 2nd argument
    $headerSearch = $em->get('shopping_feed/search/search', 'shopping_feed_manager');

    // pass vars directly to the element (no element controller needed)
    $headerSearch->set('keywords', $keywords);
    $headerSearch->set('action', $this->action('view'));

    $items = $this->getFilteredItems($keywords);

    $this->set('headerSearch', $headerSearch);
    $this->set('keywords', $keywords);
    $this->set('items', $items);

    $this->setThemeViewTemplate('full.php');
}

3) **Element view (unchanged):**

<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>
<form class="ccm-search-fields" method="get" action="<?= h($action) ?>">
  <div class="row g-2 align-items-center">
    <div class="col-auto">
      <input type="text" class="form-control" name="keywords"
             value="<?= h($keywords ?? '') ?>" placeholder="<?= t('Search') ?>">
    </div>
    <div class="col-auto">
      <button type="submit" class="btn btn-primary"><?= t('Search') ?></button>
    </div>
    <?php if (!empty($keywords)) { ?>
      <div class="col-auto">
        <a class="btn btn-outline-secondary" href="<?= h($action) ?>"><?= t('Clear') ?></a>
      </div>
    <?php } ?>
  </div>
</form>
<hr class="my-3">

4) **Render it in your single page view:**

<?= $headerSearch->render(); ?>

5) **If still not found:** clear cache (Dashboard → System & Settings → Optimization → Clear Cache) or `./vendor/bin/concrete5 c5:clear-cache`.

That’s it—passing `'shopping_feed_manager'` makes Concrete look in your package’s `/elements/…` instead of core.

User
this is the
Assistant
Great — you already have keywords in your controller. To show the **Concrete-style header search** (simple `?keywords=`) without a custom element controller, do these two things:

## A) Update your controller (`shopping_feed.php`)

1) **Fix the COUNT when keywords are present** (you’re building the QB but not executing it).
2) **Load the header search element from your package and pass variables**.

use ConcreteCoreFilesystemElementManager;
// ...

public function view()
{
    /** @var Connection $db */
    $db = $this->app->make(Connection::class);
    ['page' => $page, 'perPage' => $perPage, 'offset' => $offset] = $this->getPagination();

    try {
        $keywords = (string) $this->request->query->get('keywords', '');

        // ---- TOTAL COUNT ----
        if ($keywords !== '') {
            $qbCount = $db->createQueryBuilder()
                ->select('COUNT(*)')
                ->from(self::T_CFG)
                ->where('name LIKE :kw OR feedSlug LIKE :kw OR url LIKE :kw')
                ->setParameter('kw', '%' . $keywords . '%');

            $total = (int) $qbCount->executeQuery()->fetchOne();
        } else {
            $total = (int) $db->createQueryBuilder()
                ->select('COUNT(*)')
                ->from(self::T_CFG)
                ->executeQuery()
                ->fetchOne();
        }

        // ---- ROWS ----
        $qb = $db->createQueryBuilder()
            ->select('*')
            ->from(self::T_CFG)
            ->orderBy('id', 'DESC')
            ->setFirstResult($offset)
            ->setMaxResults($perPage);

        if ($keywords !== '') {
            $qb->where('name LIKE :kw OR feedSlug LIKE :kw OR url LIKE :kw')
               ->setParameter('kw', '%' . $keywords . '%');
        }

        $feeds = $qb->executeQuery()->fetchAllAssociative();

        // ---- HEADER SEARCH ELEMENT (from your package) ----
        $em = $this->app->make(ElementManager::class);
        $headerSearch = $em->get('shopping_feed/search/search', 'shopping_feed_manager'); // <== note package handle
        $headerSearch->set('keywords', $keywords);
        $headerSearch->set('action', $this->action('view'));

        // ---- PASS TO VIEW ----
        $this->set('feeds', $feeds);
        $this->set('total', $total);
        $this->set('page', $page);
        $this->set('limit', $perPage);
        $this->set('keywords', $keywords);
        $this->set('headerSearch', $headerSearch);

        // Optional: make page layout match other dashboard search pages
        $this->setThemeViewTemplate('full.php');

    } catch (Exception $e) {
        $this->error->add($e->getMessage());
    }
}

## B) Add the element view file

Create this file in your package:

packages/shopping_feed_manager/elements/shopping_feed/search/search.php
<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>
<form class="ccm-search-fields" method="get" action="<?= h($action) ?>">
  <div class="row g-2 align-items-center">
    <div class="col-auto">
      <input type="text"
             class="form-control"
             name="keywords"
             value="<?= h($keywords ?? '') ?>"
             placeholder="<?= t('Search') ?>">
    </div>
    <div class="col-auto">
      <button type="submit" class="btn btn-primary"><?= t('Search') ?></button>
    </div>
    <?php if (!empty($keywords)) { ?>
      <div class="col-auto">
        <a class="btn btn-outline-secondary" href="<?= h($action) ?>"><?= t('Clear') ?></a>
      </div>
    <?php } ?>
  </div>
</form>
<hr class="my-3">

## C) Use it in your single page view

In `packages/shopping_feed_manager/single_pages/dashboard/shopping_feed/view.php`, render the search header above your table:

<?= $headerSearch->render(); ?>

<?php if ($keywords) { ?>
  <p class="text-muted mb-2"><?= t('Showing results for: "%s"', h($keywords)); ?></p>
<?php } ?>

<!-- your table/list markup follows -->

### That’s it
– No element controller class needed.
– Concrete will now look in your package’s `/elements/shopping_feed/search/search.php` because you passed the **package handle** to `ElementManager::get(…)`.
– The *COUNT with keywords* bug is fixed with `executeQuery()->fetchOne()`.

User
Call to a member function render() on null
Assistant
That means `$headerSearch` is **null** when your view tries to do “. In practice, that happens when the element can’t be found/loaded.

Do these 3 quick fixes:

## 1) Load the element safely (controller)
Make sure you pass the **package handle** and fall back if anything goes wrong:

use ConcreteCoreFilesystemElementManager;

public function view()
{
    // ... your existing code above ...

    $em = $this->app->make(ElementManager::class);

    // Try in your package first (handle = folder name)
    $headerSearch = $em->get('shopping_feed/search/search', 'shopping_feed_manager');

    // Fallback: try application or core path (optional)
    if (!$headerSearch) {
        $headerSearch = $em->get('shopping_feed/search/search');
    }

    // Pass vars if element resolved
    if ($headerSearch) {
        $headerSearch->set('keywords', (string) $this->request->query->get('keywords', ''));
        $headerSearch->set('action', $this->action('view'));
    }

    $this->set('headerSearch', $headerSearch);

    // ... rest of your sets & template ...
}

## 2) Guard in the single page view
Avoid calling `render()` on null:

<?php if ($headerSearch) { echo $headerSearch->render(); } ?>

## 3) Verify the element file path & name (Linux is case-sensitive)
The file **must** exist exactly here:

packages/shopping_feed_manager/elements/shopping_feed/search/search.php

…and contain your form. Example:

<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>
<form class="ccm-search-fields" method="get" action="<?= h($action) ?>">
  <div class="row g-2 align-items-center">
    <div class="col-auto">
      <input type="text" class="form-control" name="keywords"
             value="<?= h($keywords ?? '') ?>" placeholder="<?= t('Search') ?>">
    </div>
    <div class="col-auto">
      <button type="submit" class="btn btn-primary"><?= t('Search') ?></button>
    </div>
    <?php if (!empty($keywords)) { ?>
      <div class="col-auto">
        <a class="btn btn-outline-secondary" href="<?= h($action) ?>"><?= t('Clear') ?></a>
      </div>
    <?php } ?>
  </div>
</form>
<hr class="my-3">

### Common gotchas to double-check
– Folder name (package handle) must match exactly: `shopping_feed_manager`.
– Path segments are all **lowercase**: `elements/shopping_feed/search/search.php`.
– Clear cache after adding the file: Dashboard → System & Settings → Optimization → Clear Cache (or `./vendor/bin/concrete5 c5:clear-cache`).

Once the element resolves (non-null), the error disappears and the header search will render normally.

User
okay now its working but showing two times
Assistant
Nice! If the search bar shows **twice**, it’s almost always because it’s being rendered in two places.

Do these quick checks and fixes:

1) **Make sure you only render it once in the view.**
Open `packages/shopping_feed_manager/single_pages/dashboard/shopping_feed/view.php` and keep just one of these—not both:

<?= $headerSearch->render(); ?>

**or**

<?php
View::element(
  'shopping_feed/search/search',
  ['keywords' => $keywords, 'action' => $this->action('view')],
  'shopping_feed_manager'
);
?>

2) **Don’t render the element both via `$headerSearch->render()` and `View::element(…)`.**
Search your view for `shopping_feed/search/search` or `ccm-search-fields` to ensure it’s not included twice.

3) **Confirm you’re not also outputting a hardcoded form.**
Sometimes there’s a copy of the form markup directly in `view.php` *and* the element is rendered—remove one.

4) **Controller is fine** (setting `$headerSearch` and `setThemeViewTemplate('full.php')` won’t duplicate anything by themselves).

If you want a clean, minimal setup (single render), here’s the safe pattern:

**Controller**

$em = $this->app->make(ConcreteCoreFilesystemElementManager::class);
$headerSearch = $em->get('shopping_feed/search/search', 'shopping_feed_manager');
$headerSearch->set('keywords', $keywords);
$headerSearch->set('action', $this->action('view'));
$this->set('headerSearch', $headerSearch);

**View (`view.php`)**

<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>
<div class="ccm-dashboard-content-inner">
  <?= $headerSearch->render(); ?>   <!-- Only once -->
  <!-- your table/list below -->
</div>

After removing the duplicate include, clear cache if needed (Dashboard → System & Settings → Optimization → Clear Cache).

User
$em = $this->app->make(ConcreteCoreFilesystemElementManager::class);
$headerSearch = $em->get('shopping_feed/search/search', 'shopping_feed_manager');

$headerSearch->set('keywords', $keywords);
$headerSearch->set('action', $this->action('view'));
$this->set('headerSearch', $headerSearch);
keywords not getting fetched in search form even search is working fine

Assistant
Got it—your list is filtering, but the input isn’t prefilled. That just means the element view isn’t seeing the `keywords` var you set. Easiest, bullet-proof fix: have the element read from the current request (fallback to the var you pass).

In `packages/shopping_feed_manager/elements/shopping_feed/search/search.php`, change the top to:

<?php defined('C5_EXECUTE') or die('Access Denied.'); 
$app = ConcreteCoreSupportFacadeApplication::getFacadeApplication();
$req = $app->make('request');
$currentKeywords = isset($keywords) ? $keywords : (string) $req->query->get('keywords', '');
?>
<form class="ccm-search-fields" method="get" action="<?= h($action) ?>">
  <div class="row g-2 align-items-center">
    <div class="col-auto">
      <input type="text"
             class="form-control"
             name="keywords"
             value="<?= h($currentKeywords) ?>"
             placeholder="<?= t('Search') ?>">
    </div>
    <div class="col-auto">
      <button type="submit" class="btn btn-primary"><?= t('Search') ?></button>
    </div>
    <?php if ($currentKeywords !== '') { ?>
      <div class="col-auto">
        <a class="btn btn-outline-secondary" href="<?= h($action) ?>"><?= t('Clear') ?></a>
      </div>
    <?php } ?>
  </div>
</form>
<hr class="my-3">

This works whether:
– you pass `$headerSearch->set('keywords', $keywords)`, **or**
– you forget to pass it—the element will still pull `?keywords=` from the URL.

If it still doesn’t show:
– Ensure you’re not rendering the element **somewhere else** with `View::element('shopping_feed/search/search', [], 'shopping_feed_manager')` (empty var array would wipe it).
– Clear cache (Dashboard → System & Settings → Optimization → Clear Cache).

User
still not work
Assistant
No worries—let’s make the input **always** prefill, regardless of what gets passed in.

### 1) Use the request directly inside the element
Edit `packages/shopping_feed_manager/elements/shopping_feed/search/search.php` to this **exact** top block + input:

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

// Always read from the current GET request; fall back to a passed $keywords var if present.
$req = app('request'); // same as $this->app->make('request')
$currentKeywords = (string) $req->query->get('keywords', isset($keywords) ? (string)$keywords : '');
?>
<form class="ccm-search-fields" method="get" action="<?= h($action) ?>">
  <div class="row g-2 align-items-center">
    <div class="col-auto">
      <input type="text"
             class="form-control"
             name="keywords"
             value="<?= h($currentKeywords) ?>"
             placeholder="<?= t('Search') ?>">
    </div>
    <div class="col-auto">
      <button type="submit" class="btn btn-primary"><?= t('Search') ?></button>
    </div>
    <?php if ($currentKeywords !== '') { ?>
      <div class="col-auto">
        <a class="btn btn-outline-secondary" href="<?= h($action) ?>"><?= t('Clear') ?></a>
      </div>
    <?php } ?>
  </div>
</form>
<hr class="my-3">

### 2) Make sure you render it only once
In your `view.php`, keep only:

<?= $headerSearch->render(); ?>

Remove any `View::element('shopping_feed/search/search', …)` duplicates or any hardcoded copy of the form.

### 3) Controller is fine
This is OK:

$em = $this->app->make(ConcreteCoreFilesystemElementManager::class);
$headerSearch = $em->get('shopping_feed/search/search', 'shopping_feed_manager');
$headerSearch->set('keywords', $keywords);
$headerSearch->set('action', $this->action('view'));
$this->set('headerSearch', $headerSearch);

### 4) Clear cache
Dashboard → System & Settings → Optimization → **Clear Cache**
(or run `./vendor/bin/concrete5 c5:clear-cache`)

### 5) (Optional) Quick sanity check
Temporarily add this line right under the “ in the element:

<!-- debug: kw="<?= h($currentKeywords) ?>" -->

Submit a search and view source—you should see the keyword echoed in the HTML comment.

If it’s still blank after these steps, it means **something else is rendering the element again without vars**. Search your `view.php` for another include of the same element and remove it.

User
every thing is fine buto still not wor
Assistant
Gotcha—since search is working but the input won’t prefill, the simplest way to end the mystery is to **skip the element** and render the search box directly in your single page view using the same `$keywords` you already set in the controller.

This avoids any caching/element-path/variable-scope weirdness and guarantees the value shows.

## 1) Controller (you already have this)
Make sure you set `$keywords`:

$keywords = (string) $this->request->query->get('keywords', '');
$this->set('keywords', $keywords);

## 2) View (`packages/shopping_feed_manager/single_pages/dashboard/shopping_feed/view.php`)
Put the form at the top and bind the value directly:

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

<div class="ccm-dashboard-content-inner">

  <form class="ccm-search-fields" method="get" action="<?= h($this->action('view')) ?>">
    <div class="row g-2 align-items-center">
      <div class="col-auto">
        <input type="text"
               class="form-control"
               name="keywords"
               value="<?= h((string)($keywords ?? '')) ?>"
               placeholder="<?= t('Search') ?>">
      </div>
      <div class="col-auto">
        <button type="submit" class="btn btn-primary"><?= t('Search') ?></button>
      </div>
      <?php if (!empty($keywords)) { ?>
        <div class="col-auto">
          <a class="btn btn-outline-secondary" href="<?= h($this->action('view')) ?>"><?= t('Clear') ?></a>
        </div>
      <?php } ?>
    </div>
  </form>
  <hr class="my-3">

  <?php if (!empty($keywords)) { ?>
    <p class="text-muted mb-2"><?= t('Showing results for: "%s"', h($keywords)); ?></p>
  <?php } ?>

  <!-- your table/list rendering below -->

</div>

That’s it. If this shows the value (it will), the issue was element scoping/caching. You can keep it like this (clean and “Concrete-ish” styling) or circle back to the element later.

User
yes that working on custo m form in view.php
Assistant
Perfect 👍
That confirms the problem wasn’t your query logic — it was just the way the element was being resolved / passed vars. By writing the form directly in `view.php` you’ve bypassed all the scope/caching headaches and you now have:

– A working search input,
– Prefilled keyword value,
– Clean listing filtered in your controller.

This is a perfectly valid approach for a package single page. In Concrete CMS you don’t *have* to use an element unless you want to share the same form in multiple places.

👉 If you want to refactor later into a reusable element (say you’ll reuse the search box across different views), the trick is: **always pass the variables array into `View::element()`** like this:

View::element(
  'shopping_feed/search/search',
  ['keywords' => $keywords, 'action' => $this->action('view')],
  'shopping_feed_manager'
);

That guarantees `$keywords` and `$action` show up in the element’s scope.
But since your single page only needs the form once, your current solution in `view.php` is totally fine.

Would you like me to show you a **minimal reusable element version** (so you can drop it anywhere later), or are you happy to keep the inline form in your view?

User
where i need to change code
Assistant
👌 Got it. Since your **inline form in `view.php` works**, you don’t *need* to change anything further if you’re happy to leave the form there.

But if you want to move back to an **element** (so the search box is reusable and cleaner), here’s exactly what to change:

### 1. Controller (`shopping_feed.php`)

Instead of building the element with `ElementManager`, just use `View::element` (because `View::element` lets you pass variables in directly).
So **remove** this block:

$em = $this->app->make(ConcreteCoreFilesystemElementManager::class);
$headerSearch = $em->get('shopping_feed/search/search', 'shopping_feed_manager');
$headerSearch->set('keywords', $keywords);
$headerSearch->set('action', $this->action('view'));
$this->set('headerSearch', $headerSearch);

…and just set `$keywords`:

$this->set('keywords', $keywords);

### 2. Element call in `view.php`

At the top of your `view.php`, **replace** this:

<?= $headerSearch->render(); ?>

with:

<?php
View::element(
  'shopping_feed/search/search', 
  [
    'keywords' => $keywords,
    'action'   => $this->action('view'),
  ], 
  'shopping_feed_manager'
);
?>

### 3. Element file

In `packages/shopping_feed_manager/elements/shopping_feed/search/search.php`, use the passed-in vars:

<?php defined('C5_EXECUTE') or die('Access Denied.'); ?>
<form class="ccm-search-fields" method="get" action="<?= h($action) ?>">
  <div class="row g-2 align-items-center">
    <div class="col-auto">
      <input type="text"
             class="form-control"
             name="keywords"
             value="<?= h((string)($keywords ?? '')) ?>"
             placeholder="<?= t('Search') ?>">
    </div>
    <div class="col-auto">
      <button type="submit" class="btn btn-primary"><?= t('Search') ?></button>
    </div>
    <?php if (!empty($keywords)) { ?>
      <div class="col-auto">
        <a class="btn btn-outline-secondary" href="<?= h($action) ?>"><?= t('Clear') ?></a>
      </div>
    <?php } ?>
  </div>
</form>
<hr class="my-3">

✅ Now the element will always get `keywords` + `action` injected properly, and your input will prefill.
No need for `ElementManager::get()` anymore.

👉 So the only code you really need to **change** is:
– **Controller** → remove the `ElementManager` part.
– **View** → replace `$headerSearch->render()` with a `View::element(…)` call that passes the vars.

Do you want me to show you the **full updated controller** so you can just copy-paste it?

User
but now its place is change from default postion
Assistant
Gotcha — the element renders now, but it’s sitting in the “wrong” spot. You can put it exactly where Concrete’s core lists place their search by doing two tiny tweaks:

## 1) Make sure you’re using the wide dashboard template
In your controller (`shopping_feed.php`) keep this (you already had it earlier):

$this->setThemeViewTemplate('full.php');

## 2) Move the element to the standard slot in `view.php`
Core pages put the search **at the very top of the content area** (inside `.ccm-dashboard-content-inner`), and keep any action buttons in `.ccm-dashboard-header-buttons`.

Here’s the structure to copy:

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

<!-- Optional: header buttons go here (export, add new, etc.) -->
<div class="ccm-dashboard-header-buttons">
  <?php /* Example:
  <a href="<?= h($this->action('add')) ?>" class="btn btn-primary"><?= t('Add Feed') ?></a>
  */ ?>
</div>

<div class="ccm-dashboard-content-inner">

  <?php
  // Put the search RIGHT HERE to match core lists
  View::element(
    'shopping_feed/search/search',
    [
      'keywords' => $keywords,
      'action'   => $this->action('view'),
    ],
    'shopping_feed_manager'
  );
  ?>

  <!-- Your list/table comes immediately after the search -->
  <?php if (!empty($keywords)) { ?>
    <p class="text-muted mb-2"><?= t('Showing results for: "%s"', h($keywords)); ?></p>
  <?php } ?>

  <!-- table/list markup ... -->

</div>

### Why this fixes the position
– `full.php` gives you the same layout core search pages use.
– Placing the element as the **first thing inside `.ccm-dashboard-content-inner`** puts it in the default “search bar” position (under the page title, above the results).
– Only **buttons** belong in `.ccm-dashboard-header-buttons`; the search stays in the content area.

If you still see it shifted, check for extra wrappers (cards/containers) above it. The search element should not be wrapped in additional panels — just drop it exactly as shown.

Leave a Comment

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