EditInPlace

Gem Version Build Status Inline docs Maintainability Test Coverage

edit_in_place is a Ruby gem that facilitates the creation of user interfaces that allow the user to edit content "in place" in a natural way. edit_in_place aims to be:

  • Flexible. Everything has been designed with extensibility in mind. The edit_in_place core (this repository) can be extended for a huge variety of use cases.
  • Reliable. Every aspect of the plugin is thoroughly tested and documented.
  • Natural. Coding with edit_in_place is just as natural as editing content with it is! We think you'll really enjoy working with the edit_in_place API.

Installation

edit_in_place is a Ruby gem. If you use Bundler, you may install it by adding it to your Gemfile, like so:

gem 'edit_in_place'

And then execute:

$ bundle install

Or you may install it manually with:

$ gem install edit_in_place

edit_in_place currently has two dependencies: activesupport and middlegem. The only extension required from activesupport is Hash#deep_merge.

Concepts

edit_in_place is—at the most fundamental level—a tool to render content in different modes.

This begins with the concept of a field, which is a single, self-contained piece of modal content. Given the same input, but different modes, a field can be rendered differently. This functionality could be used in a variety of cases, but is especially useful for creating "editable content". As an example, imagine that we have built a static website. On that website, we would like to allow the owner to modify certain parts of it. The way we would approach this with edit_in_place is to make each editable portion of the page a field. Then, when the site is being viewed by a visitor, we render all the fields in a :viewing mode, but when being edited by the owner, an :editing mode. Each field is then rendered differently, given the appropriate mode. A "text" field, for example, might render plain text in a :viewing mode, but some kind of text input in an :editing mode.

A field type is a subclass of EditInPlace::FieldType that is essentially a template for a field. Given various options, including a mode, it is in charge of actually rendering the field.

To render a field, the EditInPlace::Builder#field method is used. Given a field type, field options, and an input, it will merge the field options with the default ones, transform the input as appropriate, and use the FieldType to render the content, returning the result. This is where the flexibility of edit_in_place begins to reveal itself: you see, the "input" given to #field is completely arbitrary! In other words, you can set up field types to accept whatever input they need to render the field. That means that you have virtually limitless options for how exactly you acquire and save editable data—edit_in_place doesn't care.

Furthermore, edit_in_place has the concept of field "middlewares". You can pass middlewares (middlegem is used for middlewares) to #field that can arbitrarily transform its input. This can be used for a host of things—transforming the data, validating the input, adding arguments to the input based on context, or really anything you want.

Of course, with this power comes significant verbosity. With only the edit_in_place core, you would need to set everything up yourself. However, in many cases, editable content with edit_in_place follows a fairly set pattern: "models" (not necessarily ActiveRecord ones) store editable attribtues; an edit_in_place field corresponds to one (or occasionally more) attributes; and the "editable" version of a field is a form input which allows the data to be saved back to a data store. For this functionality, see the edit_in_place extension models_in_place.

Usage

Field Types

A fair amount of configuration is required to get started with edit_in_place if you're not using models_in_place. The first step is to define some field types, which provide templates for generated fields. This can be done by extended the FieldType class and doing one of the following:

  1. Overriding the render method, which is called regardless of the mode.
  2. Relying on the default render implementation, and overriding one of the render_* that it will call. Here are a few examples:
# text_field_type.rb

class TextFieldType < EditInPlace::FieldType
  protected

  def render_viewing(mode, name, value)
    "<p>#{value}</p>"
  end

  def render_editing(mode, name, value)
    "<input type='text' value='#{value}' name='#{name]' />"
  end
end
# boolean_field_type.rb

class DummyFieldtype < EditInPlace::FieldType
  def render(mode, *)
    "You are currently #{mode} the webpage!"
  end
end

Notice that the render method is passed a mode parameter. Also note how render_viewing and render_editing will be called according to the current mode. If you want to define a render_* method for a different mode, you'll need to override the supported_modes method, like so:

