The first sentence on the Polymer Project page says that Web Components usher in a new era of web development based on encapsulated and interoperable custom elements that extend HTML itself. I could not agree more - but before this new era starts, we should still be able to use the good old tools from the past. Every Web developer has loads of jQuery plugins at his disposal, but there are a few key gotchas when working with jQuery within a Polymer element that need to be addressed.

Baasic and Polymer living in (almost) perfect harmony

Working on set of Web components for Baasic, I begun to search for a simple WYSIWYG HTML editor, and there are plenty of nice jQuery plugins that can be used right off the box in standard Web projects. But things got complicated really fast when we tried to use any of these in our Web components. It turns out that when you place such “ordinary” plugin inside of Web component’s Shadow DOM, it often cannot be used by jQuery. For those of you who still didn’t stumble upon the concepts of Shadow DOM and Light DOM, there is a nice introduction here.

A naive approach

Many plugins assume that all of their content is available in the Light DOM, and that leads us to all kinds of problems. I got all sorts of javascript errors and glitches, but none of the available plugins was working as it should. My first approach to resolve this situation was to set up a jQuery plugin inside Light DOM and pass it into the Web component as content, as suggested in this post. In our particular case, most of the HTML editors operate on a textarea element that has to be present in the Light DOM. So, here is my initial attempt, using the wildly popular CKEditor:

<link rel="import" href="../polymer/polymer.html">
<script type="text/javascript">
    CKEDITOR_BASEPATH = '/bower_components/ckeditor/';
</script>
<script type="text/javascript" src="../ckeditor/ckeditor.js"></script>
<script type="text/javascript" src="../ckeditor/adapters/jquery.js"></script>
<polymer-element name="baasic-htmleditor" attributes="value">
    <template>
        <style>
            :host {
                display: block;
            }
        </style>
        <content id="contentEditor"></content>
    </template>
    <script>
        Polymer('baasic-htmleditor', {
            getData: function () {
                return CKEDITOR.instances.editor1.getData();
            },
            setData: function(value)
            {
                this.value = value;
                CKEDITOR.instances.editor1.setData(this.value);
            },
            domReady: function () {
                $(this.$.contentEditor.getDistributedNodes()[0]).ckeditor();
                CKEDITOR.instances.editor1.setData(this.value);
            }
        });
    </script>
</polymer-element>

The component itself is used like this:

<baasic-htmleditor id="htmlEditor" value=""><textarea></textarea></baasic-htmleditor>

The Light DOM element can be passed through several levels of hierarchy (nested components) by using an empty content tag:

<baasic-htmleditor id="htmlEditor" value=""><content></content></baasic-htmleditor>

It works allright, but lights up a “code smell” indicator right away. In my opinion, this approach defeats the whole purpose of Web components, that should be modular and self-contained elements, without the need for such tricks. Therefore, an alternative approach is needed to allow existing jQuery plugins to coexist with Web components.

A naive approach, take two

Here is another variant of this approach, using another editor - Redactor. This time, we are dynamically generating a textarea element inside of the Light DOM, instead of passing it through from the page holding the element.

<link rel="import" href="../polymer/polymer.html">
<link rel="stylesheet" href="../redactor/redactor/redactor.css">
<polymer-element name="test-editor" attributes="value">
    <template>
    <style>
            :host {
                display: block;
            }
        </style>
        <link rel="stylesheet" href="../redactor/redactor/redactor.css">
        <content id="contentEditor"></content>
    </template>
    <script type="text/javascript" src="../redactor/redactor/redactor.js"></script>
    <script>
        Polymer('test-editor', {
            domReady: function () {
                var el = document.createElement("textarea");
                el.id = "content";
                this.shadowRoot.host.appendChild(el);
                //let's see what happened in Light DOM
                var lightDom = $(this.$.contentEditor.getDistributedNodes());
                //make editor magic happen
                $(el).redactor();
            }
        });
    </script>
</polymer-element>

This approach still does not solve the problem of nesting Web components - we did succeed in moving the textarea up into Light DOM, however it will still be encapsulated into the master control’s Shadow DOM.

Almost there…

Here is another and much cleaner attempt to make it work.

<link rel="import" href="../polymer/polymer.html">
<link rel="stylesheet" href="../redactor/redactor/redactor.css">

<polymer-element name="test-editor" attributes="value">
    <template>
        <style>
            :host {
                display: block;
            }
        </style>
        <link rel="stylesheet" href="../redactor/redactor/redactor.css">
        <content></content>
        <textarea id="editor"></textarea>

    </template>
    <script type="text/javascript" src="../redactor/redactor/redactor.js"></script>
    <script>
        Polymer('test-editor', {
            domReady: function () {
                $(this.$.editor).redactor();
            }
        });

    </script>
</polymer-element>

This is simple enough and almost works, after we fixed CSS a bit (removing body selectors and similar stuff). However, dropdown menus are not positioned correctly, and HTML links are not inserted where they should be, as some of the functionality still obviously depends on base textarea being placed in the Light DOM.

Alternative solutions that try to wrap CKeditor or similar controls are doing things a bit differently, but still without too much success. For example, this wrapper that can be found at Component Kitchen works, but only in browsers that currently do not support Web components natively - so Chrome and Opera are out.

The morale of the story

You should not take the abundance of jQuery plugins for granted if you want to use them with Polymer; chances are that many of them will not work, as most plugins assume all of their content is available in Light DOM. We have managed to integrate some of them into our Polymer elements without changes; a couple of plugins required minor tweaks, but the most complex ones will need a total rewrite to make them ready for Web components. We are still looking for the right HTML editor that will be compatible with Polymer. In the meantime, there are a couple of “native” markdown editor components like this one that behave quite nicely.

Any thoughts on comments on how to use other jQuery plugins with Polymer? We’d love to hear them!

Feel free to leave a comment

comments powered by Disqus