В статье Неофициальный гайд по Active Admin мы рассмотрели библиотеки, из которых состоит Active Admin, и некоторые подходы к созданию собственных компонентов. Но иногда форма или информационный виджет могут быть настолько сложными или динамическими, что начинаешь задумываться, не пора ли обратиться к современному фронтенду, а не накручивать такой родной, но такой неподдерживаемый jQuery-код.

Мы в ДомКлик в таких случаях используем Vue.js и React, и под катом я расскажу, как именно можно внедрить React-компонент в Active Admin.

Рассмотрим идеальный вариант, когда Active Admin использует Webpacker для управления ресурсами. О настройке этого режима можно прочитать в официальной документации.

Active Admin с Webpacker

Начнем с того, что добавим и установим гем react-rails (подробнее в README):

# Gemfile
gem 'react-rails', '~> 2.6'
bundle install
rails webpacker:install:react
rails generate react:install

Теперь добавим React-компонент hello world в директорию app/javascript/components:

// app/javascript/components/Hello.jsx
import * as React from 'react';

const Hello = ({name}) => {
  return(
    <div>Hello {name}!</div>
  );
};

export default Hello;

Чтобы компоненты были видны в Active Admin, добавим следующий код в app/javascript/packs/activeadmin.js:

// app/javascript/packs/activeadmin.js
// ...

// Support component names relative to this directory:
var componentRequireContext = require.context("components", true);
var ReactRailsUJS = require("react_ujs");
ReactRailsUJS.useContext(componentRequireContext);

Теперь добавим новый Arbre-компонент, который позволит нам с легкостью вызывать React-виджеты (подробнее об Arbre-компонентах можно прочитать в статье Неофициальный гайд по Active Admin):

# app/admin/components/react_component.rb
module Admin
  module Components
    # Defines a `react_component` arbre component. It allows
    # to include react components into Active Admin using react-rails.
    #
    # @example
    #
    # show do
    #   panel do
    #     react_component 'Post', { title: 'My post' }
    #   end
    # end
    #
    class React < ActiveAdmin::Component
      class ReactComponentBuilder
        include ::React::Rails::ViewHelper
        include ActionView::Helpers::TagHelper
      end

      builder_method :react_component

      def to_s
        ReactComponentBuilder.new.react_component(*@args, &@block)
      end

      def build(*args, &block)
        @args = args
        @block = block
      end
    end
  end
end

И наконец, вызовем компонент в dashboard.rb:

# app/admin/dashboard.rb
ActiveAdmin.register_page "Dashboard" do
  menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }

  content title: proc { I18n.t("active_admin.dashboard") } do
    react_component 'Hello', { name: 'David' }
  end
end

Вы великолепны! Теперь можете использовать React-компоненты в Active Admin!

Бонус: убираем Sprockets из проекта

Если Assets Pipeline больше ни для чего не используется, то почему бы от него не отказаться? Для этого убедитесь, что Active Admin обновлен до версии 2.9, и затем:

  • удалите require 'sprockets/railtie' из config/application.rb (если этой строки нет, но есть require 'rails/all', то посмотрите, во что раскрывается этот require);

  • удалите строку config.assets.compile = false из config/environments/production.rb;

  • удалите строки config.assets.debug = true и config.assets.quiet = true из config/environments/development.rb;

  • удалите gem 'sass-rails' из Gemfile;

  • удалите initializers/assets.rb;

  • и наконец, удалите директорию app/assets со всем её содержимым.

Active Admin с Assets Pipeline

Если в вашем проекте для управления ресурсами Active Admin используется Sprockets, то react-rails можно подключить через Assets Pipeline (подробнее в README). Я смог запустить таким образом hello.jsx из примера выше, но, как мне показалось, это достаточно сложный для поддержки подход, поэтому я бы советовал добавить параллельно webpacker и собирать React-компоненты через него.

Для начала добавьте gem "webpacker"в Gemfile, затем установите react-rails, добавьте Arbre-компонент и React-компонент, как мы это делали выше.

Единственная разница в том, что у нас нет файла app/javascript/packs/activeadmin.js; давайте создадим его:

