Widgets API in Rails
You might have already found yourself in a situation where you would like to distribute parts of layouts among different web applications. Let’s say you manage dozens of websites, and for some reason you need a contact form to send you an email when someone fill it in. You don’t want to implement the HTML forms for all these applications, right?
If you do a quick search on Google, you will find a lot of tutorials explaining how to create iframes, as well as some discussions about inline JavaScript. However, there are important differences between these techniques. This post explains how to create a flexible Widgets API that can be used by both approaches. It also discusses the main difference between these methods, so I hope it helps you to decide when to use each one.
Route, Controller and Views
We can start by creating a route to receive incoming requests for widgets. In the routes.rb
add the following line:
get '/widgets/:template', to: 'widgets#show'
In this way, the request /widgets/contact_form
for example, would be dispatched to the widgets
controller’s show
action with { template: 'contact_form' }
in the params
.
So, let’s create our widgets
controller:
class WidgetsController < ApplicationController
def show
respond_to do |format|
format.html { render params[:template], layout: 'widgets' }
format.js { render js: js_constructor }
end
end
private
def js_constructor
content = render_to_string(params[:template], layout: false)
"document.write(#{content.to_json})"
end
end
The show
action can respond_to HTML
and JS
formats. In both cases, params[:template]
should match a view in the app/views/widgets
folder. In our example, the expected file is contact_form
, but you can create any other piece of view. So, let’s create the contact_form.html.erb
file:
<<%= form_tag send_email_action_url do %>
<%= text_field_tag :name %>
<%= text_field_tag :email %>
<%= text_area_tag :message %>
<%= submit_tag :submit %>
<% end %>
Note that send_email_action_url
must be replaced by a valid url pointing to the action that deals with this POST
. In case you are using url helpers, make sure you use _url
instead of _path
, as it provides the host, port and path prefix.
Iframe
One way of rendering our contact form, would be creating an iframe that requests the HTML
format. The action show
renders the appropriated view using the layout: 'widgets'
. Inside the app/views/layouts
folder, we must create the widgets.html.erb
file.
<!DOCTYPE html>
<html>
<head><%= stylesheet_link_tag 'widgets', media: 'all' %></head>
<body>
<%= yield %>
<body>
</html>
This layout is minimal, including only the stylesheet file and the view’s content that is rendered by the <%= yield %>
. You have to create the widgets.css
file inside the apps/assets/stylesheets/
folder, and then define the style for all your widgets.
After the above setup, you can render our contact form in different domains, using the <iframe>
tag. Be aware that the iframe will use the style defined in the widgets.css
.
<iframe src="http://mydomain.com/widgets/contact_form" width="100%" height="100%" frameborder="0"></iframe>
PS: Depending on your Rails version, you might have to configure the X-Frame-Options
.
Some important notes about iframes:
- Browsers are aware that iframes content is served by another domain. So, the same-origin policy applies;
- Iframes are secure. The same-origin policy prevents scripts to access data from different domains, unless CORS is configured;
- The content of an iframe is defined by the origin, which includes all the style (css, fonts, js, images, etc.);
- It is necessary to set the
width
andheight
of an iframe. If the iframe’s design is not responsive, you might face difficulties adapting its size in your website.
Inline JavaScript
Another way of rendering our contact form, would be running the JavaScript code provided by the JS
format. The js_constructor
method wraps the view’s HTML code inside a document.write. Note the layout: false
, which means the widgets
layout will not be included.
We can render our contact form in different domains using the <script>
tag. The script will construct the widget by writing the HTML elements to the DOM. In this way, the widget becomes part of the layout, and the existing style is also applied to it.
<script type="text/javascript" src="http://mydomain.com/widgets/contact_form.js"></script>
Some important notes about inline JavaScript:
- Browsers execute the script from another domain. The script becomes part of the hosting site, and therefore it does not violate the same-origin policy;
- Inline JavaScript is not secure, as the script might contain an injection attack. So, you must trust the origin of any script;
- If the script writes HTML code to the DOM, the hosting site’s stylesheet will define the layout of these added elements.
Deciding When to Use Each Approach
Although we have discussed security issues, our Widgets API does not provide any unsafe script. While the Inline JavaScript should be used to construct the widget as part of the client’s HTML, the Iframe can render the widget as defined in the API server (widgets.css
).
Back to our example, we can assume that our contact form has a black background and a white font. So, let’s say we want to include this widget in a clean website with white background and black font.
If your aim is to highlight the contact form in this clean website, you can use the iframe method. However, if you would like to integrate this widget with the website, the Inline JavaScript would be the best choice. So, at the end we can say that both approaches can be used, having situations where each one of them can be more suitable.
Post by: Gabriel Hilal
Originally published at gabrielhilal.com on July 4, 2015.