Kompo Queries docs
Tables & Catalogs 🤔 Automated browsing, filtering, sorting & pagination

Komposing a query

Query template

The Query class has three important sections.

  • The query method where we prepare the query.
  • The card($item) method where the different variables that a card needs are declared.
  • The filters methods: `top()`, `right()`, `bottom()`, `left()` and `headers()` for Table queries.
<?php

namespace App\Http\Komposers;

use Kompo\Query;

class MyQuery extends Query
{
   /******* The properties **********/
   public $layout = 'Masonry';
   ...

   /******* The query section *******/
   public function query() { ... }

   /******* The card section ********/
   public function card($item) { ... }

   /******* The filters section *****/
   public function top() { ... }
   public function right() { ... }
   public function bottom() { ... }
   public function left() { ... }

   public function headers() { ... } //for Tables (a subtype of Query)
}

Query properties

Here we define the Query's high-level properties or settings like the layout or pagination options. There are also other modifiable settings that are covered in the sorting, ordering and filtering sections.

The layout section

Currently, there are 4 Query layouts available:

  • `Table`: display the items in table rows.
  • `Horizontal` (the default layout): cards are just stacked in rows.
  • `Grid`: this leverages Bootstrap's infamous grid system.
  • `Masonry`: to display different height cards beautifully and responsively.

To set the layout, you declare the public property `$layout` at the beginning of your Query class:

class MyQuery extends Query
{
   public $layout = 'Horizontal'; //The layout style where cards will be displayed.

   ...

Pagination

All queries are paginated out of the box (see the query section for more info). You also define the pagination settings as properties.


class MyQuery extends Query
{
   public $perPage = 50; //The amount of items per page.
   public $noItemsFound = 'No items found'; //The message to display when no items are found.

   public $hasPagination = true; //Whether to display pagination links or not
   public $topPagination = true; //Whether to display pagination links above the cards
   public $bottomPagination = false; //Whether to display pagination links below the cards
   public $leftPagination = false; //Whether to align pagination links to the left or to the right

   public $paginationStyle = 'Links'; //The pagination component. Other option is 'Showing'

   ...

You may chose between different pagination styles. Currently, the choices are the following but more will be added to the library:

`Links`

`Showing`

Query parameters

To instantiate a Query class, you may simply declare the class with the new keyword. And if you need to inject parameters or external dependencies, you may use the first argument to add data to the Query's store.

$myQuery = new MyQuery();

//Or with parameters
$myQuery = new MyQuery([
   'some-param' => 'some-value'
]);

If it is used inside another Komposer and you would like to chain some methods to it, the `::form()` method also works:

public function komponents()
{
   return [
      MyQuery::form()->class('p-4 bg-gray-100'),

      //or with parameters
      MyQuery::form([
         'some-param' => 'some-value'
      ])->class('p-4 bg-gray-100')
   ];
}

Query data store example

Let's say we have a QuestionForm and we want to display its answers. To do so, we create an AnswersQuery where the parent Question's id is injected:

class QuestionForm extends Form
{
   public $model = Question::class;

   public function komponents()
   {
      return [
         Input::form('Title'),
         Textarea::form('Content'),
         //We pass the Question's id here
         AnswersQuery::form(['question_id' => $this->model->id])
      ];
   }
The store data is accessible at all stages of the Query, i.e. on initial display AND all subsequent browsing, filtering or sorting calls.

Then in your AnswersQuery class, you can retrieve the question id thanks to the `store()` method.

class AnswersQuery extends Query
{
    public function query()
    {
       //We retrieve the question_id here
       return Answer::where('question_id', $this->store('question_id'));
    }

...

Fetching the results

The `query()` method is where you specify the Builder statement that will fetch the displayed items. This statement will also be used on subsequent calls to filter, sort and paginate the results. It is preferable NOT to execute the query in this step with `->get()` or `->paginate()`.

Notice how we return a Builder instance and NOT an executed query.
Kompo will execute it for you behind the scenes.

class MyQuery extends Query
{
   ...

   public function query()
   {
      return Post::with('tags')->orderBy('published_at');
   }

