Skip to content

Commit

Permalink
Improve README.md documentation and include example to add new custom…
Browse files Browse the repository at this point in the history
… component (#161)

* Improve README.md documentation and include example to add new custom
component

* Update README.md

Co-authored-by: Hans Lemuet <Spone@users.noreply.github.com>

* Improve README from feedback

---------

Co-authored-by: Hans Lemuet <Spone@users.noreply.github.com>
  • Loading branch information
joelzwarrington and Spone authored Jun 17, 2024
1 parent c617ee4 commit 16818e1
Showing 1 changed file with 91 additions and 131 deletions.
222 changes: 91 additions & 131 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,69 @@
# ViewComponent::Form

**ViewComponent::Form** provides a `FormBuilder` with the same interface as [`ActionView::Helpers::FormBuilder`](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html), but using [ViewComponent](https://github.com/github/view_component)s for rendering the fields. It's a starting point for writing your own custom ViewComponents.

:warning: **This is an early release: the API is subject to change until we reach `v1.0.0`.**
**`ViewComponent::Form`** is a customizable form builder using the same interface as [`ActionView::Helpers::FormBuilder`](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html) but with extensible [ViewComponent](https://github.com/github/view_component) components.

Development of this gem is sponsored by:

<a href="https://etamin.studio/?ref=view_component-form"><img src="https://etamin.studio/images/logo.svg" alt="Sponsored by Etamin Studio" width="184" height="22"></a>      <a href="https://pantographe.studio/?ref=view_component-form"><img src="https://static.s3.office.pantographe.cloud/logofull.svg" alt="Sponsored by Pantographe" width="156" height="25"></a>

## Compatibility

> [!WARNING]
> **This is an early release, and the API is subject to change until `v1.0.0`.**
This gem is tested on:

- Rails 6.0+ (with or without ActionText)
- Ruby 2.7+

## Installation

Add this line to your application's Gemfile:
```shell
bundle add view_component-form
```

### Configuration

```ruby
gem 'view_component-form'
```
# config/initializers/vcf.rb

And then execute:
ViewComponent::Form.configure do |config|
config.parent_component = 'ApplicationFormComponent'
end
```

$ bundle install
| Attribute | Purpose | Default |
| --------------------------- | ----------------------------------------------------- | ----------------------- |
| `parent_component` (string) | Parent class for all `ViewComponent::Form` components | `"ViewComponent::Base"` |

## Usage

Add a `builder` param to your `form_for`, `form_with`, `fields_for` or `fields`:
Add your own form builder.

```shell
bin/rails generate vcf:builder FormBuilder
create app/helpers/form_builder.rb
```

To use the form builder:

- add a `builder` param to your `form_for`, `form_with`, `fields_for` or `fields`:

```diff
- <%= form_for @user do |f| %>
+ <%= form_for @user, builder: ViewComponent::Form::Builder do |f| %>
+ <%= form_for @user, builder: FormBuilder do |f| %>
```

You can also define a default FormBuilder at the controller level using [default_form_builder](https://api.rubyonrails.org/classes/ActionController/FormBuilder.html#method-i-default_form_builder).
- or; set it as a default in your controller using [default_form_builder](https://api.rubyonrails.org/classes/ActionController/FormBuilder.html#method-i-default_form_builder).

```ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
default_form_builder FormBuilder
end
```

Then call your helpers as usual:
Then use ActionView form builder helpers as you would normally:

```erb
<%# app/views/users/_form.html.erb %>
Expand All @@ -62,147 +88,67 @@ Then call your helpers as usual:
<% end %>
```

It should work out of the box, but does nothing particularly interesting for now.

```html
<form class="edit_user" id="edit_user_1" action="/users/1" accept-charset="UTF-8" method="post">
<input type="hidden" name="_method" value="patch" />
<input type="hidden" name="authenticity_token" value="[...]" />

<label for="user_first_name">First name</label>
<input type="text" value="John" name="user[first_name]" id="user_first_name" />

<label for="user_last_name">Last name</label>
<input type="text" value="Doe" name="user[last_name]" id="user_last_name" />

<label for="user_email">E-mail</label>
<input type="email" value="john.doe@example.com" name="user[email]" id="user_email" />

<label for="user_password">Password</label>
<input type="password" name="user[password]" id="user_password" aria-describedby="user_password_description" />
<div id="user_password_description">
<div>The password should be at least 8 characters long</div>
</div>
</form>
```

The `ViewComponent::Form::*` components are included in the gem.

### Customizing the `FormBuilder` and the components

First, generate your own `FormBuilder`:

```console
bin/rails generate vcf:builder CustomFormBuilder

create app/helpers/custom_form_builder.rb
```
### Customizing built-in components

This allows you to pick the namespace your components will be loaded from.
The `ViewComponent::Form::Builder` will use the provided `namespace` to find any components you've customized.

```rb
# app/helpers/custom_form_builder.rb
class CustomFormBuilder < ViewComponent::Form::Builder
# Set the namespace you want to use for your own components
namespace "Custom::Form"
```ruby
# app/helpers/form_builder.rb
class FormBuilder < ViewComponent::Form::Builder
namespace Form
end
```

Use the generator options to change the default namespace or the path where the file will be created:

```console
bin/rails generate vcf:builder AnotherCustomFormBuilder --namespace AnotherCustom::Form --path app/forms

create app/forms/another_custom_form_builder.rb
```
Let's customize the `text_field` helper by generating a new [ViewComponent](https://github.com/github/view_component) in the namespace defined within the builder.

```rb
# app/forms/another_custom_form_builder.rb
class AnotherCustomFormBuilder < ViewComponent::Form::Builder
# Set the namespace you want to use for your own components
namespace "AnotherCustom::Form"
end
```shell
bin/rails generate component Form::TextField --parent ViewComponent::Form::TextFieldComponent --inline
```

Another approach is to include only some modules instead of inheriting from the whole class:

```rb
# app/forms/modular_custom_form_builder.rb
class ModularCustomFormBuilder < ActionView::Helpers::FormBuilder
# Provides `render_component` method and namespace management
include ViewComponent::Form::Renderer

# Exposes a `validation_context` to your components
include ViewComponent::Form::ValidationContext

# All standard Rails form helpers
include ViewComponent::Form::Helpers::Rails

# Backports of Rails 7 form helpers (can be removed if you're running Rails >= 7)
# include ViewComponent::Form::Helpers::Rails7Backports

# Additional form helpers provided by ViewComponent::Form
# include ViewComponent::Form::Helpers::Custom

# Set the namespace you want to use for your own components
namespace "AnotherCustom::Form"
```ruby
# app/components/form/text_field_component.rb
class Form::TextFieldComponent < ViewComponent::Form::TextFieldComponent
def html_class
class_names("custom-text-field", "has-error": method_errors?)
end
end
```

Now let's generate your own components to customize their rendering. We can use the standard view_component generator:

```console
bin/rails generate component Custom::Form::TextField --inline --parent ViewComponent::Form::TextFieldComponent

invoke test_unit
create test/components/custom/form/text_field_component_test.rb
create app/components/custom/form/text_field_component.rb
```

:warning: The `--parent` option is available since ViewComponent [`v2.41.0`](https://viewcomponent.org/CHANGELOG.html#2410). If you're using a previous version, you can always edit the generated `Custom::Form::CustomTextFieldComponent` class to make it inherit from `ViewComponent::Form::TextFieldComponent`.
In this case we're leveraging the [`#class_names`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-class_names) helper to:

Change your forms to use your new builder:
- always add the `custom-text-field` class;
- add the `has-error` class if there is an error on the attribute (using `ViewComponent::Form::FieldComponent#method_errors?`).

```diff
- <%= form_for @user, builder: ViewComponent::Form::Builder do |f| %>
+ <%= form_for @user, builder: CustomFormBuilder do |f| %>
```
### Adding your own custom helpers and components

You can then customize the behavior of your `Custom::Form::CustomTextFieldComponent`:
Add the helper method to your `ViewComponent::Form::Builder`

```rb
# app/components/custom/form/text_field_component.rb
# app/helpers/form_builder.rb
class FormBuilder < ViewComponent::Form::Builder
def year_field(method, options = {})
render_component(:year_field, @object_name, method, objectify_options(options))
end

class Admin::Form::TextFieldComponent < ViewComponent::Form::TextFieldComponent
def html_class
class_names("custom-text-field", "has-error": method_errors?)
def money_field(method, currencies = [], options = {})
render_component(:money_field, @object_name, method, currencies, objectify_options(options))
end
end
```

In this case we leverage the [`#class_names`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-class_names) helper to:
- always add the `custom-text-field` class;
- add the `has-error` class if there is an error on the attribute (using `ViewComponent::Form::FieldComponent#method_errors?`).
Add your component which can optionally inherit from:

The rendered form field will now look like this:
- `ViewComponent::Form::FieldComponent` (suggested when adding a field because of helpers)
- `ViewComponent::Form::BaseComponent`
- or any of the `ViewComponent::Form::*Component` such as `ViewComponent::Form::TextFieldComponent`

```html
<input class="custom-text-field" type="text" value="John" name="user[first_name]" id="user_first_name">
```rb
# app/components/form/year_field_component.rb
class Form::YearFieldComponent < ViewComponent::Form::FieldComponent # or ViewComponent::Form::BaseComponent
end
```

You can use the same approach to inject options, wrap the input in a `<div>`, etc.

We'll add more use cases to the documentation soon.

### Configuration

| Attribute | Purpose | Default |
| ------------------ | ----------------------------------------------- | --------------------- |
| `parent_component` (string) | Parent class for all `ViewComponent::Form` components | `"ViewComponent::Base"` |

### Building your own components

When building your own ViewComponents for using in forms, it's recommended to inherit from `ViewComponent::Form::FieldComponent`, so you get access to the following helpers:
When inheriting from `ViewComponent::Form::FieldComponent`, you get access to the following helpers:

#### `#label_text`

Expand Down Expand Up @@ -245,16 +191,27 @@ en:
Renders:
```html
<form class="edit_user" id="edit_user_1" action="/users/1" accept-charset="UTF-8" method="post">
<form
class="edit_user"
id="edit_user_1"
action="/users/1"
accept-charset="UTF-8"
method="post"
>
<!-- ... -->
<label>
Your first name<br />
<input type="text" value="John" name="user[first_name]" id="user_first_name" />
<input
type="text"
value="John"
name="user[first_name]"
id="user_first_name"
/>
</label>
</form>
```

#### Validations
### Validations

Let's consider the following model for the examples below.

Expand Down Expand Up @@ -348,11 +305,14 @@ end
### Setting up your own base component class

1. Setup some base component from which the form components will inherit from

```rb
class ApplicationFormComponent < ViewComponent::Base
end
```

2. Configure the parent component class

```rb
# config/initializers/vcf.rb

Expand Down

0 comments on commit 16818e1

Please sign in to comment.