class LockedField
  protected

  def render_viewing(*)
    'You are viewing this field!'
  end

  def render_editing(*)
    'You are editing this field!'
  end

  def render_admin_editing(*)
    'You are editing this field as an admin!'
  end

  def supported_modes
    [:viewing, :editing:, :admin_editing]
  end
end

Attempts to call render with a mode that is not in supported_modes will result in an EditInPlace::UnsupportedModeError, even if the field type has a corresponding render_* method.

As mentioned earlier, one of the aims of edit_in_place is to be natural for the developer. Thus, in the interests of convenience, edit_in_place allows you to "register" field types with a name, making them easier to use. We might register our "text" field type, for example, like this:

EditInPlace.configure do |c|
  c.field_types.register :text, TextFieldType.new
end

Now this:

<%= @builder.field TextFieldType.new, 'contact_name', 'Jacob' %>

Becomes:

<%= @builder.field :text, 'contact_name', 'Jacob' %>

Note that field type names must be symbols—strings are not allowed. Also note that duplicate registrations are not alowed and will raise an EditInPlace::DuplicateRegistrationError.

Another convenient feature is that you may use or register field type classes whose constructor has no parameters. For example:

@builder.config.field_types.register :image, ImageFieldType
<%= @builder.field TextFieldType, 'contact_name', 'Jacob' %>
<%= @builder.field :image, 'contact_name', 'Jacob' %>

Equivalent to:

<%= @builder.field TextFieldType.new, 'contact_name', 'Jacob' %>
<%= @builder.field ImageFieldType.new, 'contact_name', 'Jacob' %>

Configuring a Builder

The next step is creating and using an EditInPlace::Builder instance. A builder always has an EditInPlace::Configuration instance that contains all its options and context. When a builder is first instantiated, its configuration is copied from the global EditInPlace configuration. Thus, you can set any global configuration options using EditInPlace.config or EditInPlace.configure, and all builders with use those options by default. For example:

EditInPlace.configure do |c|
  c.field_options.mode = :editing
end

This would make editing the default mode. Then, you can modify builder-specific configuration using Builder#config or Builder#configure, like so:

@builder = Builder.new # current mode is :editing
@builder.config.field_options.mode = :viewing # switched to :viewing

Both EditInPlace.config and Builder#config are EditInPlace::Configuration instances, so you configure them identically. You are encouraged to check out the docs on Configuration to see all the available configuration options.

There are a few approaches to managing builder modes. One approach is to have a seperate controller for each, like so:

class ViewingController < ApplicationController
  def index
    @builder = Builder.new

    render 'some_page'
  end
end
class EditingController < ApplicationController
  def index
    @builder = Builder.new
    @builder.config.field_options.mode = :editing

    render 'some_page'
  end
end

Then, in the 'some_page' view you can use the builder in the exact same way, but get different results because of the different mode. Other options are possible, however. Use whatever works best!

Rendering Fields

Once you have some fields types and a builder, you are ready to actually render some fields! The Builder#field method is used to render a field of a given type, with the given options, and the given input. For example, with our previous TextFieldType example:

<%= @builder.field :text, 'phone_number', '(123) 456-7890' %>

When viewing the site, the user would see a simple paragraph containing the phone number. When editing the site, the user would see an editable text input. Of course, the text input currently would do nothing—you are in charge of ensuring that the data actually gets saved. You can do this however you want, but typically the fields are submitted via a form or via AJAX to a controller which saves the data. As part of the philosphy of flexibility, edit_in_place makes no attempt whatsoever to handle any of this.

If the second argument to Builder#field is either a FieldOptions instance or a hash, then it will be used as the options for the field. For example, you could render a specific field as always editable:

<%= @builder.field :text, { mode: :editing }, 'phone_number', '(123) 456-7890' %>

If your field type happens to expect the first input argument to be a hash, you will need to pass an empty options hash, like this:

<%= @builder.field :some_type, {}, { option: true }, 'etc' %>

