はじめに
RailsでVue.jsを採用している場合、Vue SFCのscoped css
を利用してCSS周りの統制と取りたくなりますが、RailsのView層に色々要素を書いてしまうとそれも難しくなってきます。
vue-loader-v14.vuejs.org
なので、RailsのControllerから直接Vue.jsのSFCを指定してrender
するような仕組みができないか色々試してみたのでメモしておく📝
基本方針
以下のようにrender_vue_component
の引数にコンポーネントとprops
を渡すと<index-post-page />
、< show-post-page :post-id="id">
みたいなVueコンポーネントの要素を差し込んだhtmlを返却するような仕組みを検討します。
class PostsController < ApplicationController
def index
render_vue_component('index-post-page')
end
def show
render_vue_component('show-post-page', props: { ':post-id': params[:id] })
end
end
このようなケースだとinertiajs.js
を使えば良さそうにも思いましたが、単純にscoped cssでCSSの統制を取りたいという要求に対して少し過剰な印象を持ったので、Railsの仕組みに乗りつつ直接Vue SFC指定してcontrollerからviewをrenderするような感覚で実現できないか検討してみる。
inertiajs.com
github.com
実装したコード
render_vue_component
でVue SFCを直接指定できるように以下のようなtemplateとconcernを実装してみました。
render_vue_component
にコンポーネント名とprops
、mount_element_id
を渡すとmount_element_id
(default: 'vue-root')をid
として指定したdiv
タグの中にhelpers.content_tag(@component, '', @props)
で生成したタグ(ex. <show-post-page :post-id="id" >
)をcomponent/template.erb
に差し込んで返却します。
simpacker等を採用しておりjavascript_pack_tag
でentryのjs、stylesheet_pack_tag
でentryのcssを読み込むことが前提になってますがjavascript_entry_tag
、stylesheet_entry_tag
をoverrideすれば任意のタグに差し替えることができます。
entry
はデフォルトでcontroller名/action名
(ex. PostsController#show
-> posts/show
)が指定されるようにしてみました。
<div id="<%= mount_element_id %>">
<%= component_tag %>
</div>
<% content_for :stylesheet do %>
<%= stylesheet_entry_tag %>
<% end %>
<% content_for :javascript do %>
<%= javascript_entry_tag %>
<% end %>
module ComponentRenderable
extend ActiveSupport::Concern
included do
helper_method :component_tag, :javascript_entry_tag, :stylesheet_entry_tag
end
def render_vue_component(component, entry: nil, props: {}, mount_element_id: nil)
@component = to_kebab_case(component)
@props = props
@entry = entry.presence || default_entry
mount_element_id = mount_element_id.presence || default_mount_element_id
render defalt_template_path,
locals: { mount_element_id: mount_element_id, component: @component, entry: entry, props: @props }
end
def component_tag
helpers.content_tag(@component, '', @props)
end
def javascript_entry_tag
helpers.javascript_pack_tag(@entry, defer: true)
end
def stylesheet_entry_tag
helpers.stylesheet_pack_tag(@entry)
end
private
def default_mount_element_id
'vue-root'
end
def defalt_template_path
'component/template'
end
def default_entry
File.join(controller_name, action_name)
end
def to_kebab_case(text)
text.underscore.tr('_', '-')
end
end
(追記) 以下のようなrender inline
を使うとcomponent/temaplete.erb
消せるのでよりシンプルに実現できそうだった。
module ComponentRenderable
extend ActiveSupport::Concern
included do
helper_method :component_tag, :javascript_entry_tag, :stylesheet_entry_tag
end
def render_vue_component(component, entry: nil, mount_element_id: nil, props: {})
@component = to_kebab_case(component)
@options = props
@entry = entry.presence || default_entry
@mount_element_id = mount_element_id.presence || default_mount_element_id
render inline: template,
locals: { mount_element_id: @mount_element_id, component: @component, entry: entry, options: @options },
layout: true
end
def component_tag
helpers.content_tag(@component, '', @options)
end
def javascript_entry_tag
helpers.javascript_pack_tag(@entry, defer: true)
end
def stylesheet_entry_tag
helpers.stylesheet_pack_tag(@entry)
end
private
def template
<<~HTML
<div id="<%= mount_element_id %>">
<%= component_tag %>
</div>
<% content_for :stylesheet do %>
<%= stylesheet_entry_tag %>
<% end %>
<% content_for :javascript do %>
<%= javascript_entry_tag %>
<% end %>
HTML
end
def default_mount_element_id
'vue-root'
end
def default_entry
File.join(controller_name, action_name)
end
def to_kebab_case(text)
text.underscore.tr('_', '-')
end
end
利用例
以下のようなentryを実装してある前提で、
import { createApp } from "vue";
import ShowPostPage from "@js/components/pages/ShowPostPage.vue";
const app = createApp(ShowPostPage);
app.mount("#vue-root");
以下のようにcontroller内でrender_vue_component
を利用することで、
class PostsController < ApplicationController
include ComponentRenderable
def show
render_vue_component('show-post-page', props: { ':post-id': params[:id] })
end
end
以下のようなhtmlが返却され、Vueインスタンスのmount及び指定したpropsを指定してComponentがレンダリングされます。
<div id="vue-root">
<show-post-page :post-id="9"></show-post-page>
</div>
おわりに
とりあえず、そこまで多くのコードを書かずにViewを作らずControllerからVue.jsのSFCを指定してrenderするようなことは出来そうだなと思いましたが、多少なりとも独自の仕組みを作るとトレンドに合わせて独自の仕組みをメンテナンスするコストが発生するのが難しいところ。。。
CSSの統制とりたいというニーズにおいてはTailwind CSSをしたり、必ずしも独自の仕組みを用意する必要はないかもしれないのとView Componentのようなメジャーなライブラリでもscoped cssののような機能が議論されたりしてるので、こちらを期待するのも良いかもしれない。
github.com