Skip to content

Commit 02f42ea

Browse files
author
Nils Henning
committed
[TASK][REFACTOR] add partial and component guide, rework collection guide
1 parent b202084 commit 02f42ea

9 files changed

+454
-364
lines changed

docs/guides/essential/06_async_component.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,4 @@ git add . && git commit -m "add delete button to person list and update it dynam
124124

125125
We added a delete button to our person list on the index page. When a person is deleted our list gets automatically updated without even reloading the page, just by updating the part that is needed. And all of that with a few lines of code and without writing any javascript.
126126

127-
Take a well deserved rest and make sure to come back to the next part of this series, introducing the powerful [`collection` component](/docs/guides/essential/07_collection_async.md).
127+
Take a well deserved rest and make sure to come back to the next part of this series, introducing [partials and custom components](/docs/guides/essential/07_partials_and_custom_components.md).
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
# Essential Guide 7: Partials and custom components
2+
3+
Welcome to the seventh part of our essential guide about building a web application with matestack.
4+
5+
## Introduction
6+
7+
In this part, we will discover how we can create custom components with matestack to declutter and better structure our code.
8+
9+
In this guide, we will
10+
- refactor our new and edit forms with partials
11+
- refactor our person index page with components
12+
- refactor the show page and reuse our component
13+
- add a custom component to our app
14+
15+
## Prerequisites
16+
17+
We expect you to have successfully finished the [previous guide](guides/essential/05_collection_async.md).
18+
19+
## Using partials
20+
21+
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.
22+
23+
```ruby
24+
class Demo::Pages::FirstPage < Matestack::Ui::Page
25+
26+
def response
27+
div do
28+
heading text: 'Hello World!', size: 1
29+
end
30+
description
31+
end
32+
33+
private
34+
35+
def description
36+
paragraph text: 'This is our first page, which now uses a partial'
37+
end
38+
39+
end
40+
```
41+
42+
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.
43+
44+
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`
45+
46+
```ruby
47+
class Demo::Pages::Persons::Form < Matestack::Ui::Page
48+
49+
protected
50+
51+
def person_form(button_text)
52+
form person_form_config, :include do
53+
label text: 'First name'
54+
form_input key: :first_name, type: :text
55+
br
56+
label text: 'Last name'
57+
form_input key: :last_name, type: :text
58+
br
59+
label text: 'Person role'
60+
form_select key: :role, type: :radio, options: Person.roles.keys
61+
br
62+
form_submit do
63+
button text: button_text
64+
end
65+
end
66+
end
67+
68+
def person_form_config
69+
raise 'needs to be implemented'
70+
end
71+
```
72+
73+
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.
74+
75+
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.
76+
77+
```ruby
78+
class Demo::Pages::Persons::New < Demo::Pages::Persons::Form
79+
80+
def response
81+
transition path: persons_path, text: 'All persons'
82+
heading size: 2, text: 'Create a new person'
83+
person_form 'Create person'
84+
end
85+
86+
def person_form_config
87+
{
88+
for: Person.new,
89+
method: :post,
90+
path: persons_path,
91+
success: {
92+
transition: {
93+
follow_response: true
94+
}
95+
}
96+
}
97+
end
98+
99+
end
100+
```
101+
102+
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.
103+
104+
After this we can refactor our edit page accordingly. Inherit from our form page, use the form partial and overwrite the config method.
105+
106+
```ruby
107+
class Demo::Pages::Persons::Edit < Demo::Pages::Persons::Form
108+
109+
def response
110+
transition path: :person_path, params: { id: @person.id }, text: 'Back to detail page'
111+
heading size: 2, text: "Edit Person: #{@person.first_name} #{@person.last_name}"
112+
person_form 'Save changes'
113+
end
114+
115+
def person_form_config
116+
{
117+
for: @person,
118+
method: :patch,
119+
path: :person_path,
120+
params: {
121+
id: @person.id
122+
},
123+
success: {
124+
transition: {
125+
follow_response: true
126+
}
127+
}
128+
}
129+
end
130+
131+
end
132+
```
133+
134+
We successfully refactored our code using partials, so it's better structured, more readable and we keep it dry (don't repeat yourself).
135+
136+
Visit [localhost:3000](http://localhost:3000) and navigate to the new and edit pages, to check that everything works like before.
137+
138+
## Custom components
139+
140+
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.
141+
142+
In the next steps we want to refactor our person index page, by creating a person teaser component.
143+
144+
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.
145+
146+
```ruby
147+
module Components::Registry
148+
Matestack::Ui::Core::Component::Registry.register_components(
149+
person_teaser: Components::Persons::Teaser
150+
)
151+
end
152+
```
153+
154+
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`.
155+
156+
```ruby
157+
class Components::Persons::Teaser < Matestack::Ui::Component
158+
159+
requires :person
160+
161+
def response
162+
div class: 'teaser' do
163+
heading text: "#{person.first_name} #{person.last_name}", size: 3
164+
transition text: '(Details)', path: person_path(person)
165+
action delete_person_config(person) do
166+
button 'Delete'
167+
end
168+
end
169+
end
170+
171+
private
172+
173+
def delete_person_config(person)
174+
{
175+
method: :delete,
176+
path: persons_path(person)
177+
success: {
178+
emit: 'person-deleted'
179+
},
180+
confirm: {
181+
text: 'Do you really want to delete this person?'
182+
}
183+
}
184+
end
185+
186+
end
187+
```
188+
189+
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.
190+
191+
In order to use this our teaser component we need to include the component registry in the `ApplicationController`.
192+
193+
```ruby
194+
class ApplicationController < ActionController::Base
195+
include Matestack::Ui::Core::ApplicationHelper
196+
include Components::Registry
197+
end
198+
```
199+
200+
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.
201+
202+
```ruby
203+
class Demo::Pages::Persons::Index
204+
205+
def prepare
206+
@persons = Person.all
207+
end
208+
209+
def response
210+
ul do
211+
async id: 'person-list', rerender_on: 'person-deleted' do
212+
@persons.each do |person|
213+
person_teaser person: person
214+
end
215+
end
216+
end
217+
end
218+
219+
end
220+
```
221+
222+
Using ur 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.
223+
224+
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!
225+
226+
----
227+
# Refactor from here
228+
----
229+
230+
## Reusing the custom component
231+
How about displaying three random persons from the database on the person show page, each within a card? It's as simple as below:
232+
233+
```ruby
234+
class Demo::Pages::Persons::Show < Matestack::Ui::Page
235+
236+
def prepare
237+
@other_persons = Person.where.not(id: @person.id).order("RANDOM()").limit(3)
238+
end
239+
240+
def response
241+
# ...
242+
other_persons
243+
end
244+
245+
def other_persons
246+
heading size: 3, text: 'Three other persons:'
247+
@other_persons.each do |person|
248+
person_teaser person: person
249+
end
250+
end
251+
252+
# ...
253+
end
254+
```
255+
256+
We query the database for three random records in the `prepare` method, add a partial for better composability and then loop through the records, handing each record as input to our custom card component!
257+
258+
## Using HAML in custom components
259+
260+
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:
261+
262+
Create a file called `app/matestack/components/person/disclaimer.rb` and add this content:
263+
264+
```ruby
265+
class Components::Person::Disclaimer < Matestack::Ui::Component
266+
end
267+
```
268+
269+
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.
270+
271+
So let's add the `disclaimer.haml` file in `app/matestack/components/person/` and add a simple paragraph:
272+
273+
```haml
274+
%p
275+
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".
276+
```
277+
278+
To display this disclaimer on every page within our `Demo::App`, we need to register it in the `/app/matestack/components/registry.rb`:
279+
280+
```ruby
281+
module Components::Registry
282+
283+
Matestack::Ui::Core::Component::Registry.register_components(
284+
person_teaser: Components::Person::Teaser,
285+
person_disclaimer: Components::Person::Disclaimer
286+
)
287+
288+
end
289+
```
290+
291+
Afterwards, add it to `app/matestack/demo/app.rb` within the `main`-block:
292+
293+
```ruby
294+
# ...
295+
main do
296+
page_content
297+
person_disclaimer
298+
# ...
299+
end
300+
# ...
301+
```
302+
303+
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!
304+
305+
## More information on custom components
306+
307+
Even though we only covered very basic cases here, you may already have some idea of how powerful custom components can be!
308+
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.
309+
310+
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).
311+
312+
To learn more, check out the [extend API documentation](docs/extend/custom_static_components.md) for custom static components.
313+
314+
## Saving the status quo
315+
316+
As usual, we want to commit the progress to Git. In the repo root, run
317+
318+
```sh
319+
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"
320+
```
321+
322+
## Recap & outlook
323+
324+
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.
325+
326+
Take a well deserved rest and make sure to come back to the next part of this series, introducing the powerful [`collection` component](/docs/guides/essential/08_collection_async.md).

0 commit comments

Comments
 (0)