komposable / komponent

An opinionated way of organizing front-end code in Ruby on Rails, based on components
http://komponent.io
MIT License
426 stars 31 forks source link

[Draft] How to migrate to ViewComponent #170

Open Spone opened 3 years ago

Spone commented 3 years ago

This is an evolving draft of the steps needed to migrate your project and components from Komponent to ViewComponent. Please add your notes and comments below.

Goals

List the steps and gotchas when migrating. This is preliminary work to hopefully provide tools to ease the transition.

Preparation

In my case, I use frontend/ as my root folder, and I want to use ViewComponent's sidecar assets.

- config.i18n.load_path += Dir[config.root.join('frontend/components/**/*.yml')]
+ config.i18n.load_path += Dir[config.root.join('app/components/**/*.yml')]
- config.autoload_paths << config.root.join('frontend/components')
+ config.komponent.root = Rails.root.join('app')
- source_path: frontend
+ source_path: app/javascript

(I also had to add extract_css: true, not sure why yet)

- import "../components";
+ import "../../components";
- import "components/button/button";
+ import "./button/button";

If you have namespaced components, find their index.js and replace the import paths as well.

function importAll(r) {
  r.keys().forEach(r);
}

importAll(require.context("../components", true, /_component.js$/));

Migrating a component

Let's say you have a component named example.

- import "./example/example";

Now take a deep breath, because it will get tedious.

If your component has a Stimulus controller

import { Application } from "stimulus";
+ import { definitionsFromContext } from "stimulus/webpack-helpers";

const application = Application.start();
+ const context = require.context("./controllers", true, /\.js$/);
+ const componentsContext = require.context("../components", false, /_controller\.js$/);
+ application.load(
+   definitionsFromContext(context).concat(
+     definitionsFromContext(componentsContext)
+   )
+ );

export default application;
-import application from "stimulus_application";
 import { definitionsFromContext } from "stimulus/webpack-helpers";
+import application from "../../javascript/stimulus_application";
import "./example_component.css";

const context = require.context("./", true, /_controller\.js$/);
application.load(definitionsFromContext(context));

This trick (calling one application.load for each component) prevents you from having the -- in the controller name when placing the file in a sidecar directory, as described here.

Tools

Here is a first try at automating the last part (replacing all occurrences of rendering a given component);

Warning: this is a work in progress, not ready for use. Help needed!

namespace :komponent do
  namespace :migrate do
    desc "Migrate component renders to view_component syntax"
    task :render, [:component_name] => :environment do |_t, args|
      component_name = args.component_name
      puts "Migrating component #{component_name}"

      # This regexp only finds the beginning of the component rendering call.
      # It does not handle the parameters, so you will have a missing `))` at the end.
      # FIXME: how can we improve this?
      search_regexp = /c(omponent)?[ (]{1}"#{Regexp.quote(component_name)}",?\s+/

      component_klass = component_name.classify
      replacement = "render(#{component_klass}Component.new("

      # FIXME: Replace `slim` with your templating language of choice here.
      file_names = Dir.glob(Rails.root.join("**/*.slim"))

      file_names.each do |file_name|
        text = File.read(file_name)
        new_contents = text.gsub(search_regexp, replacement)

        File.open(file_name, "w") {|file| file.puts new_contents }
      end
    end
  end
end

That's all for now. I will update this issue when I find edge cases.