joshleblanc / view_component_reflex

Call component methods right from your markup
http://view-component-reflex-expo.grep.sh/
MIT License
291 stars 27 forks source link

My take on view_component_reflex with --sidecar nested. #24

Open sebyx07 opened 4 years ago

sebyx07 commented 4 years ago

This is how I use view_component_reflex with the --sidecar layout from view_component.

first the dir structure

screen21

Before putting the .js & .scss you must change the application.js to load controllers properly. Here is mine:

import { Application } from "stimulus";
require("@rails/ujs").start();
require("turbolinks").start();
require("@rails/activestorage").start();
require("channels");
import StimulusReflex from 'stimulus_reflex';
import consumer from '../channels/consumer';
import controller from '../controllers/application_controller';

const application = Application.start()
const context = require.context("../../components", true, /_controller\.js$/)
function getDefinitionsFromContext(ctx){
  return ctx.keys().map((path) => {
    const splitPath = path.split("/");
    const identifier = splitPath.slice(1, splitPath.length - 1).join("/").replace("_component", "");
    const obj = ctx(path);

    const klassKey = Object.keys(obj)[0];
    const controllerConstructor = obj[klassKey];

    return {
      identifier,
      controllerConstructor
    };
  });
}

application.load(getDefinitionsFromContext(context));
StimulusReflex.initialize(application, { consumer, controller, debug: false });

My application_controller.js, which is pretty standard

import { Controller } from 'stimulus'
import StimulusReflex from 'stimulus_reflex'

export default class ApplicationController extends Controller {
  connect () {
    StimulusReflex.register(this)
  }

  beforeReflex (element, reflex, noop, reflexId) {
  }

  reflexSuccess (element, reflex, noop, reflexId) {
  }

  reflexError (element, reflex, error, reflexId) {
  }

  afterReflex (element, reflex, noop, reflexId) {
  }
}

I'll skip example_component.rb, example_component.html.erb because they are standard

Here is the example_component_controller.js, which also links the .scss file

import "./example_component.scss" import ApplicationController from "../../../javascript/controllers/application_controller";

import "./example_component.scss"
import ApplicationController from "../../../javascript/controllers/application_controller";

export class ExampleComponentController extends ApplicationController {
  connect() {
    super.connect();
    console.log('ok');
  }
}

finally the .scss which is scoped to this component, nothing fancy, but usefull

div[data-controller="my/example"]{
  background-color: red;
}

Would be cool if someone will write a generator for this so we could just

rails g component my_awesome/nested_component

sebyx07 commented 4 years ago

I'll leave application_component.rb

class ApplicationComponent < ViewComponentReflex::Component
end

& the example_component.rb just in case

module My
  class ExampleComponent < ApplicationComponent
    attr_accessor :count

    def initialize
      @count = 1
    end

    def increment
      self.count += 1
    end
  end
end

& the html

<%= component_controller do %>
  <p><%= count %></p>
  <%= reflex_tag :increment, :button, "Click" %>
<% end %>
olhor commented 3 years ago

Following @sebyx07 approach I modified stimulus controller naming resolvers to match following "sidecar" folder structure. Assuming we have a parent_component with nested first_child_component I wanted to have:

app/components/
├- parent_component.rb
└- parent_component/
    ├- parent_controller.js
    ├- parent_component.html.erb
    ├- first_child_component.rb
    └- first_child_component/
        ├- first_child_controller.js
        └- first_child_component.html.erb 

I had to modify getDefinitionsFromContext function in order to remove all _component parts from filenames and to follow stimulus naming conventions ("--" for namespace separation and "-" for word separation):

const cvContext = require.context("../../components", true, /_controller\.js$/)

function getDefinitionsFromContext(ctx){
  return ctx.keys().map((path) => {
    const splitPath = path.split("/");
    const identifier = splitPath.slice(1, splitPath.length - 1).join("--").replace(/_component/g, "").replace('_', '-');
    const obj = ctx(path);
    const klassKey = Object.keys(obj)[0];
    const controllerConstructor = obj[klassKey];

    return {
      identifier,
      controllerConstructor
    };
  });
}

application.load(getDefinitionsFromContext(cvContext))

and override self.stimulus_controller method from ViewComponentReflex::Component module to:

ViewComponentReflex::Component.module_eval do
  def self.stimulus_controller
    name.gsub('Component', '').underscore.dasherize.gsub("/", "--")
  end
end

This approach allowed component_controller helper method to generate following controllers:

...
<div data-controller="parent" key="123"></div>
...
<div data-controller="parent--first-child" key="456"></div>
...

and to be properly initialized by Stimulus framework.