   ...

The return value of query

It is recommended that a query method return:

  • either an `Illuminate\Database\Eloquent\Builder` instance,
  • or an `Illuminate\Database\Query\Builder` instance.

While it is possible to also return:

  • an `Illuminate\Database\Eloquent\Collection` instance,
  • an `Illuminate\Support\Collection` instance,
  • or even a simple `Array`

it comes at a performance cost and poorer filtering capabilities. Use Arrays or Collections only if the result set is not too large.

Eloquent ORM methods

You may use any Eloquent ORM method such as:

  • where, whereHas, whereIn, having, ... to prefilter the records.
  • with, withCount, ... to eager load relationships.
  • orderBy, orderByRaw, ... to order your records.
  • groupBy, skip, take, ... to group or limiting your records.
public function query()
{
  return Post::whereHas(...)->withCount(...)->orderByRaw(...);
}

Or to query all the records, you may simply write:

public function query()
{
  return new Post(); //<-- This will paginate through ALL the posts in the DB
}

For filtering items after initial load, You may then filter the records but that will be handled out of the box by the filters in the filter section.

Query Cards

The information needed to display each card is given in the `card` method. This method is always declared with a single parameter `$item` which comes from the paginated query results. There are 3 ways of defining a card depending on the level of customization you want to have.

  1. Komposing cards with Komponents.
  2. Use one of Kompo's prebuilt cards (included in the base installation).
  3. Advanced: Create a custom card Vue component.

1. Komposing Cards

To kompose a card, you may use Layouts, Blocks, Triggers or any other Komponent of your choice to assemble cleverly a card that suits your needs.

public function card(Post $post)
{
   return FlexBetween::form(
      Title::form($post->title),
      FlexEnd::form(
        _Link('💚')->post('post.like', ['id' => $post->id]),
        _Link('💬')->get('post.comment', ['id' => $post->id])
      )
   )->class('p-2 md:p-4 text-gray-500');
}

2. Prebuilt Cards

Kompo offers prebuilt card components that you may just reference and fill the relevant info in the corresponding card properties. For example, here we are using the `ImageOverlay` card and adding the article's image, title, grid and styles class and finally a toolbar of buttons to like or share the article.

public function card($item)
{
   return ImageOverlay::form([
     'image' => asset($item->image),
     'title' => $item->title,
     'col' => 'col-4',
     'class' => 'shadow mb-6',
     'buttons' => FlexEnd::form(
        Link::icon('icon-heart')->post('article.like', ['id' => $item->id]),
        Link::icon('icon-share')->post('article.share', ['id' => $item->id])
     )
   ]);
}
To see all the predefined cards and their required properties, check out the API for Query cards.

3. Custom card

This is an advanced feature and offers the advantage of total freedom and flexibility, since you will be building your own Vue component. Check out the Custom Cards section for all the information on how to build one.

Filter, Sort & Order

Filters placement

You may include additional html, filters, and other components all around your Query's cards. There are 4 methods `top`, `right`, `left` and `bottom` that can be used for positionning these components and they have to return one or an array of komponents the same way as you would in a Form or Card:

left()
top()
Layout
Card
Card
Card
Card
Card
Card
bottom()
right()

Below is an example where we add a decorative title, a link to a Form, and filters to interact with our query results.

public function top()
{
  return [
     //Adding a title and a link to add a new item
     FlexBetween::form(
        Title::form('Examples'),
        Link::form('Add an example')->href('admin.example')
     ),
     //Adding filters (see next section how to activate them)
     Columns::form(
        Select::form('Category'),
        Input::form('Title')
     )
  ];
}

Filtering

It is very straightforward to filter your Query's cards. To do so, you may add one or more Field component in one of the filters sections and then chain one of the filtering actions.

Only Field components can filter Queries. If you wish to filter using a Button or Link, you may use the SelectButtons or SelectLinks components which are Fields.

The following operators may be used to filter:

filter
string|null $operator
Pick one of these supported operators `=`, `>`, `<`, `>=`, `<=`, `LIKE`, `STARTSWITH`, `ENDSWITH`, `BETWEEN`, `IN`.
Or keep blank to fallback to the field's default operator.
Filters a Query `onChange` for a Field or `onInput` for a text Input field by default.
If these default triggers do not suit you, use another one, for example: ->onBlur->filter('>=')

Attributes & relationships

The Field name drives the filtering behavior. It can be:

