SimpleForm is a Ruby gem providing shorter and cleaner wrappers to the Rails FormBuilder. It supports most input types, however I couldn't find anything on contenteditable containers. That is why I built my own SimpleForm contenteditable mapping: it consists of a content-editable div and a matching hidden input, bound together with some quick Javascript.

In order to have a custom SimpleForm input, you'll need to create a MyCustomInput class under app/inputs extending SimpleForm::Inputs::Base. All the action happens in the overriden #input method.

The Input

So let's call this ContenteditableInput, save it under app/inputs/contenteditable_input.rb and add the custom behaviour to the #input method:

class ContenteditableInput < SimpleForm::Inputs::Base

  def input
    # Determine field value (either from input_html or object value or empty string)
    value = input_html_options[:value] || object.send(attribute_name)  || ''

    # If the html_safe option is present and true, prevent escaping HTML tags
    value = value.html_safe if options.key?(:html_safe) && options[:html_safe]

    # Merge input_html with the contenteditable_html option, while preserving HTML classes
    contenteditable_html = input_html_options.merge(options[:contenteditable_html] || {}) do |k, oldval, newval|
      k == :class ? [oldval, newval].join(' ') : newval
    end.merge(contenteditable: true)

    # Render the contenteditable div & the hidden input as siblings
    [
      @builder.template.content_tag(:div, value, contenteditable_html),
      @builder.hidden_field(attribute_name, input_html_options)
    ].join.html_safe
  end

end

The input provides two extra options to SimpleForm fields:

  • html_safe: When true, this prevents escaping HTML tags contained in the field's value.
  • contenteditable_html: Similar to the input_html option, but applies only to the contenteditable div (e.g. when you need extra HTML attributes applied to your container, but don't want to propagate it to the hidden input as well). The contenteditable_html option is merged with input_html. It concatenates HTML classes, which means input_html classes will be preserved.

Rendering two elements as siblings via the FormBuilder proved to be a bit more tricky than I thought (see the last 4 lines of the method): the contenteditable div renderer and the matching hidden input renderer must be paired together as an array. The [...].join.html_safe part ensures that both elements get rendered. Without the joined array, FormBuilder would only spit out the last rendered tag. Check out this link for more information about rendering HTML tags in Rails.

The Template

Inside your template you would use the Input like this:

= f.input :my_field, as: :contenteditable, input_html: {class: "shared-class"}, contenteditable_html: {class: "div-only-class"}, html_safe: true

The html_safe attribute is optional - use it if your field stores HTML and you want to render it. The previous line should output something like:

<div id="my_model_my_field" class="contenteditable shared-class div-only-class" contenteditable="true">Some content</div>
<input id="my_model_my_field" class="contenteditable shared-class" type="hidden" value="Some content" name="my_model[my_field]">

Note that the contenteditable class is provided by default, and corresponds to the SimpleForm mapping used on this field.

The Javascript

If we try to save the form now, we'll only get the old state of our field posted, because changes to the contenteditable div are not synchronised with the hidden input yet.

It's time for a bit of jQuery:

$("form").submit(function() {
  $(this).find("input[type=hidden].contenteditable").each(function() {
    $input = $(this);
    $input.val($input.siblings("div.contenteditable:first").html());
  });
});

This assumes that your fields are wrapped around by individual containers (SimpleForm does that for you). Otherwise, change the selector $input.siblings(...) to something that suits you.

Conclusion

That's it! You can now safely use content-editable containers in your forms just like any other input. The contenteditable attribute has been supported by desktop browsers for a pretty long time (IE 5.5 does it!) and plugins like the MediumEditor make it a really intuitive and user-friendly tool.