Back to the guide

Intents

An intent is a gateway to a component, along with relevant context. It encapsulates tools used to generate paths, checking feasibility and rendering links and buttons pointing other components within your application.

To instanciate an intent, use either the raw Compony.intent method or one of the helpers. All methods that use intents under the hood have a similar interface, especially regarding the first two positional arguments. The most commonly used forms are:

  • Compony.intent(:index, :users): point to a component by its comp and family name
  • Compony.intent(:show, User.first): pass the intent a single model and let it figure out the family name from model_name
  • Compony.intent(:list, current_user.quotes): pass the intent an active record collection and let it figure out the family name from model_name
  • Compony.intent(Components::Users::Index): point to a component by giving its class as a single argument

The returned intent can then be used to:

  • Retrieve the target component class using intent.comp_class
  • Build an instance of the target component using intent.comp
    • If a model was given when building the intent, such as in the second form above, the comp instance will contain it as @data.
  • Retrieve the path to the target component (only works for standalone components; will automatically set ID parameter if a model was given)
  • Retrieve the name that can be used to store the intent in a hash or similar
  • Retrieve the label that can be used to refer to the component (if a model was given, passes it to the target component's label block)
  • Check for feasibility using feasible?
  • Render a button to the target component which automatically includes all of the above along with the suitable behavior using render and passing a controller

An intent's behavior can be customized by passing some of the following keyword arguments when building it:

  • standalone_name allows you to point to another endpoint within your target component, which is especially useful for generating paths. Keep in mind that within each standalone name, multiple HTTP methods can exist. This argument defaults to nil, which is the main endpoint created by standalone.
  • method defines the HTTP verb within the standalone configuration that should be addressed. Defaults to :get, but can be overriden to be :patch, :put, :post or :delete. When generating a button, the turbo_method will be automatically be derived from this argument.
  • name overrides the auto-generated name of the intent, affecting the result of the reader of the same name.
  • label accepts two forms:
    • When passing a String, it will be returned as-is when returning the label, also affecting buttons generated by this intent.
    • When passing a Hash, any included key-value pair will be used as keyword arguments to the label reader method.
  • path allows altering generated paths and accepts either a String or a Hash just like label.
  • data and data_class will be given to the target component if/when it gets instanciated. This is used to point to resourceful components. If a model was passed as the second positional argument (instead of a family name), it will become data and this argument can be omitted. If data responds to model_name, it will be considered a model-like class.
  • feasibility_target and feasibility_action can be given to alter the behavior of the feasible? reader.
  • Any further arguments are passed to the initializer of the button if/when the intent gets rendered.

Helpers

In practice, you will rarely call Compony.intent directly, and likely never Compony::Intent.new. Instead, you will be interacting with one of the following helpers that will instanciate an intent under the hood and thus all accept similar arguments as those described above:

Compony.path

This helper is useful when generating paths without rendering any HTML, such as when redirecting. Internally, this builds an intent which will in turn use the target component's path block to generate a Rails path (String) pointing to the correct location. Any keyword arguments are passed to the intent's path method, allowing you to override the model (to refer to resourceful target components), as well as specifying a standalone_name or pass extra arguments to the target component's path block.

Examples:

redirect_to Compony.path(:index, :users) # Redirects to /users
redirect_to Compony.path(:show, @data.author) # Redirects to something like /authors/42
redirect_to Compony.path(:thank_you, @data) # Redirects to something like /feedbacks/42/thank_you
redirect_to Compony.path(:step_2, :registrations, accept_terms: :yes) # Redirects to something like /registrations/step_2?accept_terms=yes

render_intent

This is the preferred way of quickly rendering links or buttons to components from other parts of your application. The method is implemented twice:

  • When called from within a content block of a component, the method implemented in the RequestContext is used, which automatically detects the component from which it is called and passes it as parent_comp to the rendered button.
  • When called from a regular Rails view, the Rails helper method is used, which instanciates a button without passing a parent_comp.

Use the button argument to customize the generated button. Example:

setup do
  content do
    div render_intent(:show, User.first, style: :link, label: { format: :short })`
  end
end

In the example above, there is a lot more going on than it seems. Due to the specified style, a plain HTML link will be generated, pointing to the component Components::Users::Show and instanciating it with the first user as @data. Assuming that component inherits from Compony::Components::Show and did not override label or path, the link will be labelled "Show" (which is the default short format label generated by Compony's default pre-built Show component), and the href will be /users/:id where ID will automatically be User.first's ID (e.g. /users/1). However, if :show was prevented, the link will be strikethrough, non-clickable, greyed out and have a title explaining why it can't be clicked. Further, if the current user does not have authorization to display the target user, the link will not show up at all, and no HTML will be generated within the div.

render_sub_comp

This is used within a component's content block to instanciate another component and nest it within. Internally, the current component's sub_comp method is used and all arguments are passed to that.

For example, let us consider you want to build your user management's Show component to include the list of quotes belonging to the displayed user. Assuming you have already built Components::Quotes::List for quotes' Index component, this helper allows you to simply write:

class Components::Users::Show < Compony::Components::Show
  setup do
    # ...
    content :quotes do
      concat render_sub_comp(:list, @data.quotes)
    end
  end
end

This implicitely builds an intent which auto-detects the family name :quotes from @data.quotes, which is an active record collection and thus implements model_name. The List component is then instanciated with its @data being that very collection and the users's Show component as parent_comp, resulting in the proper nesting and display of the desired resources.

Compony.comp_class_for

This helper is useful for checking whether a component is implemented. For instance, when implementing abstract components to inherit from later, you can check for if Compony.comp_class_for(:destroy, family_name) to only provide some functionality of a Destroy component exists for the current family.

This method also has its sibling Compony.comp_class_for!, which will fail if no such component could be found. It is however mostly used internally.

Buttons and styles

Button components are used as presenters for intents, hyperlinks to other components or submit buttons. They are a central way to define how buttons all over the application should look like. Their interface is adapted to intents, creating a standardized "slim waist" that greatly simplifies linking between components. Note that the term "button" refers to how they look and not how they are actually implemented - actual HTML buttons have several disadvantages (e.g. requiring drop-in forms and not responding to Ctrl+Click or middle-click when the user would prefer a new tab).

Compony comes with two button styles:

  • :css_button is the default button style and creates a div that looks similar to a HTML button rendered in Firefox. If the class disabled is given, it is greyed out.
  • :link is mostly just a regular <a> tag, but will appear greyed out and strikethrough if the class disabled is given.

All button styles support the following keyword arguments to their initializer:

  • label is a String which will be displayed as the text of the link or button.
  • href is the url/path that the button points to. If nil, will change to javascript:void(0).
  • method takes a HTTP verb will generate a suitable turbo_method data attribute.
  • class will mostly let be as-is, but checked for the disabled class - if given, the style will be ovewritten.

Note that since buttons are full components, they can be nested into another component by providing parent_comp. If called from within a component's content block, the helper render_intent does this automatically.

As implicitely mentioned above, Compony buttons are referenced to by a name called a style (:css_button actually points to Compony::Components::Buttons::CssButton). When rendering an intent, the style can be passed as an argument: render_intent(:show, User.first, style: :link) and the intent will automatically instanciate the desired component class.

Note: it is possible to use a button component to submit a form. In order to achieve this, you must implement a hidden submit button (for handling keyboard Enter and Return), as well as pass onclick: "this.closest('form').requestSubmit(); return false;" as an argument. See the pre-built Form component's implementation for an example.

Adding your own styles

In your application, you will likely want to implement your own button styles. Create a component (e.g. Components::Commons::MyButton) and inherit from Compony::Components::Buttons::Link. Override the method prepare_opts! and don't forget to call super first. Then, go through any @comp_args that might be of interest to you and mutate @comp_args[:style] and/or @comp_args[:class] to suit your needs. Make sure to handle the class disabled, as intents will set them if the intent is not feasible. Note that if a user is lacking authorization to perform an intent, the intent will not even instanciate the button.

Once your button class is ready, register it in config/initializers/compony.rb with: Compony.register_button_style :my_button, '::Components::Commons::MyButton'. You can also change the default button style there using: Compony.default_button_style = :my_button.

If you have multiple kinds of buttons (e.g. dropdown items, pill-style buttons, compact forms etc.), you should create a separate style and button component class for every kind. This will make it easy to refer to them by supplying something like style: :dropdown_item in render_intent.

Exposed intents

Components can expose a set of intents to be displayed elsewhere. Those can either be rendered by the parent comp, or by the application layout itself in case the component exposing them is currently root_comp (see the chapter about standalone). This is useful if you have something like an actions toolbar that changes depending on the currently contained component.

To expose an intent, proceed as shown in the following example:

# ...
class Components::Quotes::Show < Compony::Components::Show
  setup do
    exposed_intents do
      add :index, family_name, label: 'Show all', name: :index
      remove :destroy
    end
  end
end

In this example, the shown component exposes an intent pointing to the Index component of it's own family (:users). The :name argument causes the new intent to be just named :index rather than :index_users. This is for the sake of the example and would allow a component inheriting from this component to use exposed_intents { remove :index } rather than exposed_intents { remove :index_users }.

Similarly, this component removes the exposed intent :destroy which it whould otherwise inherit from Comopony::Components::Show. Every call to exposed_intents overrides properties set in previous calls by the same component, primarly useful for reusing components by inheritance.

In order to replace an existing intent defined by a previous call to exposed_intents (e.g. in a parent class), simply call add again and make sure the intent's name matches that of the one to override. add also accepts the before: keyword, allowing you to reorder intents or insert a new one into a specific place in the intent list.

Note that the add method has full intent argument support and thus also accepts parameters related to the button (e.g. style), path generation, feasibility etc.

Rendering exposed intents

You can render exposed intents in the parent component or in the application layout. To do so, call component.exposed_intents, loop across them and call .render(controller) on each (perhaps inside a div tag or whatever suits your needs).

Example in layouts/application.html.erb

<% Compony.root_comp&.exposed_intents&.each do |intent| %>
  <div class="root-intent"><%= intent.render(controller) %>
<% end %>

Guide index