Use the response method to define the UI of the component by using Matestack's HTML rendering or calling components.
classSomeComponent<Matestack::Ui::Componentdefresponse div id: "my-component"do plain "hello world!"endSomeOtherComponent.()endend
classExamplePage<Matestack::Ui::Pagedefresponse div id: "div-on-page"doSomeComponent.()endendend
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:
classSomeComponent<Matestack::Ui::Componentdefresponse div id: "my-component"do my_partial "foo from component"endendprivate# optionally mark your partials as privatedefmy_partialtext div class: "nested" plain textendendend
Partials defined in modules
Extract code snippets to modules for an even better project structure. First, create a module:
moduleMySharedPartialsdefmy_partialtext div class: "nested" plain textendendend
Include the module in the component:
classSomeComponent<Matestack::Ui::ComponentincludeMySharedPartialsdefresponse div id: "my-component"do my_partial "foo from component"endendend
Helper methods
Not only can instance methods be used as "partials" but as general helpers performing any kind of logic or data resolving:
classSomeComponent<Matestack::Ui::Componentdefresponse div id: "my-component"doif is_admin? latest_users.each do|user| div do plain user.name # ...endendelse plain "not authorized"endendendprivate# optionally mark your helper methods as privatedefis_admin?true# some crazy Ruby logic!enddeflatest_usersUser.last(10) # calling ActiveRecord models for exampleendend
Render?
Use the render? method to conditionally render the component based on custom rules:
classAdminComponent<Matestack::Ui::Component required :userdefrender? context.user.admin?enddefresponse div id: "admin-component"do plain "This component should only get rendered for admins"endendend
This is particularly useful to avoid plastering your views with conditional statements like if and unless.
Use a prepare method to resolve instance variables before rendering a component if required.
classSomeComponent<Matestack::Ui::Componentdefprepare @some_data ="some data"enddefresponse div id: "my-component"do plain @some_dataendendend
classExamplePage<Matestack::Ui::Pagedefresponse div id: "div-on-page"doSomeComponent.()endendend
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:
classSomeComponent<Matestack::Ui::Componentdefresponse div id: "my-component"do plain params[:foo]endendend
On the example page, reference the component as usual.
classExamplePage<Matestack::Ui::Pagedefresponse div id: "div-on-page"doSomeComponent.()endendend
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...
classExamplePage<Matestack::Ui::Pagedefresponse div id: "div-on-page"doSomeComponent.(foo: "bar")endendend
...this hash is accessible via options in the component:
classSome::Component<Matestack::Ui::Componentdefresponse div id: "my-component"do plain options[:foo]endendend
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:
You then can use these options simply by calling the provided OpenStruct object context, which includes the injected options with their name:
classSomeComponent<Matestack::Ui::Component required :some_property, :some_otherdefresponse# display some_property plain inside a div and some_other property inside a paragraph beneath it div do plain context.some_propertyend paragraph text: context.some_otherendend
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.
classSomeComponent<Matestack::Ui::Component optional :optional_property, :other_optional_property # optional properties could be emptydefresponse div do plain context.optional_propertyend paragraph context.other_optional_propertyendend
Passing more complex data structures to components
You can pass any object you like and use it in the component with the helper.
classSomeComponent<Matestack::Ui::Component required :some_option, optional :some_otherdefresponse div do plain context.some_optionendif context.some_other.present? paragraph context.some_other[:option]endendend
Use it in the example page and pass in one option as a hash
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
classSomeComponent<Matestack::Ui::Component required :foo, :bar, class: { as: :my_class }defresponse div class: context.my_class do plain "#{context.foo} - #{context.bar}"endendend
Text argument
Sometimes you just want to pass in a simple (text) argument rather than a hash with multiple keys:
classExamplePage<Matestack::Ui::Pagedefresponse div id: "div-on-page"do# simply pass in a string hereSomeComponent.("foo from page")endendend
A component can access this text argument in various ways:
classSome::Component<Matestack::Ui::Componentdefresponse 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 releasesendendend
This approach can be combined with injecting Hashes to components:
classExamplePage<Matestack::Ui::Pagedefresponse div id: "div-on-page"do# simply pass in a string hereSome::Component.("foo from page", { foo: "bar" })endendend
classSome::Component<Matestack::Ui::Component optional :foodefresponse 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.fooendendend
Yielding inside components
Components can yield a block with access to scope, where a block is defined.
classSomeComponent<Matestack::Ui::Componentdefresponse div id: "my-component"doyieldendendend
Pass a block to a component on the page as shown below:
classExamplePage<Matestack::Ui::Pagedefprepare @foo ="foo from page"enddefresponse div id: "div-on-page"doSomeComponent.() do plain @fooendendendend
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.
classSomeComponent<Matestack::Ui::Componentdefprepare @foo ="foo from component"enddefresponse div id: "my-component"do slot :my_first_slot br slot :my_second_slotendendend
Slots have access to the scope of the class, where they are defined. In this case @foo
classExamplePage<Matestack::Ui::Pagedefprepare @foo ="foo from page"enddefresponse div do some_component slots: { my_first_slot: method(:my_simple_slot), my_second_slot: method(:my_second_simple_slot) }endenddefmy_simple_slot span id: "my_simple_slot"do plain "some content"endenddefmy_second_simple_slot span id: "my_simple_slot"do plain @fooendendend
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:
classOther::Component<Matestack::Ui::Componentdefprepare @foo ="foo from other component"enddefresponse div id: "my-other-component"do slot :my_slot_from_component br slot :my_slot_from_page br plain @fooendendend
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:
classExamplePage<Matestack::Ui::Pagedefprepare @foo ="foo from page"enddefresponse div id: "page-div"do some_component slots: { my_slot_from_page: method(:my_slot_from_page) }endenddefmy_slot_from_page span id: "my-slot-from-page"do plain @fooendendend
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:
classSomeComponent<Matestack::Ui::Componentdefresponse div id: "my-component"doUser.last(10).each do|user| slot(:user_card, user)endendendend
classExamplePage<Matestack::Ui::Pagedefresponse div do some_component slots: { user_card: method(:user_card) }endenddefuser_carduser div class: "card"do plain user.nameendendend