Skip to content

Yummy Search

Chris Nizzardini edited this page Sep 7, 2019 · 74 revisions

Welcome to the YummySearch documentation. This is a search component designed to work with the CakePHP Paginator Component and Cake\ORM\Query. It supports searching the base model and other associations such as HasOne, BelongsTo, and HasMany. Other Associations have not been tested yet.

You must use Cake Query Builder. It was much easier to search on deep associations using matching over using the simple paginator array format. It should work with any database that CakePHP supports. To date, Yummy Search has only been tested with MySQL/MariaDb.

Configuration (3 easy steps)

Step 1: Load YummySearch in any method where you want to add search functionality to your paginated table.

Example:

// Load component
$this->loadComponent('Yummy.YummySearch',[
    'query' => $query,
    'allow' => [
        'Customers.email' => ['name' => 'Customer Email Address'],
        'Items.name' => ['name' => 'Item'],
    ]
]);

// Your query doesn't change
$query = $this->Invoices->find()->contain([
    'Customers',
    'Items' => [
        'ItemsCost'
    ]
]);

// Call YummySearch
$q = $this->YummySearch->search($query);
    
// Pass the returned Cake Query object into Paginator
$results = $this->paginate($q);

Step 2: In your view file call the Yummy Search Helper to display the search UI.

Example:

<?php
    $this->helpers()->load('Yummy.YummySearch');
    echo $this->YummySearch->basicForm();
?>

Last Step: Finally you will need to include a small JavaScript file, this is generic JS, no frameworks or libraries are used.

<script src="/yummy/js/yummy-search.js"></script>

But wait, you can do more!

Additional Settings

Add dropdowns (with use of the yummy-search.js event handler), and change the type of operators displayed in the search UI.

$this->loadComponent('Yummy.YummySearch',[

    // Required
    'query' => $query, 

    'allow' => [
        'Organization.name' => [
            'name' => 'Vendor',
            'group' => 'Organization',
        ],
        'Invoice.year' => [
            'name' => 'Year',
            // for versions < 0.5.0
            //'select' => ['2017','2016','2015'], 
            // for versions >= 0.5.0
            'select' => [2017 => '2017', 2016 => '2016', 2015 => '2015'], 
            'operators' => ['eq','not_eq','gt','gt_eq','lt','lt_eq']
            'group' => 'Invoice'
        ],
        'Invoice.created' => [
            'name' => 'Created',
            'group' => 'Invoice',
            'castToDate' => true, // forces the column to be casted to date (useful for date searches on datetime)
        ]
    ],

    /**
     * Customize which operators are displayed and their display name, 
     * but be sure to maintain the array keys listed below!
     */
    'operators' => [
        [
            'like' => 'Containing', // SQL LIKE '%%'
            'not_like' => 'Not Containing', // SQL NOT LIKE '%%'
            'gt' => 'Greater than', // SQL >
            'gt_eq' => 'Greater than or equal', // SQL >=
            'lt' => 'Less than', // SQL <
            'lt_eq' => 'Less than or equal', // SQL <=
            'eq' => 'Exact Match', // SQL =
            'not_eq' => 'Not Exact Match', // SQL !=
        ]
    ],

    /**
     * Only set if the base model is different from the controller,  
     * avoids error: Base table or view not found
     */
    'model' => false, // (default: your controllers model)

    /**
     * Set this to that data source your table exists in, 
     * only needed if your using non-default data source.
     */
    'dataSource' => 'default', // (default: 'default')

    /**
     * Whether YummySearch UI will group columns under their 
     * table using <optgroup> elements in the select element. 
     * You can also use custom groups.
     */
    'selectGroups' => true, // (default: false)
]);

Changing the layout of YummySearchHelper (optional)

The YummySearchHelper uses bootstrap classes to build its interface. You can load your own custom CakePHP element to match the style of your desired theme. To do this copy the Yummy Search Element into your applications src/Template/Element directory.

To ensure compatibility leave all yummy-search-* classes in your custom element. Beyond that you should be able to modify the classes and elements to fit your application. Once your custom element has been built, you need to reference it with YummySearchHelper.

Example:

$this->helpers()->load('Yummy.YummySearch');
echo $this->YummySearch->basicForm([
    'element' => 'yummy-search-custom',
]);

By default YummySearch will use inline styles to toggle the visibility of the plus/minus add row buttons. If you'd rather use classes or have a strict content security policy (e.g. style-src 'self') that requires classes you can define those like so:

$this->helpers()->load('Yummy.YummySearch');
echo $this->YummySearch->basicForm([
    'classes' => [
        'hide' => 'hide-class-name',
        'show' => 'show-class-name'
    ]
]);

JavaScript Event listeners (optional)

When a search field is changed a custom event called "yummySearchFieldChange" is dispatched. You can attach an event listener to do things like change text inputs to datepickers, swap out text inputs with select inputs using the _options setting that was explained earlier, or whatever else your users desire. Your event listener will receive an event object from yummySearchFieldChange. You'll want to look at the e.detail object which contains:

  • field: (dom object) the search element, the first drop down in the row
  • operator: (dom object) the operator element, the second drop down in the row
  • input: (dom object) the variable input (search parameter)
  • dataType: (string) the data type of the field: date, string, integer, etc.
  • items: (array) an array of items if _options was used in YummySearch config
  • prevValue: (string|null) on page reload if a default was selected this will be passed over in the event
  • prevOperator: (string|null) on page reload if a default was selected this will be passed over in the event

Here is an example using jQuery, but you can use whatever library or no library. Remember, your event listeners must be included AFTER yummy-search.js to work properly.

document.addEventListener('yummySearchFieldChange', function(e){

    $(e.detail.input).show().attr('disabled',false);
    $(e.detail.input).parent().find('select').remove();
    
    switch(e.detail.dataType){
        /**
         * Date/Timestamp: bootstrap datetimepicker
         */
        case 'date':
        case 'timestamp':
            $(e.detail.input).datetimepicker({
                format: 'YYYY-MM-DD'
            });
            $(e.detail.operator).find('option[value="containing"]').attr('disabled',true);
            $(e.detail.operator).find('option[value="not_containing"]').attr('disabled',true);

            if (e.detail.prevOperator !== null) {
                $(e.detail.operator).val(e.detail.prevOperator);
            } else {
                $(e.detail.operator).val('like');
            }
            
            break;
        case 'boolean':
            // you can create a radio or checkbox here
            break;
        /**
         * List: custom dropdown
         */
        case 'list':
            $(e.detail.input).hide().attr('disabled',true);
            
            var dropdown = '<select name="YummySearch[search][]" class="form-control border-input yummy-search">';
            dropdown+= '<option value=""></option>';
            for (var i=0; i<e.detail.items.length; i++) {
                if (e.detail.prevValue === e.detail.items[ i ]){
                    dropdown+= '<option value="' + e.detail.items[ i ] + '" selected="selected">' + e.detail.items[ i ] + '</option>';
                } else {
                    dropdown+= '<option value="' + e.detail.items[ i ] + '">' + e.detail.items[ i ] + '</option>';
                }
            }
            dropdown+= '</select>';
            $(e.detail.input).parent().append(dropdown);
            
            if (e.detail.prevOperator !== null) {
                $(e.detail.operator).val(e.detail.prevOperator);
            } else {
                $(e.detail.operator).val('eq');
            }
            
            break;
        default:
            $(e.detail.operator).find('option').attr('disabled',false);
            try{
                $(e.detail.input).datetimepicker('destroy');
            }
            catch(e){
                
            }
    }
}, false);

/**
 * Initiate event dispatchers for elements created from previous search
 */
YummySearch.load();

Exceptions

This component has some custom exceptions which can be thrown in certain cases.

ConfigurationException Thrown if YummySearch is missing requirements. For instance, not having Paginator will throw this exception.

QueryException Thrown if Query condition data is invalid, such as missing an operator.

SchemaException Thrown if YummySearch cannot determine associations.