Use the response method to define the UI of the component by using Matestack's HTML rendering or calling components.
class SomeComponent < Matestack::Ui::Component
def response
div id: "my-component" do
plain "hello world!"
end
SomeOtherComponent.()
end
end
class ExamplePage < Matestack::Ui::Page
def response
div id: "div-on-page" do
SomeComponent.()
end
end
end
Partials and helper methods
Use partials to keep the code dry and indentation layers manageable!
Local partials on component level
In the component definition, see how this time from inside the response, the my_partial method below is called:
class SomeComponent < Matestack::Ui::Component
def response
div id: "my-component" do
my_partial "foo from component"
end
end
private # optionally mark your partials as private
def my_partial text
div class: "nested"
plain text
end
end
end
Partials defined in modules
Extract code snippets to modules for an even better project structure. First, create a module:
module MySharedPartials
def my_partial text
div class: "nested"
plain text
end
end
end
Include the module in the component:
class SomeComponent < Matestack::Ui::Component
include MySharedPartials
def response
div id: "my-component" do
my_partial "foo from component"
end
end
end
Helper methods
Not only can instance methods be used as "partials" but as general helpers performing any kind of logic or data resolving:
class SomeComponent < Matestack::Ui::Component
def response
div id: "my-component" do
if is_admin?
latest_users.each do |user|
div do
plain user.name # ...
end
end
else
plain "not authorized"
end
end
end
private # optionally mark your helper methods as private
def is_admin?
true # some crazy Ruby logic!
end
def latest_users
User.last(10) # calling ActiveRecord models for example
end
end
Render?
Use the render? method to conditionally render the component based on custom rules:
class AdminComponent < Matestack::Ui::Component
required :user
def render?
context.user.admin?
end
def response
div id: "admin-component" do
plain "This component should only get rendered for admins"
end
end
end
This is particularly useful to avoid plastering your views with conditional statements like if and unless.
Instead of:
<% if current_user.admin? %>
<%= Components::AdminComponent.(user: current_user) %>
<% end %>
Use a prepare method to resolve instance variables before rendering a component if required.
class SomeComponent < Matestack::Ui::Component
def prepare
@some_data = "some data"
end
def response
div id: "my-component" do
plain @some_data
end
end
end
class ExamplePage < Matestack::Ui::Page
def response
div id: "div-on-page" do
SomeComponent.()
end
end
end
This is the HTML which gets created:
<div id="div-on-page">
<div id="my-component">
some data
</div>
</div>
Params access
A component can access request information, e.g. url query params, by calling the params method:
class SomeComponent < Matestack::Ui::Component
def response
div id: "my-component" do
plain params[:foo]
end
end
end
On the example page, reference the component as usual.
class ExamplePage < Matestack::Ui::Page
def response
div id: "div-on-page" do
SomeComponent.()
end
end
end
Now, visiting the respective route to the page, e.g. via /xyz?foo=bar, the component reads the [:foo] from the params and displays it like so:
<div id="div-on-page">
<div id="my-component">
bar
</div>
</div>
Passing data to components
You often need to pass data into your component to make them reusable. You have multiple possibilities to do that:
General options access
If you pass in a hash to a component...
class ExamplePage < Matestack::Ui::Page
def response
div id: "div-on-page" do
SomeComponent.(foo: "bar")
end
end
end
...this hash is accessible via options in the component:
class Some::Component < Matestack::Ui::Component
def response
div id: "my-component" do
plain options[:foo]
end
end
end
Optional and required options
Matestack components give you the possibility to define an explicit API for your component describing required and optional options for a component. Using this approach, it's way easier to understand what data can be processed by your component.
required and optional options will be deleted from the options hash and are only available via the context object.
Required options
Required options are required for your component to work, like the name suggests. If at least one required option is missing, an Exception is raised.
Declare your required options by calling required as follows:
class SomeComponent < Matestack::Ui::Component
required :some_property, :some_other
end
You then can use these options simply by calling the provided OpenStruct object context, which includes the injected options with their name:
class SomeComponent < Matestack::Ui::Component
required :some_property, :some_other
def response
# display some_property plain inside a div and some_other property inside a paragraph beneath it
div do
plain context.some_property
end
paragraph text: context.some_other
end
end
Optional options
To define optional options you can use the same syntax as required. Just use optional instead of required. Optional options are optional and not validated for presence like required options.
class SomeComponent < Matestack::Ui::Component
optional :optional_property, :other_optional_property # optional properties could be empty
def response
div do
plain context.optional_property
end
paragraph context.other_optional_property
end
end
Passing more complex data structures to components
You can pass any object you like and use it in the component with the helper.
class SomeComponent < Matestack::Ui::Component
required :some_option,
optional :some_other
def response
div do
plain context.some_option
end
if context.some_other.present?
paragraph context.some_other[:option]
end
end
end
Use it in the example page and pass in one option as a hash
class ExamplePage < Matestack::Ui::Page
def prepare
@hello = "hello!"
end
def response
div id: "div-on-page" do
SomeComponent.(some_option: @hello, some_other: { option: "world!" })
end
end
end
Alias properties
It's not possible to overwrite core methods of the OpenStruct object context
If you somehow want to inject options with a key matching a core method of the OpenStruct object, simply provide an alias name with the as: option. You can then use the alias accordingly. A popular example would be the option called class
class SomeComponent < Matestack::Ui::Component
required :foo, :bar, class: { as: :my_class }
def response
div class: context.my_class do
plain "#{context.foo} - #{context.bar}"
end
end
end
Text argument
Sometimes you just want to pass in a simple (text) argument rather than a hash with multiple keys:
class ExamplePage < Matestack::Ui::Page
def response
div id: "div-on-page" do
# simply pass in a string here
SomeComponent.("foo from page")
end
end
end
A component can access this text argument in various ways:
class Some::Component < Matestack::Ui::Component
def response
div id: "my-component" do
plain self.text # because such an argument is almost always a string
# or
plain context.text # for compatibility with older releases
end
end
end
This approach can be combined with injecting Hashes to components:
class ExamplePage < Matestack::Ui::Page
def response
div id: "div-on-page" do
# simply pass in a string here
Some::Component.("foo from page", { foo: "bar" })
end
end
end
class Some::Component < Matestack::Ui::Component
optional :foo
def response
div id: "my-component" do
plain self.text # because such an argument is almost always a string
# or
plain context.text # for compatibility with older releases
plain context.foo
end
end
end
Yielding inside components
Components can yield a block with access to scope, where a block is defined.
class SomeComponent < Matestack::Ui::Component
def response
div id: "my-component" do
yield
end
end
end
Pass a block to a component on the page as shown below:
class ExamplePage < Matestack::Ui::Page
def prepare
@foo = "foo from page"
end
def response
div id: "div-on-page" do
SomeComponent.() do
plain @foo
end
end
end
end
Not a fancy example, but this is the result:
<div id="div-on-page">
<div id="my-component">
foo from page
</div>
</div>
Slots
Slots in Matestack allow us to inject whole UI snippets into the component. It's a more specific yielding mechanism as you will yield multiple "named blocks" into the component. Each of these blocks can be referenced and positioned independently in the component,
Slots on the page instance scope
Define the slots within the component file as shown below. Please make sure to inject slots within a hash slots: { ... } into the component.
class SomeComponent < Matestack::Ui::Component
def prepare
@foo = "foo from component"
end
def response
div id: "my-component" do
slot :my_first_slot
br
slot :my_second_slot
end
end
end
Slots have access to the scope of the class, where they are defined. In this case @foo
class ExamplePage < Matestack::Ui::Page
def prepare
@foo = "foo from page"
end
def response
div do
some_component slots: {
my_first_slot: method(:my_simple_slot),
my_second_slot: method(:my_second_simple_slot)
}
end
end
def my_simple_slot
span id: "my_simple_slot" do
plain "some content"
end
end
def my_second_simple_slot
span id: "my_simple_slot" do
plain @foo
end
end
end
This gets rendered into HTML as shown below. Notice that the @foo from the component configuration got overwritten by the page's local @foo!
<div>
<div id="my-component">
<span id="my_simple_slot">
some content
</span>
<br/>
<span id="my_simple_slot">
foo from page
</span>
</div>
</div>
Using slots of components within components
To use component instance scope slots, first define slots within a static component:
class Other::Component < Matestack::Ui::Component
def prepare
@foo = "foo from other component"
end
def response
div id: "my-other-component" do
slot :my_slot_from_component
br
slot :my_slot_from_page
br
plain @foo
end
end
end
and also in some component:
class Some::Component < Matestack::Ui::Component
def prepare
@foo = "foo from component"
end
def response
div id: "my-component" do
other_component slots: {
my_slot_from_component: method(:my_slot_from_component),
my_slot_from_page: slots[:my_slot_from_page]
}
end
end
def my_slot_from_component
span id: "my-slot-from-component" do
plain @foo
end
end
end
Then, put both components (note that some component uses other component so that's how they're both in here) to use on the example page:
class ExamplePage < Matestack::Ui::Page
def prepare
@foo = "foo from page"
end
def response
div id: "page-div" do
some_component slots: { my_slot_from_page: method(:my_slot_from_page) }
end
end
def my_slot_from_page
span id: "my-slot-from-page" do
plain @foo
end
end
end
This gets rendered into the HTML below:
<div id="page-div">
<div id="my-component">
<div id="my-other-component">
<span id="my-slot-from-component">
foo from component
</span>
<br/>
<span id="my-slot-from-page">
foo from page
</span>
<br/>
foo from other component
</div>
</div>
</div>
Calling slots with params
Sometimes it's necessary to call a slot with params:
class SomeComponent < Matestack::Ui::Component
def response
div id: "my-component" do
User.last(10).each do |user|
slot(:user_card, user)
end
end
end
end
class ExamplePage < Matestack::Ui::Page
def response
div do
some_component slots: {
user_card: method(:user_card)
}
end
end
def user_card user
div class: "card" do
plain user.name
end
end
end