OctoberCMS - Relation lists with filters [HOWTO]

In more advanced projects you will soon realize that relation lists/forms and in general the whole RelationController is lacking funcionality. One of those things that are missing are filters in the relation list. But fear not, you can render lists and forms manually and then you can add filters to it. The best place to start with manual lists are these two tutorials: https://octobercms.com/support/article/ob-21
https://octobercms.com/support/article/ob-20

But they only cover how to make a simple list rendered by hand in a partial and without filters. What I will cover in this article is how to do the same but using ListController (to render the list with filters for us automatically).

Once you know the formula this is a pretty easy process. But I recon that getting there by yourself can be a painfull process (it was for me). After seeing the tutorial videos you would probably dive into the Behaviours and ListController to see how October does it because the documentation is still lacking. But it also has some good sides, you need to consciously code your plugins, you can't just paste random code form internet and make a plugin out of it. In other words, your code quality will be by default higher than code for other leading CMS platforms :)

But let's get to the point. Let's say we have Order controller and model, then we have Product controller and model, both are glued together by Many to Many relationship with some pivot data. You will soon realize that when adding products to order manually (after reaching about 100 products) it gets really annoying to scroll through list of products to get those you want to add to order. Yeah you have search but sometimes you don't remeber the name, or you just want to browse given product category or color or anything like that. List filter would come handy here. Below are the steps needed to take to achieve that:

  1. Add custom button to relation toolbar to have Ajax handler that will render the custom list. We will remove the default Add Product button(rendered by RelationController) and put a custom Add Product button.
  2. We need custom Products list widget to display list of products
  3. We need to attach filter to Products list widget
  4. As an option we need to use a query scope to show, lets say only active products.

STEP 1. Edit Controller/Orders/config_relation.yaml Your toolbarButtons declaration for products relation probably looks like that:


toolbarButtons: add | remove
Like I said before we want to use custom add button. Lets swap the default add button for a custom button. I will call it "productsadd" The line will look like this:

toolbarButtons: productsadd | remove
Now we need to put the code for the custom button somewhere, October is really making this easy for us. The only thing we need to do is to create a file called _relation_button_productsadd.htm in Controller/orders directory.

This is how my file looks like:


<button
class="btn btn-secondary oc-icon-plus"
data-control="popup"
data-handler="onAddProduct"
data-size="large">
Add Product

Two most important lines here are:

data-control="popup"

This will open the relation list in the modal window.


data-handler="onAddProduct"

This is our Ajax Handler to display custom list. We need to add a function in our Orders controller to handle it. Lets go to Controllers/Orders.php, but before we will add this action we should do some other things too. I will put it all in one file with comments explaning the lines of code we will add. Bear in mind that this is not the complete Orders.php controller file. Those are mostly only the lines of code you need to add.


[...]
# List and Filter widgets variables, name them as you want :)
protected $productsListWidget;
protected $productsFilterWidget;

[...] public function construct() { parent::construct(); BackendMenu::setContext('Redmarlin.ShopClerk', 'Shop', 'Orders');

    #We need to create Products List Widget
    $this->productsListWidget = $this->createProductListWidget();
}

[...] # This is Ajax Handler invoked when clickin on "Add Product" button. What it does is to just assign # previously created widgets to variables that are accessible from partials.

public function onAddProduct() {

    $this->vars['ProductListWidget'] = $this->ProductListWidget;

    #Variable necessary for the Filter funcionality
    $this->vars['ProductFilterWidget'] = $this->ProductFilterWidget;

    #Process the custom list partial, The name you choose here will be the partials file name
    return $this->makePartial('product_custom_list');

}

# Ahhh finally there, the most important part, here we declare all the necessary # things to make List widget with filters happen.

protected function createProductListWidget () {

    #First we need config for the list, as described in video tutorials mentioned at the beginning.
    # Specify which list configuration file use for this list
    $config = $this->makeConfig('$/redmarlin/shopclerk/models/product/columns_relation.yaml');

    # Specify the List model
    $config->model = New \Redmarlin\ShopClerk\Models\Product ;

    # Lets configure some more things like report per page and lets show checkboxes on the list.
    # Most of the options mentioned in https://octobercms.com/docs/backend/lists#configuring-list # will work
    $config->recordsPerPage = '30';
    $config->showCheckboxes = 'true';

    # Here we will actually make the list using Lists Widget
    $widget_product = $this->makeWidget('Backend\Widgets\Lists', $config);

    #For the optional Step 4. Alter product list query before displaying it.
    # We will bind to list.extendQuery event and assign a function that should be executed to extend
    # the query (the function is defined in this very same controller file)
    $widget_product->bindEvent('list.extendQuery', function ($query) {
        $this->productExtendQuery($query);
    });

    # Step 3. The filter part, we must define the config, really similar to the Product list widget config
    # Filter configuration file
    $filterConfig = $this->makeConfig('$/redmarlin/shopclerk/models/product/filter_relation.yaml');

    # Use Filter widgets to make the widget and bind it to the controller
    $filterWidget = $this->makeWidget('Backend\Widgets\Filter', $filterConfig);
    $filterWidget->bindToController();

    # We need to bind to filter.update event in order to refresh the list after selecting 
    # the desired filters.
    $filterWidget->bindEvent('filter.update', function () use ($widget_product, $filterWidget) {
            return $widget_product->onRefresh();
        });

    #Finally we are attaching The Filter widget to the Product widget.
    $widget_product->addFilter([$filterWidget, 'applyAllScopesToQuery']);

    $this->productFilterWidget = $filterWidget;

    # Dont forget to bind the whole thing to the controller
    $widget_product->bindToController();

    #Return the prepared widget object
    return $widget_product;

}

# Function that will extend default Product query and only show active products 

     public function productExtendQuery($query)
{
    $query->where('status','active');
}


That is basically all that is needed in the Orders controller. But we are still a few things short. We need a partial that we have declared in our Ajax Handler (onAddProduct) - "product_custom_list".

Create a file _product_custom_list.htm in Controllers/orders/ directory. The code in this file is basically copied from the RelationController partial for managing pivot relation (modules/backend/behaviors/relationcontroller/partials/_manage_pivot.htm). If you need code for other relation type just copy appropriate file from RelationController dir and then modify it to suit your needs. In the first line, by using the data-request-data we are telling relation controller what relation we are displaying here. Apart from that we are rendering Filter and List widget.

I have also customized a few other things here like: removed search widget and removed parts I wont use (ie the list will be always rendered with checkboxes).

<div id="relationManagePopup" data-request-data="_relation_field: 'product'">
    <?= Form::open() ?>
        <div class="modal-header">
            <button type="button" class="close" data-dismiss="popup">×</button>
            <h4 class="modal-title">Product Selection List</h4>
        </div>
        <div class="list-flush">
            <?php if ($productFilterWidget): ?>
                <?= $productFilterWidget->render() ?>
            <?php endif ?>
        </div>

        <?= $productListWidget->render() ?>

        <div class="modal-footer">
            <button
                    type="button"
                    class="btn btn-primary"
                    data-control="popup"
                    data-handler="onRelationManageAddPivot"
                    data-size="huge"
                    data-dismiss="popup"
                    data-stripe-load-indicator>
                    <?= e(trans('backend::lang.relation.add_selected')) ?>
            </button>
            <button
                type="button"
                class="btn btn-default"
                data-dismiss="popup">
                <?= e(trans('backend::lang.relation.cancel')) ?>
            </button>
        </div>
    <?= Form::close() ?>
</div>
<script>
    setTimeout(
            function(){ $('#relationManagePivotPopup input.form-control:first').focus() },
            310
    )
</script>

If you need search widget you need to add it the same way we added Filter widget.

With this we can render Products list with working filters in the Orders update/create screen as relation. After choosing Product from the list a pivot create form will be shown.

But there is still a tiny detail we should take care of. When using group type filter the dropdown list will be shown below our modal window. In other words it will be invisible!!! You can fix it with just one line of css. You need to change z-index of "control-popover" class to show it above the modal window. something like:


div.control-popover {
z-index: 9999;
}
will do. Then I simply injected css file from plugin/assets/backend_mods.css into Orders controller. But you can inject it globally in the Plugin.php. This way you don't need to add it in every controller.

That's it, I hope you'll find this tutorial helpful. Let me know if I got something wrong or something is not clear enough.

blog comments powered by Disqus