Builder also overrides method_missing to make it easier to call registered fields. If, for example, you have registered a :text field type, you can do the following:

<%= @builder.text_field 'phone_number', '(123) 456-7890' %>

Middlewares

Perhaps the most powerful feature of edit_in_place are the field middlewares. edit_in_place uses middlegem for middlewares, which you may want to briefly review. Field middlewares allow the inputs to a field to be transformed before actually making it to the field type's render method. The use cases for this are limitless.

There are two steps for using field middlewares: defining them and using them. First, you must define all the middleware classes that you will be using in your application. This is to ensure that middlewares are always run in the right order. For example, let's say we have two middleware classes, one that multiplies a number by 10, and another that surrounds it with parentheses. We would define the middleware as follows:

EditInPlace.configure do |c|
  c.defined_middleware = [
    MultiplyMiddleware,
    ParenthesesMiddleware
]

This will ensure that, no matter what order they're added, the MultplyMiddleware will always be run before the ParenthesesMiddleware. If a middleware is used that has not been defined, a Middlegem::UnpermittedMiddleware error will be raised by middlegem.

Next, the middlewares can be actually used. Middlewares reside in the FieldOptions class, meaning you have three options for adding middlewares to fields: adding it to the global EditInPlace configuration, adding it to the Builder configuration, and passing them to Builder#field. You might want all fields to be surrounded in parentheses, for example, but only some to be multiplied by ten. This could be accomplished with:

class SomeController
  def index
    @builder = Builder.new
    @builder.config.field_options.middlewares << ParenthesesMiddleware.new
  end
end
<%= @builder.field :text, { middlewares: [MultiplyMiddleware.new] }, 'name', '500' %>
<%= @builder.field :text, 'name', 'a string' %>

Now, when viewing the site, the first one would show "(5000)" and the second would show "(a string)". Note that middlewares are often too verbose to be easily used like this. They are really best used by edit_in_place extensions.

Like field types, middlewares can also be registered, using Configuration#registered_middlewares. For example:

@builder = Builder.new
@builder.configure do |c|
  c.registered_middlewares.register :parentheses, ParenthesesMiddleware
  c.field_options.middlewares << :parentheses

Scoped Builders

There will likely be times when you wish for many fields to have the same FieldOptions. This can be accomplished with the Builder#scoped method, which allows field options to be shared across a block. For example:

<%= @builder.scoped middlewares: [AppendArgumentMiddleware.new('example')] |b| do %>
  <%= b.field :text, 'name', 'value' %>
<% end %>

Now any fields generated with the scoped b builder will have an 'example' argument appended to their input (assuming that AppendArgumentMiddleware has been defined). In fact, scoping a builder with middleware is comon enough that Builder also provides a with_middlewares convenience method:

<%= @builder.with_middlewares AppendArgumentMiddleware.new('example') |b| do %>
  <%= b.field :text, 'name', 'value' %>
<% end %>

Note that Builder#scope and Builder#middleware_scope are aliases for Builder#scoped and Builder#with_middlewares, respectively.

Extending Builder

If you are developing an extension for edit_in_place, you may wish to add methods to Builder. While you could create a subclass, subclassing has its share of problems. You could not, for example, use multiple builder subclasses at once. Thus, edit_in_place provides an ExtendedBuilder class which you can use to add new methods to Builder. Essentially, ExtendedBuilder stores a base builder and delegates all missing method calls to it. You can use it like:

class MyBuilderExtension < EditInPlace::ExtendedBuilder
  def hello
    'Hello, world!'
  end
end

And then instantiate it:

class SomeController < ApplicationController
  def index
    base = Builder.new
    @my_builder = MyBuilderExtension.new(base)
  end
end

Then, in the view:

<%= @my_builder.hello %>

These builder extension can be chained as many times as desired. Please note however that you should only add new methods, never override existing ones. If you override existing methods, further down the chain, that method may be called, and will not be sent to your extension, which could cause some confusing results.

License

The gem is available as open source under the terms of the MIT License.