I wanted to try ViewComponentReflex, which is something that Rails can implement the process of rewriting values with ajax, so I tried various notes.
https://github.com/joshleblanc/view_component_reflex
--I don't like JavaScript so much, so I wanted to write it only on the server side if I only wanted to update the data on the screen. --It may be a way of thinking that is incompatible with people who want to do various things with JavaScript + API
ViewComponentReflex is an extension of ViewComponent. Therefore, first of all, minimum knowledge of ViewComponent is required.
Also, the part (miscellaneous) that communicates with an ajax-like feeling is written based on stimulus_reflex, but it is in a good form without being aware that it is just a rough process.
From here, we will first roughly describe ViewComponent, and then look at ViewComponentReflex.
--To enable the HTML spit out by Rails to be componentized like React etc. ――When componentized, it is convenient to test the view and it is easy to follow the data.
Rails already has partials for componentizing parts of HTML.
Something like this
//Caller
<div>
<%= render :partial => "(template_name))" %>
</div>
//Called side
<p>This guy is displayed</p>
//result
<div>
<p>This guy is displayed</p>
</div>
Roughly speaking, the following is different.
--ViewComponent is faster to load ――It is written that it is about 10 times faster, but it depends on the situation --Easy to test View --Easy to write View test code
To be honest, it looks like an evolved version of partial.
(personally...) Partial is simpler for static HTML, such as embedding Google Analytics tags. If you want to expand variables in a list etc., ViewComponent is faster and better. If you want to separate CSS etc. into components, ViewComponent seems to be better even without variable expansion.
From here, a memo when implementing the sample according to the guide. Since it is implemented in slim, please read it as appropriate.
https://viewcomponent.org/
Installation Rails 6.1 fits in smoothly. Monkey patch is required for Rails 5.0+. It may be easier to upgrade to Rails 6.1 first.
gem "view_component", require: "view_component/engine"
If you define Component in Ruby, HTML in the same directory will be loaded automatically.
To specify a variable for Component, specify it with initialize
.
app/components/example/component.rb
module Example
class Component < ViewComponent::Base
def initialize(title:)
@title = title
end
end
end
app/components/example/component.html.slim
h1 = @title
When calling, render Example :: Component.new
will be done on the View side.
If there are no variables, just new is needed.
Caller.slim
= render Example::Component.new(title: 'hogehoge')
If you write this roughly, the HTML with the expanded components will be output.
Output result.html
<h1>hogehoge</h1>
If you want to pass not only variables but also HTML itself to the component, do as follows.
This is the same as the above-mentioned normal time.
Because it automatically expands block elements to content
without using special variables.
app/components/example2/component.rb
module Example2
class Component < ViewComponent::Base
def initialize(title:)
@title = title
end
end
end
Place content
where you want the block element to expand.
app/components/example2/component.html.slim
h1 = @title
div
= content
I will pass HTML as a block element at the time of calling.
Caller.slim
= render Example2::Component.new(title: 'hogehoge') do |component|
h2
|You can write various HTML here
Block elements are automatically expanded to content
.
Output result.html
<h1>hogehoge</h1>
<div><h2>You can write various HTML here</h2></div>
You can name it according to the location you want to expand, not just content
.
This is convenient when you want to describe header
and hooter
separately.
Name the content areas with with_content_areas
.
app/components/example3/component.rb
module Example3
class Component < ViewComponent::Base
with_content_areas :header, :footer
def initialize(title:)
@title = title
end
end
end
Place a named content area. content
can be used at any time.
app/components/example3/component.html.slim
div
= header
div
= content
div
= footer
By creating a block of component.with (: content area name)
at the time of calling, that element is expanded in the content area.
Caller.slim
= render Example3::Component.new(title: 'hogehoge') do |component|
= component.with(:header) do
|This is the header
= component.with(:footer) do
|This is a footer
h2
|This will be developed as content
Expanded to the corresponding location of header
and footer
.
Also, the part not specified in component.with
is expanded as content
, so the contents of h2 are displayed in the middle.
Output result.html
<div>This is the header</div>
<div>
<h2>This will be developed as content</h2>
</div>
<div>This is a footer</div>
Slots ViewComponent has a mysterious concept called Slots. Slots allow you to call content consisting of multiple blocks from a single ViewComponent.
Roughly speaking, the caller is a single line, but it will be able to handle nested components.
The type changes depending on whether it is read only once or multiple times.
renders_one Used when setting a component that is read only once, such as a header, as a slot.
Example: renders_one: header
renders_many Set to a component that is called multiple times from one parent component, such as a comment to an article.
Example: renders_many: comments
It is divided into three types.
This is a simple form.
Define the parent component.
app/components/example4/component.rb
module Example4
class Component < ViewComponent::Base
include ViewComponent::SlotableV2
#If it is in the same class, specify the component with a character string
renders_one :header, 'HeaderComponent'
#When calling from another file, specify the class directly
#Renders to call the post component multiple times_many is specified
renders_many :posts, ExamplePost::Component
class HeaderComponent < ViewComponent::Base
attr_reader :title
def initialize(title:)
@title = title
end
end
end
end
You can call the header
and posts
defined above from within the parent component.
By doing so, each child component will be called.
app/components/example4/component.html.slim
div
/ TODO:For some reason html is expanded as a character string, so html_with escape
= html_escape header
div
- posts.each do |post|
= html_escape post
The contents of each child component look like this.
app/components/example4/header_component.html.slim
div
h1 = @title
div
= content
app/components/example_post/component.html.slim
h5 = @title
app/components/example_post/component.rb
module ExamplePost
class Component < ViewComponent::Base
def initialize(title:)
@title = title
end
end
end
The caller calls the parent component directly.
Also, the caller sets the value to the component set by renders_one
and renders_many
.
Of course, the caller may loop around here in the form of @ posts.each ~ ~
.
Caller.slim
div
= render Example4::Component.new do |c|
= c.header(title: "hogehoge") do
It's the contents of the p header
= c.post(title: "The first one")
= c.post(title: "Second")
Just by calling the parent component, you can see that the child component is also expanded without permission.
Output result.html
<div>
<div>
<h1>hogehoge</h1>
<div>
<p>It's the contents of the header</p>
</div>
</div>
</div>
<div>
<h5>The first one</h5>
<h5>Second</h5>
</div>
Only one renders_one
can be read.
If you have more than one as shown below, you will win later (that is, hogehoge2 will be displayed).
Multiple renders_one example.slim
div
= render Example4::Component.new do |c|
= c.header(title: "hogehoge1") do
= c.header(title: "hogehoge2")
Also, when calling multiple times with renders_many
, you can pass an array in the form of posts
.
div
= render Example4::Component.new do |c|
= c.posts([{title: "The first one"}, {title: "Second"}])
The case of transfer can also be written as follows. This is more convenient when you want to insert processing in between.
app/components/example4/component.rb
module Example4
class Component < ViewComponent::Base
include ViewComponent::SlotableV2
renders_one :header, -> (title:) do
HeaderComponent.new(title: title)
end
renders_many :posts, -> (title:) do
#This is convenient when you want to change the title a little, or when you want to control the display contents by passing variables.
ExamplePost::Component.new(title: title + '_hogehoge')
end
class HeaderComponent < ViewComponent::Base
attr_reader :title
def initialize(title:)
@title = title
end
end
end
end
app/components/example5/component.rb
module Example5
class Component < ViewComponent::Base
include ViewComponent::SlotableV2
renders_one :header
renders_many :posts
end
end
It is also possible to call it by defining it roughly as described above. This shape is simple if the components are well designed.
Caller.slim
div
= render Example5::Component.new do |c|
= c.header(title: "hogehoge")
= c.posts([{title: "The first one"}, {title: "Second"}])
It is also possible to create a component only with rb without writing a template.
app/components/example6/component.rb
module Example6
class Component < ViewComponent::Base
#The name call is set by default
def call
link_to 'link to root', root_path
end
# call_If you name it xxx, you can call it as a public method.
def call_other
link_to 'link to other', root_path
end
end
end
Caller.slim
div
= render Example6::Component.new
br
/ with_variant(:By specifying other), call_Calling other
= render Example6::Component.new.with_variant(:other)
It can be controlled by using render?
.
This makes it possible to exclude the display control logic from the View.
app/components/example7/component.rb
module Example7
class Component < ViewComponent::Base
def initialize(is_show:)
@is_show = is_show
end
def render?
@is_show == true
end
end
end
Caller.slim
div
= render Example7::Component.new(is_show: true)
= render Example7::Component.new(is_show: false)
Output result.html
<div><p>Only one is displayed</p></div>
Preprocessing can be done before rendering. I think it's better to do it with initialize, but this seems to be faster when looping multiple times.
app/components/example8/component.rb
module Example8
class Component < ViewComponent::Base
def before_render
@title = 'Pre-rendered title'
end
def initialize; end
end
end
app/components/example/component.rb
module Example
class Component < ViewComponent::Base
#The parameter name passed for collections is hoge by default_component.hoge part of rb
#If you want to change this with_collection_Must be named as parameter
with_collection_parameter :title
def initialize(title:)
@title = title
end
end
end
Caller.slim
div
- titles = ["hoge", "fuga"]
= render Example::Component.with_collection(titles)
app/components/example9/component.rb
module Example9
class Component < ViewComponent::Base
with_collection_parameter :title
# _The name counter sets a count for the number of times the collection is looped.
def initialize(title:, title_counter:)
@title = title
@counter = title_counter
end
end
end
Caller.slim
div
- titles = ["hoge", "fuga"]
= render Example9::Component.with_collection(titles)
Place various items under the app/components
directory.
From the viewpoint of making it easy to understand as a component, I think that it is better to create a directory for each component for app/components/example
and put the necessary files in it.
Of course, there is no problem even if you put various things directly under app/components
.
app/components
├── ...
├── example
| ├── component.rb
| ├── component.css
| ├── component.html.slim
| └── component.js
├── ...
//When calling, the folder name is considered as the namespace, so it will be in the following form
<%= render(Example::Component.new(title: "my title")) do %>
Hello, World!
<% end %>
The following is for rspec.
It is necessary to set to generate convenient methods such as render_inline
.
spec/rails_helper.rb
require "view_component/test_helpers"
RSpec.configure do |config|
config.include ViewComponent::TestHelpers, type: :component
end
I think it's convenient because it makes it easier to write View tests.
require 'rails_helper'
RSpec.describe Example::Component, type: :component do
it 'renders something useful' do
render_inline(described_class.new(title: 'hoge'))
assert_includes rendered_component, 'hoge'
#When capybara is included, you can also use capybara's matcher
assert_text('hoge')
end
end
You can see a preview of the component, like ActionMailer :: Preview
.
It seems to be useful when writing CSS individually.
The following is required as the initial setting.
config/application.rb
config.view_component.preview_paths << "#{Rails.root}/spec/components/previews"
The settings for viewing the preview are as follows. Just create a preview class and method and call it from there.
spec/components/previews/example_preview.rb
class TestComponentPreview < ViewComponent::Preview
#Considering that it is a component, it seems better not to set layout
#Is it set according to taste?
layout false
def default_title
render(Example::Component.new(title: 'Test component default'))
end
end
The above files can be accessed here. http://127.0.0.1:3000/rails/view_components
From here, we finally start talking about ViewComponent Reflex.
I'm not sure about it, so I'm trying this. Roughly speaking, let's describe everything on the server side that changes the value with ajax.
Since you need Redis in ActionCable, prepare Redis in some way. If you have completed various settings for ViewComponent, you do not need to do anything else.
app/components/example10/component.rb
module Example10
class Component < ViewComponentReflex::Component
def initialize
@count = 0
end
def increment
@count += 1
end
end
end
All you have to do is write refrex_tag
and specify the method defined on the component side.
app/components/example10/component.html.slim
= component_controller do
p
= @count
= reflex_tag :increment, :button, "Click"
Caller.slim
div
= render Example10::Component.new
Only this is written, but the number is incremented by communicating with the server side.
If you want to change to mouse enter etc. instead of click event, write as follows.
app/components/example10/component.html.slim
= component_controller do
p
= @count
= reflex_tag "mouseenter->increment", :button, "Click"
Just hover your mouse cursor over it to increase the number.
When component_controller is used, the variable key is automatically generated. By using this key, you can move the processing of other components.
parent.slim
= component_controller do
div#loader
- if @loading
p loading
= reflex_tag :do_action, :button, "Click"
/ component_The key is automatically generated by the controller
= render Child::Component.new(parent_key: key)
parent.rb
module Parent
class Component < ViewComponentReflex::Component
def initialize
@loading = false
end
#Processing called by the child
def update_loading
@loading = !@loading
#The process of updating a particular selector in a component
# prevent_refresh!There is also a process that does not accept updates
# refresh_all!Then, update all body elements
refresh! '#loader'
end
end
end
child.rb
module Child
class Component < ViewComponentReflex::Component
def initialize(parent_key:)
@parent_key = parent_key
end
def stimulate_parent
#Hit the parent's process with stimulate
# parent_The component is linked with the key
stimulate("Parent::Component#update_loading", { key: @parent_key })
end
end
end
Implement separately for the parent element that holds the element collection itself and the child element that displays the element collection.
parent.rb
class MyUserModel
attr_accessor :id, :name
def initialize(id:, name:)
@id = id
@name = name
end
end
module Parent
class Component < ViewComponentReflex::Component
def initialize(users:)
@users = users
end
def add_user
#Normally, I think that data is passed from the outside, but it is a sample.
#It's hard to know how much to do in ViewComponent. There are times when you need to redisplay from data storage.
@users.append(MyUserModel.new(id: @users.length + 1, name: "#{(@users.length + 1).to_s}Ro"))
end
end
end
child.rb
module Child
class Component < ViewComponentReflex::Component
#If you forget to define this, the argument will not be read properly \
with_collection_parameter :user
def initialize(user:)
@user = user
end
#I get angry if I don't set this
def collection_key
@user.id
end
end
end
parent.slim
= component_controller do
/Call the component (child) that displays the collection
= render Child::Component.with_collection(@users)
= reflex_tag :add_user, :button, "Increase Taro"
child.slim
/Just displaying
= component_controller do
= @user.id
= @user.name
Caller.slim
div
= render Parent::Component.new(users: [MyUserModel.new(id:1, name:'1ro')])
All you have to do is apply these.
I have some quirks, and there are still many things that are missing, but if it's a light service, I think it's a good idea to try it. It seems to be easy to use if it is reloaded after posting comments on the bulletin board etc. It is difficult who should update the data and so on. I don't know the best practice.