Form Component API
The matestack_form core component is a Vue.js driven component. It enables you to implement dynamic forms without writing JavaScript. It relies on child components to collect and submit user input: form_input, form_textarea, form_radio, form_select, and form_checkbox . They are described on their own documentation page

Parameters

The core form component accepts the following parameters. Pass them in as a hash like so:
1
class ExamplePage < Matestack::Ui::Page
2
3
def response
4
matestack_form some_form_config do
5
#...
6
end
7
end
8
9
def some_form_config
10
{
11
for: :my_object,
12
method: :post,
13
path: success_form_test_path,
14
#...
15
}
16
end
17
18
end
Copied!

For - required

The form component wraps the input in an object. The name of this object can be set in multiple ways:

set as a symbol

1
for: :my_object
Copied!
1
matestack_form some_form_config do
2
form_input key: :some_input_key, type: :text
3
end
Copied!
When submitting this form, the form component will perform a request with a payload like this:
1
my_object: {
2
some_input_key: "foo"
3
}
Copied!

set by Active Record class name

1
@my_model = MyActiveRecordModel.new
2
#...
3
for: @my_model
Copied!
1
matestack_form some_form_config do
2
form_input key: :some_model_attribute, type: :text
3
end
Copied!
When submitting this form, the form component will perform a request with a payload like this:
1
my_active_record_model: {
2
some_model_attribute: "foo"
3
}
Copied!
Please be aware that if you use an Active Record model, the keys of the input components should match a model attribute/method. The form automatically tries to prefill the form inputs through calling the keys as methods on the model.

Method - required

This specifies which kind of HTTP method should get triggered. It accepts a symbol like so:
1
method: :post
Copied!

Path - required

This parameter accepts a typical Rails path
1
path: action_test_path
Copied!
or
1
path: action_test_path(id: 42)
Copied!

Emit

This event gets emitted right after form submit. In contrast to the success or failure events, it will be emitted regardless of the server response.
1
emit: "form_submitted"
Copied!

Delay

You can use this attribute if you want to delay the actual form submit request. It will not delay the event specified with the emit attribute.
1
delay: 1000 # means 1000 ms
Copied!

Multipart

If you want to perform file uploads within this form, you have to set multipart to true. It will send the form data with "Content-Type": "multipart/form-data"
1
multipart: true # default is false which results in form submission via "Content-Type": "application/json"
Copied!

Success

The success part of the matestack_form component gets triggered once the controller action we wanted to call returns a success code, usually the 2xx HTTP status code.

Emit event

To trigger further behavior, we can configure the success part of a form to emit a message like so:
1
success: {
2
emit: 'my_form_success'
3
}
Copied!

Perform transition

We can also perform a transition that only gets triggered on success and also accepts further params:
1
success: {
2
emit: 'my_form_success',
3
transition: {
4
path: form_test_page2_path(id: 42)
5
}
6
}
Copied!
When the server redirects to a url, for example after creating a new record, the transition needs to be configured to follow this redirect of the server response.
1
success: {
2
emit: 'my_form_success',
3
transition: {
4
follow_response: true
5
}
6
}
Copied!
A controller action that would create a record and then respond with the url the page should transition to, could look like this:
1
class TestModelsController < ApplicationController
2
3
def create
4
@test_model = TestModel.create(test_model_params)
5
6
render json: {
7
transition_to: test_model_path(@test_model)
8
}, status: :ok
9
end
10
end
Copied!
Same applies for the failure configuration.

Perform redirect

