Essential Guide 8: Collection and async component
Demo: Matestack Demo Github Repo: Matestack Demo Application
Welcome to the eighth part of our tutorial about building a web application with matestack.
Introduction
In this part, we will improve the index page by making use of some of matestacks unique features!
In this guide, we will
refactor the index page to use matestacks
collectioncomponentadd pagination and basic search functionality to it
use matestack
asynccomponent from the sixth guide
Prerequisites
We expect you to have successfully finished the previous guide.
Adding a simple, filterable collection
Right now, the index page displays a simple list of all persons in our database. Starring a growing database with hundreds or thousands of records, this page will become quite large and confusing. In order to avoid this, we will first change it into a filterable list and later add pagination and a search to it. We use matestacks collection component to achieve this easily. First we implement our list with the collection component in our app/matestack/demo/pages/persons/index.rb.
class Demo::Pages::Persons::Index < Matestack::Ui::Page
  include Matestack::Ui::Core::Collection::Helper
  def prepare
    @person_collection_id = "person-collection"
    current_filter = get_collection_filter(@person_collection_id)
    person_query = Person.all
    filtered_person_query = person_query.where("last_name LIKE ?", "%#{current_filter[:last_name]}%")
    @person_collection = set_collection({
      id: @person_collection_id,
      data: filtered_person_query
    })
  end
  def response
    filter
    async id: 'person-list', rerender_on: "#{@person_collection_id}-update" do
      content
    end
    transition path: new_person_path, text: 'Create person'
  end
  def filter
    collection_filter @person_collection.config do
      collection_filter_input key: :last_name, type: :text, placeholder: "Filter by Last name"
      collection_filter_submit do
        button text: "Apply"
      end
      collection_filter_reset do
        button text: "Reset"
      end
    end
  end
  def content
    collection_content @person_collection.config do
      @person_collection.data.each do |person|
        person_teaser person: person
      end
    end
  end
endWhat's going on here? Let's break it down and go through it one part after another:
include Matestack::Ui::Core::Collection::Helper is being added to the page to add the necessary helpers for collections.
Within the prepare method we choose an id for our collection, define a default person_query and a filtered query which uses the filter param last_name to further filter the person_query. With set_collection we pass these as as hash to our collection and configure it this way.
In our response method we added three new parts.
A partial for the filter component. It uses a
collection_filtercomponent which represents a form wrapper for collection filter inputs. Inside the block we use acollection_filter_inputcomponent to render a text input for thelast_name. Underneath like in forms we have a wrapper for the submit action, if the contents of the block is clicked, the form will be submitted. Followed by a reset wrapper, which will reset the form if its content is clicked.An async component. The
collectioncomponent requires the content of it to be wrapped inside a async component withrerender_onset to the collection id followed by '-update'. When the collection filter is submitted or reset it will trigger this event so the collection content gets updated and rerendered matching the new filters.A content partial. The content partial takes care of rendering the records contained in the collection. It should render the same content, as our index page did before in the
responsemethod. The records should be wrapped inside acollection_contentcomponent, which needs our collection config as parameter. Inside it we iterate over each record the collection contains, by accessing itsdatamethod.
Now, run rails s and head over to localhost:3000 to check out what has changed. Apparently, the changes are not very visible to the website visitor, but you now can filter your persons by last name and already have laid the basis for more features that we will add in a moment!
Let's save the new status quo to Git - this makes it easier to see where changes to the code are applied later on:
git add . && git commit -m "Refactor person index page to use collection component"Adding pagination
So there's a neat filter on our page now, but the list still contain hundreds or thousands of persons. This will increase page load times with an increasing number of person. To keep page loads fast and to not show more persons at once than people could process, we implement pagination, showing only 6 items per page. We do this by configuring the collection component appropriately.
Within the prepare method, we update the set_collection call to enable pagination. There should be 6 person teaser per page.
@person_collection = set_collection({
  id: person_collection_id,
  data: filtered_person_query,
  init_limit: 6,
  filtered_count: filtered_person_query.count,
  base_count: person_query.count
})By setting an init_limit, we configure the maximum amount of items displayed on one page (this does not refer to matestack pages, but the collection component splitting the collection into smaller chunks, which typically are pages, therefore the name pagination). In order for our collection to know how many pages there are for this collection we need to pass in the count of filtered records as well as the base count, representing the count of records without any filtering.
To only render the correct amount of persons and to allow the pagination to work, we need to update our content partial. Instead of iterating over the data of the collection we know need to iterate over paginated_data. Also the user should be able to switch between the different pages, therefore we add a paginator partial.
def content
  collection_content @person_collection.config do
    @person_collection.paginated_data.each do |person|
      person_teaser person: person
    end
    paginator
  end
endOur paginator partial takes care of rendering the page links and displaying information like how many records out of all are displayed. Let's add the paginator partial
def paginator
  plain "showing #{@person_collection.from}"
  plain "to #{@person_collection.to}"
  plain "of #{@person_collection.filtered_count}"
  plain "from total #{@person_collection.base_count}"
  collection_content_previous do
    button text: "previous"
  end
  @person_collection.pages.each do |page|
    collection_content_page_link page: page do
      button text: page
    end
  end
  collection_content_next do
    button text: "next"
  end
endWhat does the paginator partial exactly do? At first we render information about how many records we see, how many there are etc. All this information is available through our collection, which we saved in the instance variable @person_collection. Next we render a 'previous' button, which will load the previous page. Afterwards we render a button for each page, labelled with the page number, which will load the according page. And at last we render a 'next' button, which will load the next page.
Make sure to run rails s and head over to localhost:3000 to check out the changes! As you should see, the visible persons should be reduced to six, and below them, it shows not only the currently displayed range of persons, but also handy buttons to browse the different collection pages (not to be confused with the matestack pages in app/matestack/demo/pages/*)!
Again, let's commit the new status quo to Git before enhancing things once more:
git add . && git commit -m "Add pagination to collection component on person index page"Adding ordering
Let's spice it up by adding another feature: Dynamically ordering our displayed records the way we like!
In the prepare method, define the current_order as displayed below and pass it to the filtered_person_query:
def prepare
  person_collection_id = "person-collection"
  current_filter = get_collection_filter(person_collection_id)
  current_order = get_collection_order(person_collection_id)
  person_query = Person.all
  filtered_person_query = person_query
  .where("last_name LIKE ?", "%#{current_filter[:last_name]}%")
  .order(current_order)
  @person_collection = set_collection({
    # ..
  })
endWhile the changes above may work, the page visitor can't yet change the current order. So let's add another partial called ordering to the response method:
def response
  filter
  ordering
  # ...
endAnd underneath the filter partial definition, add the ordering partial as shown below:
def ordering
  collection_order @person_collection.config do
    plain "sort by:"
    collection_order_toggle key: :last_name do
      button do
        plain "last_name"
        collection_order_toggle_indicator key: :last_name, asc: '(A-Z)', desc: '(Z-A)'
      end
    end
  end
endWithout much hassle, clicking on the button inside the collection_order_toggle now switches the order of records by the :last_name key.
Run rails s and head over to localhost:3000 to and check out what has changed! On the person index page, you should be able to alter the display order between three different states: Ascending and descending last names as well as the date the record was added. And all of this without writing a single line of JavaScript!
Deep dive into the collection component
collection componentSo we have come quite a way from simply displaying all the person records in a plain list, right? By making use of the collection component and adding the necessary configuration, the person index page content is now searchable, orderable and paginated! This would have required us to write a lot of JavaScript and a complex controller action, but with matestack collection component we could do it all just with a few lines of ruby.
The collection component was created with exactly this use case in mind - you got a collection of data (e.g. from ActiveRecord) that needs to be displayed in a filterable, ordered and paginated fashion (list, table, cards) without forcing you to write a lot of (repetitive, yet complex) logic.
Under the hood, the collection component consists of a couple of Vue.js components that listen for events (e.g. pressing the "Apply filter" button) and make sure the view snippets received from the server (e.g. the response only containing the filtered person records) get applied correctly.
From the developer's perspective, here is a more detailed overview of the main building blocks of this component:
A collection gets instantiated using the
set_collectionmethod which receives all the important configuration optionsidanddatainit_limit,filtered_count,base_countif the collection should be paginated
You can add an optional
collection_filter, usingcollection_filter_inputto filter for either text, number, email, date, password etc. with an input fieldcollection_filter_submitto apply the current input field contents on your collectioncollection_filter_resetto remove the filtering conditions
You can add an optional
collection_order, usinga
collection_order_toggle, expecting a key to order bya
collection_order_toggle_indicatorinside thecollection_order_toggleto indicate which order is currently being displayed
The (mandatory)
collection_contentinside anasynccomponentinside, you can loop through the collection's content using the
dataorpaginated_datamethod of the collection, depending on whether you're using a paginated collection or not
An optional pagination inside the
collection_contentcollection_content_previousandcollection_content_nextlet you access previous and next collection pageswhile looping through the
pagesof the collection, you can link to all the pages in the collection by using thecollection_content_page_linkmethod
That's a lot of input, so it may take some time to get used to this powerful component - but it's an investment that will pay off down the road and will make your life easier.
To learn more, check out the API documentation for the collection component.
Saving the status quo
As usual, we want to commit the progress to Git. In the repo root, run
git add . && git commit -m "Add ordering to collection component on person index page"Recap & outlook
We learned how to use matestacks collection component to generate a filterable, orderable, paginatable index page and reused our knowledge about partials and the async component.
So what's left? In the upcoming guides, you will create your own Vue.js components and learn about other topics like styling, notifications and authorization that are part of modern web applications
So stay tuned and, once ready, head over to the next part, covering Vue.js components for powerful custom components.
Last updated
Was this helpful?