Reflection: Actions
Actions cause changes in the underlying state of your application, often changing data stored in the database. Reflection is concerned with actions triggered by a user interacting with the interface, such as form submissions. When data needs to be changed, action endpoints are responsible for interacting directly with data sources to create, update, or delete data.
Each reflected action handles the following concerns for you:
- Verifying that the values submitted through the form are expected, removing values that shouldn't be there. This prevents users from tampering with a form and submitting values that they shouldn't.
- Validating that the submitted values contain the right information. For example, you might want to ensure that a value looks like an email address.
- Presenting errors back to the user if verification or validation fails.
- Saving verified and validated values to the database, either by creating new records, updating existing ones, or deleting records entirely.
- Redirecting the user to the next page in the application.
Let's look at an example of how an action is defined. Here's a form that defines a message
binding with a single content
attribute:
<form binding="message">
<div class="form-field">
<input type="text" binding="content" required>
</div>
<input type="submit" value="Save">
</form>
Reflection defines a matching messages
data source, which you can inspect using the info:sources
command:
:messages pakyow/reflection
has_many :replies
attribute :id, :bignum
attribute :content, :string
attribute :created_at, :datetime
attribute :updated_at, :datetime
Next, Reflection defines an action that creates messages through the data source. The action is attached at an endpoint, just like the presentation endpoints we discussed in the previous section. Reflection builds endpoints based on REST, or Representational State Transfer. Following RESTful conventions, action endpoints are always defined with a non-GET
request method.
Here's what the endpoint looks like for the example above:
Action Name HTTP Method Request Path Who Defined the Action
----------- ----------- ------------ ----------------------
:messages_create POST /messages pakyow/reflection
The form is automatically configured to submit to the action defined for it. You can see how reflection handles the form submission in the logs. Here's what you would see after submitting a form with valid values:
124.00μs http.d5edd133 | POST /messages (for 127.0.0.1 at 2019-07-16 18:52:59 +0000)
1.58ms http.d5edd133 | [reflection] verified and validated submitted values for `message'
1.67ms http.d5edd133 | [reflection] performing `messages_create' for `/'
5.55ms http.d5edd133 | [reflection] changes have been saved to the `messages' data source
24.44ms http.d5edd133 | [reflection] redirecting to `/messages/13'
32.77ms http.d5edd133 | 302 (Found)
Submissions that don't pass validation are also logged:
643.00μs http.984c735c | POST /messages (for 127.0.0.1 at 2019-07-04 15:27:53 +0000)
4.98ms http.984c735c | INVALID DATA
|
| › Provided data didn't pass verification
|
| Here's the data:
|
| {
| "message": {
| "content": ""
| }
| }
|
| And here are the failures:
|
| {
| "message": {
| "content": [
| "cannot be blank"
| ]
| }
| }
Just for fun, here's what the backend code would look like if we were to write it on our own:
resource :messages, "/messages" do
create do
verify do
required :message do
required :content do
validate :presence
end
end
end
created_message = data.messages.create(params[:message]).one
redirect :messages_show, created_message
end
end
Not only does reflection keep you from writing this code yourself, it automatically reflects changes made in your form. If you add another field to the form, the reflection will expect the new field when it performs the verification step. Writing the code yourself means coordinating changes in the view with changes on the backend, requiring someone to manually update the verify
block to expect the new field.
Reflection makes your backend actions reactive, updating your backend to immediately reflect frontend changes.
Forms for create vs update
Reflection defines either a create
or update
action for forms it finds in your view templates. One of these two actions is chosen based on the form's context. We'll go through all the conventions around form actions in this section.
Forms for create
All forms are connected to a create
action unless additional context is available. This includes forms defined at the RESTful presentation path for new
, as well as standalone forms that aren't connected to any outside context.
For example, this form will be connected to a create
action for messages
:
<form binding="message">
...
</form>
This form will also be connected to the same action:
<form binding="message">
...
</form>
And finally, forms for a nested binding type will also be connected to a create
action:
<article binding="message">
<form binding="reply">
...
</form>
</article>
Reflection defines a create
action for comments
as a child resource to messages
. This means that comments created through the above form will automatically be associated with the message they are defined within.
Forms for update
Reflection defines an update
action when a form is defined at a presentation path matching the RESTful edit path for a resource. For example, this view template will be connected to an update
action for messages
:
<form binding="message">
...
</form>
Reflection makes this connection because 1) the form is defined in a view template at a presentation path for edit and 2) the form is setup for the resource we're editing. This tells reflection that your intent is to edit a message, then update it in the database.
You must set an endpoint explicitly to connect other forms to update
. Reflection will use the context from the current request path to build the correct action. For example, this form, defined at the presentation path for showing a message, would be connected to an update
action for the message being shown:
<form binding="message" endpoint="messages_update">
...
</form>
This also works in the case of forms nested within the binding type they are for:
<article binding="message">
<form endpoint="messages_update">
...
</form>
</article>
Reflection will render the message
binding once for every message, settting up an update form for each one. Form endpoints are discussed more in the next section.
Specifying the action endpoint
You can always specify the action endpoint for a form. This is useful in cases where you need to break away from the default conventions, and required in cases like delete actions (we'll discuss delete actions more in the next section).
To define an action endpoint, just set the endpoint
attribute on the element:
<form binding="message" endpoint="messages_create">
...
</form>
You can see information about defined actions, including their names, by running the info:endpoints
command from the command line.
Custom actions
If your backend defines a custom action not handled by reflection, you can hook the form up to it by setting the endpoint
attribute as discussed above. For example, say there's an endpoint for messages named increment
that increments an internal counter of some sort. We can hook up a form to the increment
endpoint like this:
<article binding="message">
<p binding="content">
message content goes here
</p>
<form endpoint="messages_increment">
<input type="submit" value="Give a thumbs up">
</form>
</article>
When submitted, the form will call the custom increment
action on the backend.
Setting up delete actions
Delete actions usually aren't tied to forms. Instead, delete actions are usually triggered by links or buttons. Here's an example view template that defines a delete link for a message:
<article binding="message">
<p binding="content">
message content goes here
</p>
<a endpoint="messages_delete">
Delete this message
</a>
</article>
Setting up a delete action requires the endpoint to be defined explicitly on the link. Reflection understands your intent based on the name of the endpoint, setting up the delete action for you on the backend. When the link is clicked, the delete endpoint will be invoked and the data deleted.
Pakyow does a few other things to create a nice user experience for delete endpoints. This is discussed more in the frontend docs, so check that out if you want to learn more.
Presenting form errors
Pakyow has several built-in form helpers, including one for presenting form errors. You can use the built in error handling behavior by defining a form errors element within your form:
<form>
<ul class="form-errors" ui="form-errors">
<li binding="error.message">
Error message goes here.
</li>
</ul>
...
</form>
When invalid values are submitted, Pakyow will automatically present errors back to the user.
Most modern web browsers offer built-in form validation that covers simple cases like missing values for required fields. To disable browser validation in favor of Pakyow's backend validation, add the novalidate
attribute to your form element:
<form novalidate>
...
</form>
Pakyow continues to provide backend validation for forms even when browser validation is enabled. This ensures that sophisticated users can't bypass browser validation and submit invalid values.
Forms are discussed in more detail in the frontend docs. Take a peek over there to learn more.
Using nested forms
Reflection gracefully handles forms nested within bindings. Let's walk through a couple of examples that illustrate how you can use nested forms to build more complex behavior into your interface.
Forms for the parent binding
The first use-case for nested forms is defining a form specifically for the parent binding that contains it. You could use this approach to define an edit form for every message in a list. Here's what a view template might look like in this case:
<article binding="message">
<p binding="content">
message content goes here
</p>
<form endpoint="messages_update">
<div class="form-field">
<label for="content">
Update this message:
</label>
<input type="text" binding="content">
</div>
<input type="submit" value="Save">
</form>
</article>
When Reflection presents the messages, it sets up each form for updating the message that the form is presented for. Submitting the form for a message will update the related message with a new content
value.
Forms for a new binding type
Nested forms are also useful for creating related data for a parent binding. For example, we might want to let users reply to messages. This can be done by defining a form for a new binding type within the parent binding:
<article binding="message">
<p binding="content">
message content goes here
</p>
<form endpoint="reply">
<div class="form-field">
<input type="text" placeholder="Leave a reply..." binding="content">
</div>
<input type="submit" value="Submit">
</form>
</article>
When Reflection presents the message, it sets up the reply form for creating new replies. Replies created through the form will also be associated with the message they were created for. This behavior relies on the reflected association behavior for data sources.
Multiple forms for a single action
As your project grows, you may have several forms that submit to the same endpoint. Each form might be defined with its own rules for validation, or even completely different fields. Reflection is smart enough to distinguish between form submissions and perform the correct behavior for each.
Here are two forms that will both be hooked up to the messages_create
action:
<form binding="message">
<div class="form-field">
<input type="text" binding="title">
</div>
<div class="form-field">
<input type="text" binding="content" required>
</div>
<input type="submit" value="Create" class="button">
</form>
<form binding="message">
<div class="form-field">
<input type="text" binding="content" required>
</div>
<input type="submit" value="Create" class="button">
</form>
These forms are pretty similar, the one difference being that the first form defines an optional field for title
. When the first form is submitted, the action will allow a value for title
through during verification. But for the second form, the action will only expect a value for content
.
Customizing reflected actions
Reflection is designed to integrate with any existing backend actions that might exist in your project. It extends your application with actions only when the doesn't already exist. This gives you a way to replace a reflected action entirely, or extend it to fit your specific needs.
Replacing an action
You can replace a reflected action by defining an action yourself. Let's look at an example. In this case, we'll assume that we need an action for creating messages through a form defined on the frontend. Pakyow expects you to define the action at a route that follows RESTful conventions for request method and path.
When reflection looks for an existing action, it only looks for routes that match the method and path. Reflection does not require you to setup your backend endpoints in any particular structure.
Here's how you would define an action for create
within the messages
resource:
resource :messages, "/messages" do
create do
# Custom behavior can be performed here.
end
end
The :messages
argument defines the type of data our resource is for, while the second /messages
argument defines the request path that the resource is mounted at. Defining the resource with a create action is all that's required to replace the reflected behavior. Reflection will see that an action already exists and will step aside and let the custom code handle these form submissions.
Extending an action with operations
Actions can be extended rather than completely replaced. There are a couple of ways to do this depending on what exactly you need to do. The first approach is to extend the reflect
operation.
Operations are a backend feature in Pakyow that let you define a sequence of steps to perform in order. Most of the business logic for an application will be defined in operations, and reflection is no exception. Here are the steps defined on the reflect
operation as it pertains to actions:
verify
: Verifies and validates submitted values, raising an error if it encounters invalid data.perform
: Performs the business logic for the action, such as creating data in the database.redirect
: Redirects the user to a logical endpoint after the action is performed.
Operations can be extended at runtime, injecting custom behavior where you need it. Each step is defined as an "action" on the operation (not to be confused with the action endpoint on the resource). Here's how you can extend the reflect
operation to replace the perform
step:
resource :messages, "/messages" do
create do
operations.reflect do
action :perform do
# Custom behavior goes here.
end
end
end
end
The operation will still handle verify
and redirect
, which take place before and after perform
. Custom behavior you define in the perform
action will be used instead of the default perform behavior defined by reflection.
You can also insert new steps into the operation. Here's how you can add behavior after the perform
step:
resource :messages, "/messages" do
create do
operations.reflect do
action :custom, after: :perform do
# Custom behavior goes here.
end
end
end
end
Now the entire reflect
operation will be called, but your custom behavior will be called between the default perform
and redirect
behavior.
Extending an action with helpers
Reflection makes several helper methods available for you to use. You can use these helpers as primitives to build your own custom behavior with. Here's a list of available action helpers:
with_reflected_action
Lets you safely perform behavior if a reflected action is available.
with_reflected_action do |action|
# only called if a reflected action is available
end
verify_reflected_form
Performs verification and validation on a form submission.
perform_reflected_action
Performs the behavior for the action, creating, updating, or deleting data as necessary.
redirect_to_reflected_destination
Redirects the user to the logical destination for the action.
reflected_destination
Returns the path to the destination for the action. Does not perform the redirect.