We can also perform a redirect (full page load) that only gets triggered on success and also accepts further params:
Please be aware, that emiting a event doen't have an effect when performing a redirect instead of a transition, as the whole page (including the surrounding app) gets reloaded!
1
success: {
2
emit: 'my_form_success', # doesn't have an effect when using redirect
3
redirect: {
4
path: action_test_page2_path(id: 42)
5
}
6
}
Copied!
When the server redirects to a url, for example after creating a new record, the redirect needs to be configured to follow this redirect of the server response.
1
success: {
2
emit: 'my_form_success', # doesn't have an effect when using redirect
3
redirect: {
4
follow_response: true
5
}
6
}
Copied!
A controller action that would create a record and then respond with the url the page should redirect to, could look like this:
1
class TestModelsController < ApplicationController
2
3
def create
4
@test_model = TestModel.create(test_model_params)
5
6
render json: {
7
redirect_to: test_model_path(@test_model)
8
}, status: :ok
9
end
10
end
Copied!
Same applies for the failure configuration.

Reset form

If submitted successfully, the form component resets its state by default when using the "post" method. When using the "put" method, the state is not resetted by default. You may control this behavior explictly by using the reset option:
1
method: :post
2
success: {
3
emit: 'my_form_success',
4
reset: false #default true when using the :post method when submitted successfully
5
}
Copied!
1
method: :put
2
success: {
3
emit: 'my_form_success',
4
reset: true #default false when using the :put method when submitted successfully
5
}
Copied!

Failure

As counterpart to the success part of the form component, there is also the possibility to define the failure behavior. This is what gets triggered after the response to our form submit returns a failure code, usually in the range of 400 or 500 HTTP status codes.

Emit event

To trigger further behavior, we can configure the failure part of an action to emit a message like so:
1
failure: {
2
emit: 'my_form_failure'
3
}
Copied!

Perform transition

We can also perform a transition that only gets triggered on failure:
1
failure: {
2
emit: 'my_form_failure',
3
transition: {
4
path: root_path
5
}
6
}
Copied!

Reset form

The form component does not reset its state by default when not submitted successfully. You may control this behavior explictly by using the reset option:
1
method: :post
2
failure: {
3
emit: 'my_form_failure',
4
reset: true #default false when using the :post method and not successful
5
}
Copied!
1
method: :put
2
failure: {
3
emit: 'my_form_failure',
4
reset: true #default false when using the :put method and not successful
5
}
Copied!

ID

This parameter accepts a string of ids that the form component should have:
1
id: 'my-form-id'
Copied!
which renders as an HTML id attribute, like so:
1
<form id="my-form-id" class="matestack-form">...</form>
Copied!

Class

This parameter accepts a string of classes that the form component should have:
1
class: 'my-form-class'
Copied!
which renders as an HTML class attribute, like so:
1
<form class="matestack-form my-form-class">...</form>
Copied!

Error rendering

If the server is responding with a well formatted error response and status after submitting the form, matestack will automatically render server error messages right next to the corresponding input (matching error and input key). Additionally the input itself will get a 'error' css class; the parent form will get a 'has-errors' css class.
The described approach is suitable for all form_* input components.
By default it would look like this:
1
form_input key: :title, type: :text
Copied!
1
<form class="matestack-form has-errors">
2
<input type="text" class="error">
3
<span class="errors">
4
<!-- for each error string within the error array for the key 'title' -->
5
<span class="error">
6
can't be blank
7
</span>
8
</span>
9
</form>
Copied!
when a 4xx JSON server response like that was given (ActiveRecord errors format):
1
{
2
"errors":
3
{
4
"title": ["can't be blank"]
5
}
6
}
Copied!
You can modify the error rendering like this:
On input level
1
form_input key: :foo, type: :text, errors: {
2
wrapper: { tag: :div, class: 'my-errors'}, tag: :div, class: 'my-error'
3
}
4
form_input key: :bar, type: :text, errors: false
Copied!
1
<form class="matestack-form has-errors">
2
<input type="text" class="error">
3
<div class="my-errors">
4
<!-- for each error string within the error array for the key 'title' -->
5
<div class="my-error">
6
can't be blank
7
</div>
8
</div>
9
<input type="text" class="error">
10
<!-- without any error rendering because its set to false -->
11
</form>
Copied!
On form level
Configuring errors on a per form basis. Per form field configs take precedence over the form config.
1
def response
2
matestack_form form_config do
3
form_input key: :foo, type: :text
4
form_input key: :bar, type: :text, errors: false
5
end
6
end
7
8
def form_config
9
{
10
for: :my_model,
11
#[...]
12
errors: {
13
wrapper: { tag: :div, class: 'my-errors' },
14
tag: :div,
15
class: 'my-error',
16
input: { class: 'my-field-error' }
17
}
18
}
19
end
Copied!
Outputs errors as:
1
<input type="text" class="my-field-error" />
2
<div class="my-errors">
3
<div class="my-error">
4
can't be blank
5
</div>
6
</div>
7
<input type="text" class="my-field-error" />
8
<!-- without any errors, because its config takes precedence over the form config -->
Copied!

