はじめに
RailsでVue.jsを採用している場合、Vue SFCのscoped css
を利用してCSS周りの統制と取りたくなりますが、RailsのView層に色々要素を書いてしまうとそれも難しくなってきます。
なので、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するような感覚で実現できないか検討してみる。
実装したコード
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 %>
# frozen_string_literal: true 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
消せるのでよりシンプルに実現できそうだった。
# frozen_string_literal: true 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, # rubocop:disable Rails/RenderInline 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ののような機能が議論されたりしてるので、こちらを期待するのも良いかもしれない。