// app/javascript/packs/activeadmin.js

// Support component names relative to this directory:
var componentRequireContext = require.context("components", true);
var ReactRailsUJS = require("react_ujs");
ReactRailsUJS.useContext(componentRequireContext);

Теперь еще одно отличие: из-за того, что мы работаем в режиме sprockets, Active Admin не подключает данный pack, поэтому нам необходимо сделать это вручную. Например, вот так:

# app/admin/dashboard.rb
ActiveAdmin.register_page "Dashboard" do
  menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }

  content title: proc { I18n.t("active_admin.dashboard") } do
    script src: asset_pack_path("activeadmin.js")
    react_component 'Hello', { name: 'David' }
  end
end

Есть и более «чистые» варианты, например, можно написать подобное расширение Active Admin:

# lib/active_admin/extra_headers.rb

module ActiveAdmin
  module ExtraHeaders
    def build_active_admin_head(*args)
      super
      return unless controller.respond_to?(:extra_headers)

      within head do
        controller.extra_headers.each do |extra_header|
          text_node(extra_header)
        end
      end
    end
  end
end

Теперь добавим расширение в начало инициализатора:

# config/initializers/active_admin.rb

require_relative '../../lib/active_admin/extra_headers'

ActiveAdmin::Views::Pages::Base.send(:prepend, ActiveAdmin::ExtraHeaders)

Ну и наконец изменим app/admin/dashboard.rb:

# app/admin/dashboard.rb
ActiveAdmin.register_page "Dashboard" do
  menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }

  content title: proc { I18n.t("active_admin.dashboard") } do
    react_component 'Hello', { name: 'David' }
  end

  controller do
    def extra_headers
      [helpers.javascript_pack_tag('activeadmin')]
    end
  end
end

Вот такой хак. Интерфейс можно сделать еще удобнее, но я остановлюсь на этом варианте. Не забудьте перезагрузить приложение, чтобы применились изменения в инициализаторе.

React-rails и Webpacker в бою

Приблизим руководство к боевым условиям: добавим в проект поддержку Typescript и внешнюю библиотеку с react-компонентом, и вызовем его с помощью .tsx-файла.

Начнем с настройки Typescript:

rails webpacker:install:typescript
yarn add -D @types/react @types/react-dom

Теперь переведем наш тестовый компонент из .jsx в .tsx:

import * as React from 'react';

const Hello = ({name}: {name: string}) => {
  return(
    <div>Hello {name}!</div>
  );
};

export default Hello;

В качестве примера внешней библиотеки возьмем react-vis. Начнем с добавления зависимости:

yarn add -D react-vis @types/react-vis

Добавим новый компонент app/components/javascript/Plot.tsx:

// app/components/javascript/Plot.tsx

import * as React from 'react';
import 'react-vis/dist/style.css';
import { XYPlot, LineSeries, LineSeriesPoint } from 'react-vis';

type Params = {
  data: LineSeriesPoint[];
}
const Plot = ({data}: Params) => {
  return (
    <div className="Plot">
      <XYPlot height={300} width={300}>
        <LineSeries data={data} />
      </XYPlot>
    </div>
  );
};

export default Plot;

Проверим работу нового компонента:

# app/admin/dashboard.rb

ActiveAdmin.register_page "Dashboard" do
  menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }

  content title: proc { I18n.t("active_admin.dashboard") } do
    react_component 'Hello', { name: 'David' }
    data = [
      { x: 0, y: 5 },
      { x: 2, y: 7 },
      { x: 5, y: 8 },
      { x: 7, y: 7 },
      { x: 8, y: 3 },
      { x: 6, y: 0 },
      { x: 0, y: -5 },
      { x: -6, y: 0 },
      { x: -8, y: 3 },
      { x: -7, y: 7 },
      { x: -5, y: 8 },
      { x: -2, y: 7 },
      { x: 0, y: 5 }
    ]
    react_component 'Plot', { data: data }
  end
end

Ну вот и всё, теперь вы знаете, как добавить React в Active Admin, и наверняка смотрите в светлое будущее взором, полным решимости использовать (или не использовать) это знание на практике.