Error message rendering

Given a server error response like that:
1
{
2
"errors": {
3
"title": ["can't be blank"]
4
},
5
"message": "Something went wrong"
6
}
Copied!
now including a message which is not mapped to an input field, we can display this error message like:
1
def response
2
matestack_form form_config do
3
form_input key: :foo, type: :text
4
# ...
5
end
6
# somewhere else or within the form:
7
toggle show_on: :form_failed, hide_on: :form_succeeded do
8
plain "{{event.data.message}}"
9
end
10
end
11
12
def my_form_config
13
{
14
#...
15
success: {
16
emit: "form_succeeded"
17
},
18
failure: {
19
emit: "form_failed"
20
}
21
}
22
end
Copied!
The matestack_form component emits the event together with all errors and the message coming from the server's response. The toggle component can then access all this data via event.data.xyz

Loading state

The form will get a 'loading' css class while submitting the form and waiting for a server response:
1
<form class="matestack-form loading">
2
3
</form>
Copied!
If you simply want to disable your submit button, you can use a simple Vue.js binding:
1
button text: "Submit me!", attributes: { "v-bind:disabled": "loading" }
Copied!
If you want to adjust the submit element more flexible while the form is being submitted, you could use the event mechanism of the form in combination with the toggle component:
1
toggle hide_on: "form_loading", show_on: "form_succeeded, form_failed", init_show: true do
2
button text: "Submit me!"
3
end
4
toggle show_on: "form_loading", hide_on: "form_succeeded, form_failed" do
5
button text: "submitting...", disabled: true
6
end
Copied!
in combination with a form config like this:
1
def my_form_config
2
{
3
#...
4
emit: "form_loading",
5
success: {
6
emit: "form_succeeded"
7
},
8
failure: {
9
emit: "form_failed"
10
}
11
}
12
end
Copied!

Form and other Vue.js components

The child components form_* have to be placed within the scope of the parent form component, without any other Vue.js component like toggle, async creating a new scope between the child component and the parent form component**
1
# that's working:
2
matestack_form some_form_config do
3
form_input key: :some_input_key, type: :text
4
toggle show_on: "some-event" do
5
plain "hello!"
6
end
7
end
8
9
# that's not working:
10
form some_form_config do
11
toggle show_on: "some-event" do
12
form_input key: :some_input_key, type: :text
13
end
14
end
Copied!
We're working on a better decoupling of the form child components. In the mean time you can enable dynamic child component rendering utilizing Vue.js directly. We will shortly publish a guide towards that topic.

Examples