  • A simple string `'attribute'` : to refer to one of the model's attributes.
  • A dot-separated string `'relationship.attribute'` or `'relationship.relationship.attribute'` : to filter by nested relationships. Kompo currently supports two levels deep nesting.
  • If no attribute is specified after a relationship, for example: `'relationship'` or `'relationship.relationship'`: it will use the last relationship's PRIMARY KEY.
public function top()
{
   return [
      Select::form('Category')
         ->name('category_id') //Filtering by attribute
         ->filter(), //Fields filter onChange by default.
      Input::form('Title')
         ->name('category.name') //Filtering the relationship's name
         ->filter() //Input Komponents filter onInput by default and debounce by 500ms
   ];
}

Filtering operators

You may assign one of these operators in the `filter` method: `=`, `>`, `<`, `>=`, `<=`, `LIKE`, `STARTSWITH`, `ENDSWITH`, `BETWEEN`, `IN`. If you don't specifically assign an operator in the `filter` method (.i.e. null argument), Kompo will use these default conditions for each Field:

  • If the Field can support multiple values (MultiSelect for example), it will use a whereIn.
  • If the Field is a text Input, it will use a where($column, 'LIKE', '%'.$value.'%').
  • Otherwise, it will perform a simple where.

If you have multiple filters, Kompo will perform an AND where.

Prefilter a Query

Of course, if you want to prefilter your Query, you may define that in the `query` method. This filter is permanent and will always be preserved on subsequent browse requests.

public function query()
{
   //Permanent Query filter
   return Article::whereNotNull('published_at');
}

Sorting

You may also define sorting capabilities in your Query as easily as filters. The komponents that are capable of sorting are:

  • `Fields`: in this case their values will determine the sort order.
  • `Buttons` and `Links`: you have to instruct the sort order with one of the sorting actions.
  • `Th` (table headers): you also have to instruct the sort column and direction in one of the sorting actions.

Sorting action

The following action may be used to sort:

sort
string|null $sortOrders
If field, the value will determine the sort. If trigger (link or th), we need to add a pipe serapated string of column:direction for ordering.
Triggers a sort event of the query. The parameter is a pipe separated string of column:direction. Example: updated_at:DESC|last_name|first_name:ASC.

The `$sortOrders` parameter accepts a pipe-delimited string of one or more `column:direction` pairs. You may also sort on nested relationships (one level deep only) by using a dot-delimited string in the column part:

  • Sorting by an attribute `'attribute'` : this will sort by the model's attribute and the direction is ASC by default.
  • Sorting by one attribute with a direction `'attribute:DESC'`: this will sort by the model's attribute in the descending order.
  • Sorting by multiple attributes and directions `'attribute1:DESC|attribute2|attribute3:DESC'` : this will sort by 3 different attributes with attribute1 and attribute3 in the descending order and attribute2 in the ascending order.
  • Sorting with relationships and attributes `'relationship.attribute1:DESC|attribute2'` : this will sort by the model's attribute2 in the ascending order and the relationship's attribute1 in the descending order.

Presort a Query

Of course, if you want a presorted Query, you may define that in the `query` method. This sort will always be preserved on subsequent browse requests.

public function query()
{
   //Permanent Query sort
   return Article::orderBy('published_at', 'DESC');
}

Draggable ordering

Kompo also offers the ability to drag and drop cards and reorder the records on the back-end. Let's say your model has an `order_column` column that will be used to display the items in the desired order.

You may set the `$orderable` property to activate this functionality:

class MyQuery extends Query
{
   public $orderable = 'order_column';

Now the Query cards will be draggable and the user may change the cards' orders from the Front-end and the Back-end will get automatically updated.

The orderable column should be defined as an INT datatype or equivalent in your database.