Ignore

Please note that your browser is not supported.

We recommend upgrading to the latest Firefox or Google Chrome.

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:

  1. 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.
  2. Validating that the submitted values contain the right information. For example, you might want to ensure that a value looks like an email address.
  3. Presenting errors back to the user if verification or validation fails.
  4. Saving verified and validated values to the database, either by creating new records, updating existing ones, or deleting records entirely.
  5. 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:

frontend/pages/messages/new.html
<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:

frontend/pages/messages/new.html
<form binding="message">
  ...
</form>

This form will also be connected to the same action:

frontend/pages/index.html
<form binding="message">
  ...
</form>

And finally, forms for a nested binding type will also be connected to a create action:

frontend/pages/index.html
<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:

frontend/pages/messages/edit.html
<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:

frontend/pages/messages/show.html
<form binding="message" endpoint="messages_update">
  ...
</form>

This also works in the case of forms nested within the binding type they are for:

frontend/pages/messages/index.html
<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:

backend/resources/messages.rb
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.

Next Up: Configuration