These examples show generic use cases and can be used as a guideline of what is possible with the form core component.
Beforehand, we define some example routes for the form input in our config/routes.rb:
1
post '/success_form_test', to: 'form_test#success_submit', as: 'success_form_test'
2
post '/failure_form_test', to: 'form_test#failure_submit', as: 'failure_form_test'
3
post '/model_form_test', to: 'model_form_test#model_submit', as: 'model_form_test'
Copied!
We also configure our example controllers to accept form input and react in a predictable and verbose way:
1
class FormTestController < ApplicationController
2
3
def success_submit
4
render json: { message: 'server says: form submitted successfully' }, status: 200
5
end
6
7
def failure_submit
8
render json: {
9
message: 'server says: form had errors',
10
errors: { foo: ['seems to be invalid'] }
11
}, status: 400
12
end
13
14
end
Copied!
1
class ModelFormTestController < ApplicationController
2
3
def model_submit
4
@test_model = TestModel.create(model_params)
5
if @test_model.errors.any?
6
render json: {
7
message: 'server says: something went wrong!',
8
errors: @test_model.errors
9
}, status: :unprocessable_entity
10
else
11
render json: {
12
message: 'server says: form submitted successfully!'
13
}, status: :ok
14
end
15
end
16
17
protected
18
19
def model_params
20
params.require(:test_model).permit(:title, :description, :status, some_data: [], more_data: [])
21
end
22
23
end
Copied!

Async submit request with clientside payload

On our example page, we define a form that accepts text input and has a submit button.
1
class ExamplePage < Matestack::Ui::Page
2
3
def response
4
matestack_form form_config do
5
form_input id: 'my-test-input', key: :foo, type: :text
6
button 'Submit me!'
7
end
8
end
9
10
def form_config
11
{
12
for: :my_object,
13
method: :post,
14
path: success_form_test_path,
15
}
16
end
17
18
end
Copied!
When we visit localhost:3000/example, fill in the input field with bar and click the submit button, our FormTestController receives the input.
Furthermore, our bar input disappears from the input field - Easy!

Async submit request with failure event

This time, we break the form input on purpose to test our failure message! Again, we define our example page. Notice that we explicitly aim for our failure_form_test_path.
1
class ExamplePage < Matestack::Ui::Page
2
3
def response
4
matestack_form form_config do
5
form_input id: 'my-test-input', key: :foo, type: :text
6
button 'Submit me!'
7
end
8
toggle show_on: 'my_form_failure' do
9
plain "{{event.data.message}}"
10
plain "{{event.data.errors}}"
11
end
12
end
13
14
def form_config
15
{
16
for: :my_object,
17
method: :post,
18
path: failure_form_test_path,
19
failure: {
20
emit: 'my_form_failure'
21
}
22
}
23
end
24
25
end
Copied!
Now, when we visit our example page on localhost:3000/example and fill in the input field with e.g. bar and hit the submit button, we get displayed both server says: form had errors and 'foo': [ 'seems to be invalid' ]. Just what we expected to receive!

Async submit request with success transition

