Skip to content

espago/phlex-stimulus

Repository files navigation

Phlex::Stimulus

phlex-stimulus pairs every Stimulus controller in your app with a Phlex component, and keeps both sides strictly typed.

For each Stimulus controller (the TypeScript side) there is a matching Phlex component (the Ruby side). The component owns the markup that mounts the controller — the data-controller attribute, its targets, actions, and values — and exposes strictly Sorbet-typed helpers so the wiring between Ruby and JS can't silently drift.

Controllers can also be chained: a set of controllers can be rendered so that each one's connect logic runs to completion before the next one connects.

Installation

Install the gem and add to the application's Gemfile by executing:

bundle add phlex-stimulus

If bundler is not being used to manage dependencies, install the gem by executing:

gem install phlex-stimulus

Then run the installer.

bundle exec rails generate phlex:stimulus:install

Usage

Generating a controller + component

bundle exec rails generate phlex:stimulus:controller Summary

This creates a matched pair:

  • app/components/summary_controller.rb — a Components::SummaryController
  • app/javascript/controllers/summary_controller.ts — a SummaryController

Ruby controller components

A controller component declares the Stimulus controller name, its actions, and its targets. Those declarations generate strictly-typed helpers so your markup references controllers, actions, and targets by method call instead of by hand-written string:

# typed: strict

module Components
  class SummaryController < Controller
    self.controller_name = 'summary'

    actions :redirect          # => defines SummaryController.redirect_action  #=> "summary#redirect"
    targets :body              # => defines SummaryController.body_target       #=> "body"

    #: (title: String) -> void
    def initialize(title:)
      @title = title
    end

    # @override
    #: ?{ -> void } -> void
    def view_template(&block)
      div(data: { controller: self.class.controller_name, title: @title }) do
        block&.call
      end
    end

  end
end

You can render a controller like any other Phlex component:

render SummaryController.new(title: 'Report') do
  # define an action
  button(data: { action: event('click', SummaryController.redirect_action) }) { 'Go' }

  # define a target
  div(data: { SummaryController.target_key => SummaryController.body_target }) do
    plan "Foo"
  end
end

Controller component helpers

_action

Each action you define will result in a corresponding _action method being available in Ruby. Sorbet is fully aware of these methods thanks to a tapioca compiler.

These methods just return the name of the controller together with the name of the action: "controller#action". They mostly exist so that sorbet can check if the actions your using when building HTML elements actually exist.

You can read more about actions here.

class SummaryController < Controller
  self.controller_name = 'summary'

  actions :foo
end

SummaryController.foo_action #=> "summary#foo"

This can be used to attach an action to an HTML element with full type safety.

div(data: { action: event('click', SummaryController.foo_action) })

This is the same as:

div(data: { action: 'click->summary#foo' })

_target

Each target you define will result in a corresponding _target method being available in Ruby. Sorbet is fully aware of these methods thanks to a tapioca compiler.

These methods just return the name of the target so they may seem pointless. They exist only so that sorbet can check if the actions your using when building HTML elements actually exist.

You can read more about targets here.

class SummaryController < Controller
  self.controller_name = 'summary'

  targets :foo
end

SummaryController.foo_target #=> "foo"

You would use it like so to attach a target with full type safety:

div(data: { SummaryController.target_key => SummaryController.foo_target })

This is the same as:

div(data: { 'summary-target' => 'foo' })

target_key

This method returns the key that can be used to attach targets to the controller.

class SummaryController < Controller
  self.controller_name = 'summary'

  targets :foo
end

SummaryController.target_key #=> "summary-target"

You would use it like so to attach a target with full type safety:

div(data: { SummaryController.target_key => SummaryController.foo_target })

This is the same as:

div(data: { 'summary-target' => 'foo' })

dispatched

This method helps you get the names of custom events dispatched by a stimulus controller. You can read more about event dispatching here.

class SummaryController < Controller
  self.controller_name = 'summary'
end

SummaryController.dispatched('redirected') #=> "summary:redirected"

You would use it like so to define an action on an dispatched event:

div(data: { action: event(SummaryController.dispatched('redirected'), OtherController.do_action) })

This is the same as:

div(data: { 'summary:redirected' => 'other#do' })

Component Helpers

There are some useful helper methods you can use in every phlex component.

attrbool

attrbool converts a Ruby boolean to 'true' or nil so the boolean value can be used in attributes of HTML elements

attrbool(true) #=> "true"
attrbool(false) #=> nil

You would use it when passing a boolean value from Ruby to an HTML attribute.

button(data: { enabled: attrbool(@enabled) })

event

event let's you easily and safely create stimulus action strings.

event('click', 'summary#redirect') #=> "click->summary#redirect"
event('click', SummaryController.redirect_action) #=> "click->summary#redirect"

You would use it when attaching a controller action to some HTML element.

button(data: { action: event('click', SummaryController.redirect_action) })

class_list

class_list let's you easily add classes to html elements conditionally.

class_list("foo", "bar", "baz") #=> "foo bar baz"

error = false
class_list("foo", "red" if error, "bar") #=> "foo bar"

error = true
class_list("foo", "red" if error, "bar") #=> "foo red bar"

You would use it when declaring an HTML element.

div(class: class_list("foo", "red" if error, "bar")) do
  plain "Hello!"
end

class_list is aliased as strlist as it may be useful for other things than CSS classes.

Stimulus controllers

There is a TypedController factory that let's you define strict types for controller targets.

import { application } from "./application"
import { TypedController } from "./typed_controller"

class SummaryController extends TypedController<HTMLElement, { body: HTMLDivElement }>() {
  static targets = ["body"]

  redirect() {
    this.bodyTarget.innerText = "…"   // fully typed
  }
}

application.register("summary", SummaryController)

Chaining controllers

Sometimes controllers must connect in a specific order — each one finishing its setup before the next starts. phlex-stimulus supports this on both sides.

On the TypeScript side, a chainable controller extends ChainableController (or TypedChainableController for typed targets) and puts its connect logic in init(). When init() resolves, the controller dispatches a ready event that the chain waits on:

class LogController extends ChainableController<HTMLElement> {
  async init() {
    console.log("connected")
  }
}

On the Ruby side, render your controller components through Components::ControllerChain. It renders each controller inside a deferred <template> and drives them via the chain controller so they attach one after another:

render Components::ControllerChain.new(
  Components::SleepController.new(timeout: 500),
  Components::LogController.new(message: "ready!"),
)

This would result in a "ready!" console message getting displayed after 500 milliseconds.

Two of the bundled controllers are especially useful as chain building blocks:

  • Components::SleepController.new(timeout:) — pauses the chain for the given number of milliseconds.
  • Components::LogController.new(message:, on_event:)console.logs a message when it connects (handy for debugging a chain).

You can look those up as examples to see how you can implement your own chainable controllers.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/espago/phlex-stimulus. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

About

Stimulus.js and Sorbet support for Phlex components

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors