Skip to content
mFilter
mFilter
Faceted filtering for MODX 3 with SEO URL support
  • MODX 3
  • PHP 8.1
  • Vue 3
  1. Extras
  2. mFilter
  3. Cookbook
  4. External filters

External filters ​

Placing individual filters outside the main form.

Task ​

Place some filters (e.g. sort or quick filters) separately from the main filter form.

Solution ​

Use the JavaScript API to control filters from anywhere on the page.

Example: Sort in header ​

HTML ​

html
<!-- Catalog header (outside form) -->
<div class="catalog-header">
    <div class="catalog-header__sort">
        <label>Sort:</label>
        <select id="external-sort">
            <option value="pagetitle-asc">By name (A–Z)</option>
            <option value="pagetitle-desc">By name (Z–A)</option>
            <option value="Data.price-asc">Price: low to high</option>
            <option value="Data.price-desc">Price: high to low</option>
            <option value="publishedon-desc">Newest first</option>
        </select>
    </div>

    <div class="catalog-header__total">
        Found: <span data-mfilter-total></span>
    </div>
</div>

<!-- Main form in sidebar -->
<aside class="catalog-sidebar">
    [[!mFilterForm]]
</aside>

<!-- Results -->
<main class="catalog-content">
    <div data-mfilter-results>
        [[!mFilter? ...]]
    </div>
</main>

JavaScript ​

javascript
document.addEventListener('DOMContentLoaded', function() {
    const sortSelect = document.getElementById('external-sort');

    sortSelect.addEventListener('change', function() {
        const [field, dir] = this.value.split('-');
        const mfilter = window.mFilter.getInstance();

        // Set sort via tech params
        mfilter.setTechParam('sort', this.value);
        mfilter.submit();
    });

    // Sync on load
    document.addEventListener('mfilter:success', function(e) {
        if (e.detail.tech?.sort) {
            sortSelect.value = e.detail.tech.sort;
        }
    });
});

Example: Quick filter buttons ​

HTML ​

html
<!-- Quick filters (outside form) -->
<div class="quick-filters">
    <button class="quick-filter" data-filter="new" data-value="1">
        New
    </button>
    <button class="quick-filter" data-filter="popular" data-value="1">
        Popular
    </button>
    <button class="quick-filter" data-filter="sale" data-value="1">
        On sale
    </button>
</div>

<!-- Main form -->
[[!mFilterForm]]

JavaScript ​

javascript
document.querySelectorAll('.quick-filter').forEach(button => {
    button.addEventListener('click', function() {
        const key = this.dataset.filter;
        const value = this.dataset.value;
        const mfilter = window.mFilter.getInstance();

        // Toggle filter
        const current = mfilter.getState().filters[key];

        if (current && current.includes(value)) {
            mfilter.removeFilter(key);
            this.classList.remove('active');
        } else {
            mfilter.setFilter(key, [value]);
            this.classList.add('active');
        }

        mfilter.submit();
    });
});

// Sync button state
document.addEventListener('mfilter:success', function(e) {
    document.querySelectorAll('.quick-filter').forEach(button => {
        const key = button.dataset.filter;
        const value = button.dataset.value;
        const active = e.detail.filters[key]?.includes(value);
        button.classList.toggle('active', active);
    });
});

Example: Search by name ​

HTML ​

html
<!-- Search outside form -->
<div class="catalog-search">
    <input type="text" id="catalog-search-input" placeholder="Search products...">
    <button id="catalog-search-btn">Search</button>
</div>

[[!mFilterForm]]

JavaScript ​

javascript
const searchInput = document.getElementById('catalog-search-input');
const searchBtn = document.getElementById('catalog-search-btn');

function doSearch() {
    const query = searchInput.value.trim();
    const mfilter = window.mFilter.getInstance();

    if (query) {
        mfilter.setFilter('pagetitle', query);
    } else {
        mfilter.removeFilter('pagetitle');
    }

    mfilter.submit();
}

searchBtn.addEventListener('click', doSearch);
searchInput.addEventListener('keypress', function(e) {
    if (e.key === 'Enter') {
        doSearch();
    }
});

Example: Price range in separate block ​

HTML ​

html
<!-- Separate price block -->
<div class="price-filter-external">
    <h4>Price</h4>
    <div class="price-inputs">
        <input type="number" id="price-min" placeholder="From">
        <span>—</span>
        <input type="number" id="price-max" placeholder="To">
    </div>
    <button id="price-apply">Apply</button>
</div>

<!-- Form without price filter -->
[[!mFilterForm?
    &filters=`vendor,color,size`
]]

JavaScript ​

javascript
const priceMin = document.getElementById('price-min');
const priceMax = document.getElementById('price-max');
const priceApply = document.getElementById('price-apply');

priceApply.addEventListener('click', function() {
    const mfilter = window.mFilter.getInstance();

    const min = priceMin.value ? parseInt(priceMin.value) : null;
    const max = priceMax.value ? parseInt(priceMax.value) : null;

    if (min !== null || max !== null) {
        mfilter.setFilter('price', {
            min: min,
            max: max
        });
    } else {
        mfilter.removeFilter('price');
    }

    mfilter.submit();
});

// Sync on load and after AJAX
document.addEventListener('mfilter:success', function(e) {
    const price = e.detail.filters.price;
    if (price) {
        priceMin.value = price.min || '';
        priceMax.value = price.max || '';
    } else {
        priceMin.value = '';
        priceMax.value = '';
    }
});

Example: Vendor in dropdown menu ​

HTML ​

html
<!-- Category menu with vendors -->
<nav class="category-menu">
    <div class="category-menu__item">
        <a href="/catalog/electronics/">Electronics</a>
        <ul class="vendor-submenu">
            <li><a href="#" data-vendor="apple">Apple</a></li>
            <li><a href="#" data-vendor="samsung">Samsung</a></li>
            <li><a href="#" data-vendor="xiaomi">Xiaomi</a></li>
        </ul>
    </div>
</nav>

JavaScript ​

javascript
document.querySelectorAll('[data-vendor]').forEach(link => {
    link.addEventListener('click', function(e) {
        e.preventDefault();

        const vendor = this.dataset.vendor;
        const mfilter = window.mFilter.getInstance();

        // Reset all filters and set only vendor
        mfilter.reset();
        mfilter.setFilter('vendor', [vendor]);
        mfilter.submit();
    });
});

Syncing with the form ​

If a filter exists both in the form and outside, sync their state:

javascript
document.addEventListener('mfilter:success', function(e) {
    // Update external elements
    syncExternalFilters(e.detail.filters);

    // Update counters
    document.querySelectorAll('[data-mfilter-total]').forEach(el => {
        el.textContent = e.detail.total;
    });
});

function syncExternalFilters(filters) {
    // Sync sort select
    const sortSelect = document.getElementById('external-sort');
    if (sortSelect && filters.sort) {
        sortSelect.value = filters.sort;
    }

    // Sync quick filters
    document.querySelectorAll('.quick-filter').forEach(btn => {
        const key = btn.dataset.filter;
        const value = btn.dataset.value;
        btn.classList.toggle('active', filters[key]?.includes(value));
    });
}

Tips ​

  1. Use events — subscribe to mfilter:success for syncing
  2. Avoid duplication — if the filter is in the form, hide it and control via JS
  3. Preserve UX — external filters should visually reflect current state
  4. Test without JS — provide fallback for SEO and accessibility