In this example, things get a bit more complex. We now want to transition to another page of our application after successfully submitting a form!
In order to additionally show a success/failure message, we define our matestack app layout with messages, using the async core component:
1
class ExampleApp::App < Matestack::Ui::App
2
3
def response
4
h1 'My Example App Layout'
5
main do
6
yield
7
end
8
toggle show_on: 'my_form_success', hide_after: 300 do
9
plain "{{event.data.message}}"
10
end
11
toggle show_on: 'my_form_failure', hide_after: 300 do
12
plain "{{event.data.message}}"
13
plain "{{event.data.errors}}"
14
end
15
end
16
17
end
Copied!
On our first example page, we define our form to transfer us to the second page (form_test_page_2_path) on successful input:
1
class ExampleApp::Pages::ExamplePage < Matestack::Ui::Page
2
3
def response
4
h2 'This is Page 1'
5
matestack_form form_config do
6
form_input id: 'my-test-input-on-page-1', key: :foo, type: :text
7
button 'Submit me!', type: :submit
8
end
9
end
10
11
def form_config
12
{
13
for: :my_object,
14
method: :post,
15
path: success_form_test_path,
16
success: {
17
emit: 'my_form_success',
18
transition: {
19
path: form_test_page_2_path
20
}
21
}
22
}
23
end
24
25
end
Copied!
On the second example page, we aim for our failure path (failure_form_test_path) on purpose and define our form to transfer us to the first page (form_test_page_1_path) on failed input:
1
class ExampleApp::Pages::SecondExamplePage < Matestack::Ui::Page
2
3
def response
4
h2 'This is Page 2'
5
matestack_form form_config do
6
form_input id: 'my-test-input-on-page-2', key: :foo, type: :text
7
button 'Submit me!', type: :submit
8
end
9
end
10
11
def form_config
12
{
13
for: :my_object,
14
method: :post,
15
path: failure_form_test_path,
16
failure: {
17
emit: 'my_form_failure',
18
transition: {
19
path: form_test_page_1_path
20
}
21
}
22
}
23
end
24
25
end
Copied!
Of course, to reach our two newly defined pages, we need to make them accessible through a controller:
1
class ExampleAppPagesController < ExampleController
2
3
include Matestack::Ui::Core::Helper
4
matestack_app ExampleApp::App
5
6
def page1
7
render ExampleApp::Pages::ExamplePage
8
end
9
10
def page2
11
render ExampleApp::Pages::SecondExamplePage
12
end
13
14
end
Copied!
We also need to extend our routes in config/routes.rb to handle the new routes:
1
scope :form_test do
2
get 'page1', to: 'example_app_pages#page1', as: 'form_test_page_1'
3
get 'page2', to: 'example_app_pages#page2', as: 'form_test_page_2'
4
end
Copied!
Now, if we visit localhost:form_test/page1, we can fill in the input field with e.g. bar and click the submit button.
We then get displayed our nice success message (server says: form submitted successfully) and get transferred to our second page.
If we fill in the the input field there and hit the submit button, we not only see the failure messages (server says: form had errors and 'foo': [ 'seems to be invalid' ]), we also get transferred back to the first page, just the way we specified this behavior in the page definition above!

Async submit request with success transition - dynamically determined by server

In the example shown above, the success transition is statically defined. Sometimes the transition needs to be dynamically controlled within the server action. Imagine creating a new Active Record instance with a form. If you want to show the fresh instance on another page and therefore want to define a transition after successful form submission, you would need to know the ID of the fresh instance! That is not possible, as the ID is auto-generated and depends on the current environment/state. Therefore you can tell the form component to follow a transition, which the server action defines after creating the new instance (and now knowing the ID):
On the page:
1
#...
2
3
def form_config
4
{
5
for: :my_object,
6
method: :post,
7
path: success_form_test_path,
8
success: {
9
emit: 'my_form_success',
10
transition: {
11
follow_response: true # follow the serverside transition
12
}
13
}
14
}
15
end
Copied!
On the controller action:
1
#...
2
def model_submit
3
@test_model = TestModel.create(model_params)
4
if @test_model.errors.any?
5
render json: {
6
message: 'server says: something went wrong!',
7
errors: @test_model.errors
8
}, status: :unprocessable_entity
9
else
10
render json: {
11
message: 'server says: form submitted successfully!',
12
transition_to: some_other_path(id: @test_model.id) #tell the form component where to transition to with the id, which was not available before
13
}, status: :ok
14
end
15
end
Copied!

Multiple input fields of different types

Of course, our input core component accepts not only 'text', but very different input types: In this example, we will introduce 'password', 'number', 'email', 'range' types!
On our example page, we define the input fields, together with a type: X configuration:
1
class ExamplePage < Matestack::Ui::Page
2
3
def response
4
matestack_form form_config do
5
form_input id: 'text-input', key: :text_input, type: :text
6
form_input id: 'email-input', key: :email_input, type: :email
7
form_input id: 'password-input', key: :password_input, type: :password
8
form_input id: 'number-input', key: :number_input, type: :number
9
form_input id: 'range-input', key: :range_input, type: :range
10
11
button 'Submit me!', type: :submit
12
end
13
end
14
15
def form_config
16
{
17
for: :my_object,
18
method: :post,
19
path: success_form_test_path
20
}
21
end
22
23
end
Copied!
Now, we can visit localhost:3000/example and fill in the input fields with various data that then gets sent to the corresponding path, in our case success_form_test_path.

