I had to create an embeddable and portable widget for one of our customers. What it seemed to be an easy task it had a number of challenges to overcome. On this blog post I am sharing those challenges and what I did to solve each one of them.
The third party JavaScript widget
Our customer wanted to have a calculator widget on its website for one its lab products. The widget had to be hosted in our servers and the calculation algorithm was to be somehow protected. As with any third party javascript widget, we face the following:
- We don't own the DOM, so forget about modifying the container page's CSS or altering the page
- We should avoid conflicts with site's script and its global scoped variables
- To encapsulate the algorithm in an API, thus our API should support CORS
CSS Inline Injection
If you cannot modify the container's CSS there is only a couple of solutions available in order to avoid overlapping with their site's style. One of them is using a tool like cleanslate, which provides the functionality to reset your widget's stylesheet providing a top-level namespace (cleanslate), that you use to style the HTML of your widget. But I was in a rush (had to do the widget in less than three days) and I also wanted to use bootstrap style elements and I didn't want to include the full CSS, so the other option was to use a technique I call: CSS Inline Injection.
The technique is very simple, we create the HTML of the form (call me lazy but I used an online tool for this task too: https://bootsnipp.com/forms), and then we "inject" the styles that the HTML classes specify into their elements themselves. For that task, I used the excellent tool created by "TijsVerkoyen": CssToInlineStyles.
Assuming you are on your project root and having composer
installed on your computer globally:
composer require tijsverkoyen/css-to-inline-styles
Then I created the following script:
1require('vendor/autoload.php'); // use composer vendor autoloader
2
3use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles;
4
5// create instance
6$cssToInlineStyles = new CssToInlineStyles();
7
8// get the HTML of your bootstrap form
9$html = file_get_contents(__DIR__ . '/form.html');
10// get the CSS contents
11$css = file_get_contents(__DIR__ .'/bootstrap.css');
12
13// and finally save the HTML of the form with the inline injected styles
14file_put_contents(__DIR__ . '/inline-form.html', $cssToInlineStyles->convert($html, $css));
After we do that, the form elements will have this nasty
look:
1<input id="{INPUT-ID}" name="{INPUT-NAME}" type="text" placeholder="Enter value in liters"
2 class="form-control input-md"
3 style="margin: 0; font: inherit; color: #555; line-height: 1.42857143; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; font-family: inherit; font-size: 14px; display: block; width: 100%; height: 34px; padding: 6px 12px; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;"/>
It's ugly I know, but this way we do not need to add a big CSS file shipped with our widget. Nevertheless, I would say that my widget wasn't really big (only five inputs), if you need to create a bigger widget, I highly recommend you to check cleanslate. Very, very easy to use (wink).
Avoiding Script Conflicts
Here comes the fun part, we need to ensure that our code doesn't conflict with our beloved host. So, my first thought was to simply create a jquery module with a loader that ensures jQuery existed. The following is the pattern I was about to use:
1; (function (MYNAMESPACE, undefined) {
2 // private variables here
3 // ...
4
5 // public methods
6 MYNAMESPACE.initialize = function () {
7 // initialization procedures here
8 initializejQuery();
9 };
10 MYNAMESPACE.scrollTo = function (id) {
11 jQuery('html,body').animate({ scrollTop: $("#" + id).offset().top - 120 }, 'slow');
12 };
13 // private methods
14 // ensure jQuery existed?
15 function initializejQuery() {
16 if (window.jQuery === undefined || (MYNAMESPACE.jQueryLatest && window.jQuery.fn.jquery !== '3.1.0')) {
17 injectScript(getProtocol() + 'code.jquery.com/jquery-3.1.0.min.js', main);
18 } else {
19 jQuery = window.jQuery;
20 main();
21 }
22 }
23 function getProtocol() {
24 return ('https:' == document.location.protocol ? 'https://' : 'http://');
25 }
26 function injectScript(src, cb) {
27 var sj = document.createElement('script');
28 sj.type = 'text/javascript';
29 sj.async = true;
30 sj.src = src;
31 sj.addEventListener ? sj.addEventListener('load', cb, false) : sj.attachEvent('onload', cb);
32 var s = document.getElementsByTagName('script')[0];
33 s.parentNode.insertBefore(sj, s);
34 }
35 function main() {
36 // initialization magic
37 }
38})(window.MYNAMESPACE = window.MYNAMESPACE || {});
That module pattern was good but that implied the usage of a third call to our API to get the form's html. So, I decided to use requireJs
as it makes the process of an embeddable script very simple and it has an optimizer tool called r.js
that takes care of dependency handling, uglifying, minifiying, etc...
Setting up the development environment
We will need requirejs
, bower
and gulp
. If you don't have any of them, then you need to install them using npm
package manager - I am assuming that you have nodejs
installed in your development machine. We are at the project's root folder, so lets start with our our package.json
file:
1npm init
This will prompt us to answer a few questions and once completed, it will create a file in the root directory named package.json
. This file provides information about the project and its dependencies. For more information, please see the great tutorial of Travis Maynard. Now, lets do the second step:
1# install global dependencies if we don't have them
2npm install -g requirejs
3npm install -g bower
4npm install -g gulp
5# we also need to install gulp locally on our projects root
6npm install --save-dev gulp
The -g
parameter means that you install them globally. If you don't want to do that, you are free to install them locally on your project's directory by removing that parameter. The commands will be placed on the node_modules/.bin
directory.
After, I needed to install the required packages for my gulp.js
file. My tasks were really simple: use the requirejs optimizer tool and move the built to another location. So, I only installed gulp-run
pipe plugin. The resulting gulp.js
file was the following:
1var gulp = require('gulp');
2
3// include plugins
4var run = require('gulp-run');
5
6// use gulp-run to build requirejs optimizer
7gulp.task('build', function () {
8 return run('r.js -o build.js').exec()
9 .pipe(gulp.dest('output'))
10});
11
12// use watch to automatically build when the file has been changed
13gulp.task('watch', function () {
14 gulp.watch('js/*.js', ['build']);
15});
16
17gulp.task('move', ['build'], function () {
18 return run('cp ./dist/my-widget-min.js ~/path/api/web/js/').exec()
19 .pipe(gulp.dest('output'));
20});
That file created three tasks: build
, watch
and move
. The only thing I had to do was to call gulp move
to have my plugin compiled and moved to the location of my local api service to test it.
As you can see on the above code is the use of the build.js
file. That file is required for the requirejs
optimizer tool. For requirejs
there is a couple of files to do (please visit requirejs.org for further information). There is a number of processes and files to be done prior having that build.js
:
Install bower components
We need almond (an AMD replacement loader for RequireJS), requireJs-Text (a text loader plugin) and jquery.
1# https://github.com/requirejs/almond#almond
2bower install almond
3# https://github.com/requirejs/text
4bower install requirejs-text
5bower install jquery
Those commands will install the project dependencies into the bower_components
folder.
Create RequireJs config.js file
For more information about the configuration options available, please visit: requirejs.org/docs/optimization.html#basics
1var requirejs = {
2 paths: {
3 jquery: 'bower_components/jquery/dist/jquery',
4 text:'bower_components/text/text'
5 }
6};
Create RequireJs build.js file
And finally our build.js
file. This file tells the r.js
optimizer tool the options on the command line when building your optimized file. Mine looked like this:
1({
2 baseUrl:'',
3 mainConfigFile: 'config.js',
4 name: 'bower_components/almond/almond',
5 include: ['js/widget'],
6 out: 'dist/widget.min.js',
7 optimizeCss: 'standard', // if we need it
8 stubModules: ['text']
9})
The actual widget.js file
I created a new js
directory and created my widget.js file there:
1require(["jquery", "js/app"], function($, app){
2 'use strict';
3 var config = {
4 // configuration parameters here
5 };
6 $(function() {
7 app.init(config);
8 });
9});
This file worked as an entry script to initialize settings for the actual widget, the one that had all the widget functionality and dynamics was the app.js:
1define(['jquery', 'text!template/form.html'], function ($, formHtml) {
2 'use strict';
3
4 // private variables here...
5 var settings, $form;
6
7 var app = {
8 init: function (config) {
9 // get the settings and make them available through the app
10 settings = config;
11 $(formHtml).insertAfter('element where to inject the form');
12 // get a reference of the form after is inserted
13 $form = $('#FORM-ID');
14 // call initialization methods
15 initializeEvents();
16 }
17 };
18
19 // example initialization
20 function initializeEvents() {
21 // here I have used the $form pointer to initialize the events on the form
22 }
23
24 return app;
25});
My finished project folder structure look like this (dist
is only created once I call gulp build
or gulp move
commands):
+- bower_components
+- node_modules
+- js
|-- widget.js
|-- app.js
+ dist
|- widget.min.js
+ template
|- form.html
build.js
config.js
gulpfile.js
package.json
I know it's not easy to follow, that's why I have created a template repository to ease the task to understand this process.
Embedding the widget
In order to embed the widget to the page, I opt for the following technique:
1<script>
2 (function (window, document) {
3 var loader = function () {
4 var script = document.createElement("script"), tag = document.getElementsByTagName("script")[0];
5 script.src = ('https:' == document.location.protocol ? 'https://' : 'http://') + 'example.js/widget.min.js';
6 tag.parentNode.insertBefore(script, tag);
7 };
8 window.addEventListener ? window.addEventListener("load", loader, false) : window.attachEvent("onload", loader);
9 })(window, document);
10</script>
Add CORS support to your API
There are many ways to do it, but remember, I had no much time so I decided for the easiest, configuring our .htaccess
file:
<IfModule mod_headers.c>
Header always set Access-Control-Allow-Origin *
Header always set Access-Control-Allow-Methods "POST,GET,DELETE,PUT,PATCH,DELETE,HEAD"
Header always set Access-Control-Max-Age 86400
Header always set Access-Control-Allow-Headers "Content-Type, Access-Control-Allow-Headers,
Authorization, X-Requested-With, Origin, Accept, Client-Security-Token"
</IfModule>
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule . blank.html
You probably asking why I added all possible methods on my headers, that means all end points of my api do handle those methods? Well, the answer is no. My API is built upon request filters (or middlewares) that permit only certain methods. For example, my calculator API only permitted POST
, so even if my headers are set to all possible methods, my API only support POST
for that specific controller.
Why did I do it that way? The reason was a matter of speed and by adding that line on my .htaccess
file I didn't have to provide an extra method on my API for each endpoint that specifies which methods were allowed. If I was to create a public API I would've changed it but for a private usage, I thought that by adding OAuth2 + Filtering options that was more than enough.
One interesting part is the last one. Let's check it again:
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule . blank.html
I created a blank.html
page and redirected all requests calls to OPTIONS
to that page. So that request will always returned the allowed headers
and doesn't need to go throughout the filtering process of my API routes. The reason for that call from the jQuery.ajax
of your widget is because of preflighted calls
: Unlike simple requests, "preflighted" requests first send an HTTP OPTIONS request header to the resource on the other domain, in order to determine whether the actual request is safe to send.
References
- RequireJS
- Thomassileo
- Travis Maynard
- GulpJS
- Cleanslatecss
- 2amigos
Accelerate Your Career with 2am.tech
Join our team and collaborate with top tech professionals on cutting-edge projects, shaping the future of software development with your creativity and expertise.
Get Started