[![license](https://img.shields.io/badge/License-MIT-purple.svg)](LICENSE) [![Gem Version](https://img.shields.io/gem/v/amber_component.svg?style=flat)](https://rubygems.org/gems/amber_component) [![Maintainability](https://api.codeclimate.com/v1/badges/ad84af499e9791933a87/maintainability)](https://codeclimate.com/github/amber-ruby/amber_component/maintainability) [![CI badge](https://github.com/amber-ruby/amber_component/actions/workflows/ci_ruby.yml/badge.svg)](https://github.com/amber-ruby/amber_component/actions/workflows/ci_ruby.yml) [![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/Verseth/6a095c79278b074d79feaa4f8ceeb2a8/raw/amber_component__heads_main.json)](https://github.com/amber-ruby/amber_component/actions/workflows/ci_ruby.yml) # AmberComponent AmberComponent is a simple component library which seamlessly hooks into your Rails project and allows you to create simple backend components which consist of a Ruby controller, view, stylesheet and even a JavaScript controller (using [Stimulus](https://stimulus.hotwired.dev/)). Created by [Garbus Beach](https://github.com/garbusbeach) and [Mateusz Drewniak](https://github.com/Verseth). ## Getting started You can read a lot more about AmberComponent in its [official docs](https://ambercomponent.com). ## Installation Install the gem and add to the application's Gemfile by executing: $ bundle add amber_component If bundler is not being used to manage dependencies, install the gem by executing: $ gem install amber_component If you're using a Rails application there's an installation generator that you should run: ```sh $ bin/rails generate amber_component:install ``` Amber component supports [Stimulus](https://stimulus.hotwired.dev/) to make your components reactive using JavaScript. If you want to use stimulus you should install this gem with the `--stimulus` flag ```sh $ bin/rails generate amber_component:install --stimulus ``` ## Usage ### Components Components are located under `app/components`. And their tests under `test/components`. Every component consists of: - a Ruby file which defines its properties, encapsulates logic and may implement helper methods (like a controller) - a view/template file (html.erb, haml, slim etc.) - a style file (css, scss, sass etc.) - [optional] a JavaScript file with a Stimulus controller (if you installed the gem with `--stimulus`) `amber_component` automatically detects what kind of view and stylesheet formats your app is configured to use. So if you've got `haml-rails`, components will be generated with `haml`. When your app uses `slim-rails`, components will be generated with `slim`. When your `Gemfile` contains `sassc-rails`, components will use `scss` etc. All of these formats can be overridden in an initializer or by adding arguments to the component generator. ``` app/components/ ├─ [name]_component.rb └─ [name]_component/ ├─ style.css # may be .sass or .scss ├─ view.html.erb └─ controller.js # if stimulus is configured test/components/ └─ [name]_component_test.rb ``` An individual component which implements a button may look like this. ```ruby # app/components/button_component.rb class ButtonComponent < AmberComponent::Base prop :label, required: true end ``` ```html
<%= label %>
``` ```scss // app/components/button_component/style.scss .button_component { background-color: indigo; border-radius: 1rem; transition-duration: 500ms; &:hover { background-color: blue; } } ``` If you used the `--stimulus` option when installing the gem, a JS controller will be generated as well. ```js // app/components/button_component/controller.js import { Controller } from "@hotwired/stimulus" // Read more about Stimulus here https://stimulus.hotwired.dev/ export default class extends Controller { connect() { console.log("Stimulus controller 'button-component' is connected!") } greet() { alert("Hi there!") } } ``` You can render this component in other components or in a Rails view. ```html

We're inside FooController

<%= button_component label: 'Click me!' %> <%= ButtonComponent.call label: 'Click me!' %> ``` Or even directly in Ruby ```ruby # Calling a method on the component class. Outputs an HTML string. ButtonComponent.call label: 'Click me!' #=> '
Click me!
' ``` ### Components with namespaces Components may be defined inside multiple modules/namespaces. ```ruby # app/components/sign_up/button_component.rb class SignUp::ButtonComponent < AmberComponent::Base prop :label, required: true end ``` ```html
<%= label %>
``` ```scss // app/components/sign_up/button_component/style.scss .sign_up_button_component { background-color: indigo; border-radius: 1rem; transition-duration: 500ms; &:hover { background-color: blue; } } ``` You can render such a component by calling the `::call` method on its class, or by using the helper method defined on its parent module. ```ruby SignUp::ButtonComponent.call label: 'Sign up!' SignUp.button_component label: 'Sign up!' ``` ### Generating Components You can generate new components by running ```sh $ bin/rails generate component [name] ``` Name of the component may be PascalCased like `FooBar` or snake_cased `foo_bar` This will generate a new component in `app/components/[name]_component.rb` along with a view, stylesheet, test file and a stimulus controller (if configured). ``` app/components/ ├─ [name]_component.rb └─ [name]_component/ ├─ style.css # may be `.scss` or `.sass` ├─ view.html.erb # may be `.haml` or `.slim` └─ controller.js # if stimulus is configured test/components/ └─ [name]_component_test.rb ``` View and stylesheet formats can be overridden by providing options. ``` -v, [--view=VIEW] # Indicate what type of view should be generated eg. [:erb, :haml, :slim] --styles, -c, [--css=CSS] # Indicate what type of styles should be generated eg. [:css, :scss, :sass] ``` ### Component properties There is a neat prop DSL. ```ruby # app/components/comment_component.rb class CommentComponent < ApplicationComponent # will raise an error when not present prop :body, required: true # will raise an error when an object of a different # class is received (uses `is_a?`) prop :author, type: User, allow_nil: true # the default value prop :date, default: -> { DateTime.now } end ``` Props can be passed as keyword arguments to the `::call` method of the component class or the helper method. ```ruby CommentComponent.call body: 'Foo bar', author: User.first # only in views and other components comment_component body: 'Foo bar', author: User.first ``` ### Helper methods Defining helper methods which are available in the template is extremely easy. Just define a method on the component class. ```ruby # app/components/comment_component.rb class CommentComponent < ApplicationComponent # you can also include helper modules include SomeHelper prop :body, required: true prop :author, type: Author, allow_nil: true prop :date, default: -> { DateTime.now } private def humanized_date date.strftime '%Y-%m-%d %H:%M' end def author_name author&.name || 'Unknown' end def author_avatar author&.avatar_url || User.placeholder_avatar_url end end ``` ```html
<%= author_name %> avatar
<%= author_name %>
<%= humanized_date %>
<%= body %>
``` ### Overriding prop getters and setters Getters and setters for properties are defined in a module which means that you can override them and call them with `super`. ```ruby # app/components/priority_icon_component.rb class PriorityIconComponent < ApplicationComponent PriorityStruct = Struct.new :icon, :color PRIORITY_MAP = { low: PriorityStruct.new('fa-solid fa-chevrons-down', 'green'), medium: PriorityStruct.new('fa-solid fa-chevron-up', 'yellow'), high: PriorityStruct.new('fa-solid fa-chevrons-up', 'red') } prop :severity, default: -> { :low } def severity=(val) # super will call the original # implementation of the setter super(PRIORITY_MAP[val]) end end ``` ```html ``` ### Nested components It's possible to nest components or provide custom HTML to a component. This works similarly to React's `props.children`. To render the passed nested content call `yield.html_safe` somewhere inside the template/view. ```ruby # app/components/modal_component.rb class ModalComponent < ApplicationComponent prop :id, required: true prop :title, required: true end ``` ```html