Initialization with a value

Our form_input field doesn't need to be empty when we load the page. We can init it with all kinds of values:
1
class ExamplePage < Matestack::Ui::Page
2
3
def response
4
matestack_form form_config do
5
form_input id: 'text-input', key: :text_input, type: :text, init: 'some value'
6
button 'Submit me!', type: :submit
7
end
8
end
9
10
def form_config
11
{
12
for: :my_object,
13
method: :post,
14
path: success_form_test_path
15
}
16
end
17
18
end
Copied!
Now, when we visit localhost:3000/example, we see our input field already welcomes us with the value some value!

Pre-filling the input field with a placeholder

Instead of a predefined value, we can also just show a placeholder in our form_input component:
1
class ExamplePage < Matestack::Ui::Page
2
3
def response
4
matestack_form form_config do
5
form_input id: 'text-input', key: :text_input, type: :text, placeholder: 'some placeholder'
6
form_submit do
7
button 'Submit me!', type: :submit
8
end
9
end
10
end
11
12
def form_config
13
{
14
for: :my_object,
15
method: :post,
16
path: success_form_test_path
17
}
18
end
19
20
end
Copied!
Now, when we visit localhost:3000/example, the input field is technically empty, but we see the text some placeholder. In contrary to the init value in example 5, the placeholder can't get submitted)!

Defining a label

Another useful feature is that we can also give a label to our form_input!
1
class ExamplePage < Matestack::Ui::Page
2
3
def response
4
matestack_form form_config do
5
form_input id: 'text-input', key: :text_input, type: :text, label: 'some label'
6
button 'Submit me!', type: :submit
7
end
8
end
9
10
def form_config
11
{
12
for: :my_object,
13
method: :post,
14
path: success_form_test_path
15
}
16
end
17
18
end
Copied!
Now, when we visit localhost:3000/example, the input field carries a some label-label.

Asynchronously display error messages

Here, we aim for the failure_form_test_path on purpose to check how error messages are handled!
1
class ExamplePage < Matestack::Ui::Page
2
3
def response
4
matestack_form form_config do
5
form_input id: 'text-input', key: :foo, type: :text
6
button 'Submit me!', type: :submit
7
end
8
end
9
10
def form_config
11
{
12
for: :my_object,
13
method: :post,
14
path: failure_form_test_path
15
}
16
end
17
18
end
Copied!
If we head to localhost:3000/example, and fill in the input field with, e.g., text and click the submit button, we will get displayed our error message of seems to be invalid. Neat!

Mapping the form to an Active Record Model

To test the mapping of our form to an Active Record Model, we make sure our TestModel's description can't be empty:
1
class TestModel < ApplicationRecord
2
3
validates :description, presence:true
4
5
end
Copied!
Now, on our example page, we prepare a new instance of our TestModel that we then want to save through the form component:
1
class ExamplePage < Matestack::Ui::Page
2
3
def prepare
4
@test_model = TestModel.new
5
@test_model.title = 'Title'
6
end
7
8
def response
9
matestack_form form_config do
10
form_input id: 'title', key: :title, type: :text
11
form_input id: 'description', key: :description, type: :text
12
button text: 'Submit me!', type: :submit
13
end
14
end
15
16
def form_config
17
{
18
for: @test_model,
19
method: :post,
20
path: model_form_test_path
21
}
22
end
23
24
end
Copied!
Notice that we only prepared the title, but missed out on the description.
If we head to our example page on localhost:3000/example, we can already see the title input field filled in with Title. Trying to submit the form right away gives us the error message (can't be blank) because the description is, of course, still missing!
After filling in the description with some input and hitting the submit button again, the instance of our TestModel gets successfully saved in the database - just the way we want it to work.
Last modified 7mo ago