Essential Guide 7: Partials and custom components
Demo: Matestack Demo Github Repo: Matestack Demo Application
Welcome to the seventh part of our tutorial about building a web application with matestack.
Introduction
In this part, we will discover how we can create custom components with matestack to declutter and better structure our code.
In this guide, we will
refactor our new and edit forms with partials
refactor our person index page with components
refactor the show page and reuse our component
add a custom component to our app
Prerequisites
We expect you to have successfully finished the previous guide.
Using partials
Partials are an easy way to structure code in apps, pages and components. Let's take a look at how partials work with an example, changing our first page.
class Demo::Pages::FirstPage < Matestack::Ui::Page
def response
div do
heading text: 'Hello World!', size: 1
end
description
end
private
def description
paragraph text: 'This is our first page, which now uses a partial'
end
end
Partials can be used to split view code apart, in order to write better structurem, more readable, cleaner code. As we can see in the example, a partial is nothing else but a method call. The method implements a view part. Partials can also be used within partials.
Now we know what partials can do, lets refactor our person new and edit pages. They share a lot of same code, both containing the same form. To reuse our code, we create a Demo::Pages::Person::Form
page, from which both new and edit will inherit. Let's create it in app/matestack/demo/pages/persons/form.rb
class Demo::Pages::Persons::Form < Matestack::Ui::Page
protected
def person_form(button_text)
form person_form_config, :include do
label text: 'First name'
form_input key: :first_name, type: :text
br
label text: 'Last name'
form_input key: :last_name, type: :text
br
label text: 'Person role'
form_select key: :role, type: :radio, options: Person.roles.keys
br
form_submit do
button text: button_text
end
end
end
def person_form_config
raise 'needs to be implemented'
end
Now we have a form page, which only implements a partial person_form
containing the new and edit form for our person. It takes one parameter, which will be used as the button label, in order to allow different labels for new and edit. We know from an earlier chapter, that a form needs a hash as parameter. Our person_form_config
method should return the config hash. As this hash changes, depending on the page we use our form, we need to overwrite it in the class where we inherit from our form.
First we refactor our new page. We change it, so it inherits from our form page, replace the form with the partial and rename the method returning the form config hash to match the name of our form page.
class Demo::Pages::Persons::New < Demo::Pages::Persons::Form
def response
transition path: persons_path, text: 'All persons'
heading size: 2, text: 'Create a new person'
person_form 'Create person'
end
def person_form_config
{
for: Person.new,
method: :post,
path: persons_path,
success: {
transition: {
follow_response: true
}
}
}
end
end
Our new page looks now much cleaner. We overwrite person_form_config
, which is used in the partial as an input for the required hash parameter of the form, setting the correct configurations for our new form.
After this we can refactor our edit page accordingly. Inherit from our form page, use the form partial and overwrite the config method.
class Demo::Pages::Persons::Edit < Demo::Pages::Persons::Form
def response
transition path: :person_path, params: { id: @person.id }, text: 'Back to detail page'
heading size: 2, text: "Edit Person: #{@person.first_name} #{@person.last_name}"
person_form 'Save changes'
end
def person_form_config
{
for: @person,
method: :patch,
path: :person_path,
params: {
id: @person.id
},
success: {
transition: {
follow_response: true
}
}
}
end
end
We successfully refactored our code using partials, so it's better structured, more readable and we keep it dry (don't repeat yourself).
Visit localhost:3000 and navigate to the new and edit pages, to check that everything works like before.
Custom components
Custom components can be used to create reusable components, representing ui parts. They can be just a button, or a more complex card or even a complete slider, which reuses the card component.
In the next steps we want to refactor our person index page, by creating a person teaser component.
But first, we need to create a component registry. Every custom components need to be registered through a component registry. It is possible to use multiple registries to keep for example admin components apart from public components, but in this guide we keep it simple and only use one registry. Let's create it in app/matestack/components/registry.rb
. As our components should be reusable by all apps, pages and components, we create them in the app/matestack/components
folder, where our registry lives.
module Components::Registry
Matestack::Ui::Core::Component::Registry.register_components(
person_teaser: Components::Persons::Teaser
)
end
In the above registry, we registered a person_teaser
component, which refers to the class Components::Person::Teaser
. Let's implement this class, our first component, in app/matestack/components/persons/teaser.rb
.
class Components::Persons::Teaser < Matestack::Ui::Component
requires :person
def response
div class: 'teaser' do
heading text: "#{person.first_name} #{person.last_name}", size: 3
transition text: '(Details)', path: person_path(person)
action delete_person_config(person) do
button 'Delete'
end
end
end
private
def delete_person_config(person)
{
method: :delete,
path: persons_path(person)
success: {
emit: 'person-deleted'
},
confirm: {
text: 'Do you really want to delete this person?'
}
}
end
end
Let's take a look whats happening in our component. Components need to inherit from matestacks component Matestack::Ui::Component
. They also define a response method, which contains the content that is rendered. As we want to display information from a person, we need access to a person in our component. To achieve this components offer you the possibility to define required and optional properties with the requires
and optional
methods. We can pass as many symbols as we like to one of the calls or call it multiple times. Every so defined property is accessible through a method with the same name as the symbol. In the response
we create a div containing a h3 tag with the full name of the person, a transition to the show page and the delete button we also had on the index page.
In order to use this our teaser component we need to include the component registry in the ApplicationController
.
class ApplicationController < ActionController::Base
include Matestack::Ui::Core::ApplicationHelper
include Components::Registry
end
Now we can use our custom person teaser by calling person_teaser
to refactor our list of person on our index page. Instead of iterating over the persons and creating a list element for every person, we now can iterate over the persons and call our component passing the person to it.
class Demo::Pages::Persons::Index
def prepare
@persons = Person.all
end
def response
ul do
async id: 'person-list', rerender_on: 'person-deleted' do
@persons.each do |person|
person_teaser person: person
end
end
end
end
end
Using our custom component is as easy as calling the name we defined in our registry. Required and optional propertys are passed to a component as a hash. If we would not provide a person to our person teaser the component would raise an exception as a person is required. Optional properties are as the name suggests optional and therefore can be left out.
When you start your application locally now, the missing list bullet points should be the only visible change. We will take care of styling soon - but first, let's reuse our newly introduced component!
Using HAML in custom components
If you need more fine-grained control of your view layer or want to reuse some old HAML files, you can also create custom components like this:
Create a file called app/matestack/components/person/disclaimer.rb
and add this content:
class Components::Person::Disclaimer < Matestack::Ui::Component
end
Since this component does not have a response
method, matestack automatically looks for a disclaimer.haml
file right next to the component. Note that you can still use a prepare
method.
So let's add the disclaimer.haml
file in app/matestack/components/person/
and add a simple paragraph:
%p
None of the presented names belong to and/or are meant to refer to existing human beings. They were created using a "Random Name Generator".
To display this disclaimer on every page within our Demo::App
, we need to register it in the /app/matestack/components/registry.rb
:
module Components::Registry
Matestack::Ui::Core::Component::Registry.register_components(
person_teaser: Components::Person::Teaser,
person_disclaimer: Components::Person::Disclaimer
)
end
Afterwards, add it to app/matestack/demo/app.rb
within the main
-block:
# ...
main do
yield_page
person_disclaimer
# ...
end
# ...
Different from our person_teaser
, we don't need to hand over properties to our person_disclaimer
. Spin up your application and check out the changes!
More information on custom components
Even though we only covered very basic cases here, you may already have some idea of how powerful custom components can be! By leveraging useful namespaces and calling custom components within other custom components, you can get quite fancy and build complex user interfaces while keeping the code maintainable and reasonable.
Side note: Custom components also give you a neat way of reusing your *.haml
views with matestack (erb and slim support will be added soon).
To learn more, check out the basic building blocks guides section about custom components.
Saving the status quo
As usual, we want to commit the progress to Git. In the repo root, run
git add . && git commit -m "Refactor person new, edit, index, show page to use custom components, add custom component registry, add disclaimer component to app"
Recap & outlook
Today, we covered a great way of extracting recurring UI elements into reusable components with matestack. Of course, we only covered a very basic use case here and there are various ways of using custom components.
Take a well deserved rest and make sure to come back to the next part of this series, introducing the powerful collection
component.
Last updated
Was this helpful?