We have published a three-part series covering the Polymer platform basics almost a year ago. It included a set of 8 components that together provide simple content management functionality for the Baasic service, including article preview and editing, authorization, and session expiration control. We’ve kept an eye on all things related to Polymer while building support for other platforms. However, when Google released the version 1.0 at this year’s Google IO conference held in May and declared it production-ready, we decided that now is the right time to make all our components compatible with the new platform. It has been quite an experience, and here are our thoughts and the lessons that we’ve learned during the process.

What’s new

According to Google, the new library is about 3x faster on Chrome, 4x faster on Safari, and a third less code than in developer preview. However, these performance improvements come at a price: there are many breaking changes that developers need to be aware of. I would strongly recommend that you spend some time going through the migration guide, as the Polymer team did a really nice job explaining the changes in v1.0. Apparently, they are also working on a tool called polyup that will perform the migration from v0.5 to 1.0 automatically. This time I opted for an old-fashioned manual approach, to see what issues and challenges lie ahead.

Before we start, please go through the previous post that describes how Baasic Polymer demo works. You might want to get the source code for all components from our GitHub repository - Polymer repo names are prefixed with baasic-sdk-polymer. A complete list can be found in our Dev Center, under “SDK: Polymer”.

Changes

Some changes are rather obvious. Polymer no longer requires the shadow DOM polyfill, which is included in webcomponents.js. We are using the smaller webcomponents-lite.js library instead. Shadow DOM was all the rage in the previous version, but it turned out that polyfilling it is very difficult. The entire DOM API surface had to be wrapped, resulting in slow and complex solutions. This is why a new concept - shady DOM - was born. It is a much lighter implementation of the local DOM that offers similar encapsulation features, as long as one uses the shady DOM APIs to work with scoped trees. Note that shady DOM and shadow DOM are compatible with each other: shady DOM API uses the native shadow DOM in browsers when it is available.

Styling local DOM is also a bit different - for example, the Polymer team is currently exploring other options for theming that will make /deep/ and ::shadow combinators obsolete.

Element registration is now completely different: the <polymer-element> tag is no longer used to define an element. In this release, these are replaced by a <dom-module> element (to define local DOM and styles) and the Polymer call (to register the element).

Declared properties introduce new syntax and features. Same holds for data binding and many other features that are described in the migration guide.

Issues

As it turns out, the syntax differences were easy to resolve, the real challenges in the upgrade process were caused by more subtle changes in the platform. After I finished resolving changes mentioned in the migration guide, I was greeted with the following message:

Uncaught NotSupportedError: Failed to execute 'registerElement' on 'Document': Registration failed for type 'baasic-ajax'. The tag name specified in 'extends' is a custom element name. Use inheritance instead.

The general approach in the previous version was to inherit the base iron-ajax element (formerly core-ajax), and override its go method to include the required Authorization headers on every call (these are in turn retrieved from the local cache) to make sure we are in line with the Baasic authorization infrastructure. Unfortunately, this isn’t going to work in 1.0. This release doesn’t support inheriting from other custom elements — only from standard DOM elements. This should be supported in a future release, so we are left in the cold for now. The suggested approach is to use either composition or mixins to share functionality between elements.

The good news is that Polymer now supports extending custom element prototypes with shared code modules called behaviors. I must admit that I never got along with the declarative approach for some of the core elements, especially Ajax. It was adequate for simple scenarios, but using it to access a set of REST endpoints from a single element, each requiring different methods and parameters, was a bit clunky. This time I decided to step back, and encapsulate the Ajax functionality into a separate behavior. It includes everything we need for this demo in less than 60 lines of code. The built-in Ajax element offers more functionality, at the expense of flexibility.

    var Baasic = Baasic || {};
    Baasic.Ajax = {
    /**
    * Go method performs a HTTP request to the URL specified in the param list. User can specify method, body content, authorization fields and a content type of the request.
    */
    go: function (url, method, body, authorization, contentType) {
        var baasicUser = Baasic.Cache.getItem('baasicUser');
        var promise = new Promise(function (resolve, reject) {
            var authHeader = '';
            if (baasicUser && baasicUser.accessToken) {
                authHeader = baasicUser.tokenType + ' ' + baasicUser.accessToken;
                //referesh token sliding expiration time
                baasicUser.expiresAbsolute = new Date(new Date().getTime() + (baasicUser.expiresIn * 1000));
                Baasic.Cache.setItem('baasicUser', baasicUser, {
                    expirationAbsolute: baasicUser.expiresAbsolute,
                    expirationSliding: null,
                    priority: Cache.Priority.HIGH,
                    callback: null
                });
            }
            else {
                authHeader = '';
            }

            fetch(url, {
                method: ((method) ? method : 'GET'),
                body: ((body) ? body : null),
                headers: {
                    'Authorization': ((!authorization) ? authHeader : authorization),
                    'Content-Type': ((!contentType) ? 'application/json' : contentType)
                }
            })
            .then(function (response) {
                if (response.status >= 200 && response.status < 300) {
                    return Promise.resolve(response);
                } else {
                    return Promise.reject(new Error(response.statusText));
                }
            })
            .then(function (response) {
                if (response && response.headers.get("Content-Type") && response.headers.get("Content-Type").indexOf('json') > -1) {
                    return Promise.resolve(response.json());
                } else {
                    if (response)
                        return Promise.resolve(response);
                    else
                        return Promise.reject(new Error('No response content'));
                }
            })
            .then(function (data) {
                resolve(data);
            }).catch(function (error) {
                console.log(error);
                reject(error);
            })
        })
        return promise;
    }
    //... other stuff
 }

I have decided to spice things up by using Fetch API instead of good old XMLHttpRequest, as it provides more powerful and flexible feature set. Currently only Google Chrome supports it natively, so window.fetch polyfill is used to make it work on all modern browsers. This solution would not be complete without another newcomer - javascript promises. Again, a polyfill is needed to make it work across all browser platforms.

So now, instead of having a rather cumbersome Ajax element declaration and a method that makes it work…

<baasic-ajax url="{{baseUrl}}/{{version}}/{{application}}/login"
         id="ajax"
         method="POST"
         params='{"type":"Forms"}'
         handleas="json"
         on-core-response="{{ajaxPostResponse}}"
         on-core-error="{{ajaxPostError}}"></baasic-ajax>
...
logIn: function () {
    this.loginInProgress = true;
    var ajax = this.$.ajax;
    ajax.body = 'grant_type=password&username=' + this.username + '&password=' + this.password;
    ajax.go();
}

… we only declare behaviors that we need, and use Ajax - just one line of code, with all the goodness provided by the Promise library:

behaviors: [Baasic.Ajax, Baasic.Config],
...
Baasic.Ajax.go(this._loginUrl,
                    'POST',
                    'grant_type=password&username=' + this.username + '&password=' + this.password,
                    null,
                    'application/x-www-form-urlencoded')
                    .then(this._postResponse.bind(this))
                    .catch(this._postError.bind(this));

The difference becomes even more pronounced when you need to hit multiple REST endpoints, using different signatures and methods. I’ve been using baasic-crud element for such purposes before, but it has been far from an elegant solution. There is no need for such “utility” elements with this approach.

In a similar manner, I have removed the baasic-globals element in favor of the config behavior. It makes sense to have such configuration properties centralized.

var Baasic = Baasic || {};

Baasic.Config = {
    properties: {
        /**
        * Roles that have administrative privileges in the target application.
        */
        adminRoles: {
            type: Array,
            value: ['Administrators'],
            notify: true
        },
        /**
        * Baasic service base URL.
        */
        baseUrl: {
            type: String,
            value: "https://api.baasic.com",
            notify: true
        },
        /**
        * Baasic version.
        */
        version: {
            type: String,
            value: "beta",
            notify: true
        },
        /**
        * Name of the Baasic applciation.
        */
        application: {
            type: String,
            value: "WebComponents",
            notify: true
        },
        /**
        * Cache expiration for lookups storage in seconds.
        */
        cacheExpiration: {
            type: Number,
            value: 7200,
            notify: true
        }
    }
}

Once this “behavior-based” technique was put to work, the rest of the upgrade process was done by following instructions from the migration guide and looking at the code from the built-in elements.

Tools

The development workflow has changed a bit in this release. More details can be found here. The Polymer team introduced Polyserve, a simple web server that lets you use local Bower components, making testing and development much easier. Install it by typing

npm install polyserve -g

After that, all you have to do is to create a folder for your component and start with the development work. I prefer to use Yeoman as a scaffolding tool to avoid manual creation of the boilerplate code. Its generator-polymer can be used to scaffold Polymer apps or single elements. You can install the generator by

npm install -g generator-polymer

To scaffold a new, reusable Polymer element based on the seed-element workflow, type

yo polymer:seed my-component

That is what we do when developing new components (to scaffold a complete application you would just type yo polymer and get an app based on the Polymer Starter Kit).

You can now run

polyserve

By default, it will listen on port 8080 - that is, http://localhost:8080/components/my-component/demo/. If you want to see the documentation, direct your browser to http://localhost:8080/components/my-component/. For more information on element documentation, check out Polymer style guide.

Polyserve documentation page Polyserve displaying login element documentation

Another nice addition to the Polymer toolset is WCT or web-component-tester. With it, you get a complete browser-based testing environment, configured out of the box with familar tools like Mocha (test framework), Chai (assertion library), Sinon (mocks, stubs, spies, …) and other. WCT will run your tests against whatever browsers you have locally installed. Install it via:

npm install -g web-component-tester

Then, you can run your tests on your local browsers via:

wct
wct -l (chrome will only run tests in chrome)
wct -p (will keep the browsers alive after test runs, refresh to re-run)
wct test/some-file.html (will test only the files you specify)

Note that Firefox on Windows has some issues with WCT due to backslashes in the path - it is easy to resolve using the information for Stack Overflow. If you used Yeoman to scaffold a seed element, you can alternatively navigate to the /test directory of your element to run its tests. If you are using Polyserve, the URL is: http://localhost:8080/components/my-component/test/

Here is a test suite for the baasic-login element. We did not finish all the tests yet, but this and other tests (look at the GitHub repo for other elements) demonstrate the most important aspects of the WCT and accompanying libraries. Note that, as Fetch API is used to access server-side resources, we cannot use Sinon’s fake server (it relies on XMLHttpRequest), so we are using plain stubs. Also note how I’m calling flush after making changes to the element, to ensure that our tests are performing assertions after all observers have resolved and events have fired. In addition to that, some of the callbacks we pass to Mocha have an extra done argument. This tells Mocha that it should treat the callbacks as asynchronous. Unit testing Polymer elements has more information on this and similar topics - although it is a bit outdated.

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
    <script src="../../webcomponentsjs/webcomponents-lite.js"></script>
    <script src="../../web-component-tester/browser.js"></script>
    <script src="../../test-fixture/test-fixture-mocha.js"></script>
    <script src="../../sinonjs/sinon.js"></script>
    <link rel="import" href="../../polymer/polymer.html">
    <link rel="import" href="../../test-fixture/test-fixture.html">

    <link rel="import" href="../../baasic-globals/globals-import.html">
    <link rel="import" href="../baasic-login.html">
</head>
<body>
    <test-fixture id="LoginFixture">
        <template>
            <baasic-login application="WebComponents" base-url="/testurl" version="beta"></baasic-login>
        </template>
    </test-fixture>
    <test-fixture id="ErrorFixture">
        <template>
            <baasic-login application="WebComponents" base-url="/errorurl" version="beta"></baasic-login>
        </template>
    </test-fixture>

    <script>
        var myEl;
        var localuser = {};

        var setItem = function (key, value, options) {
            localuser = value;
        }

        var getItem = function () {
            return localuser;
        }

        var removeItem = function (item) {
            localuser = {};
        }

        describe('<baasic-login>', function () {

            before(function () {
                var postResponse = '{"access_token":"testtoken","token_type":"bearer","expires_in":7200}';
                var getResponse = '{"displayName":"testuser","email":"testuser@baasic.com","permissions":null,"roles":["Administrators"],"userName":"testuser","dateCreated":"0001-01-01T00:00:00","dateUpdated":"0001-01-01T00:00:00","id":"q3Ffk00aSawklADb0HhJi0"}';
                var errorResponse = 'An error has occured';

                var ajaxStub = sinon.stub(Baasic.Ajax, 'go');
                ajaxStub.withArgs('/testurl/beta/WebComponents/login?type=Forms', 'POST', 'grant_type=password&username=testuser&password=testpwd', null, 'application/x-www-form-urlencoded')
                    .returns(Promise.resolve(JSON.parse(postResponse)));

                ajaxStub.withArgs('/testurl/beta/WebComponents/login?type=Forms', 'POST', 'grant_type=password&username=testuser&password=error', null, 'application/x-www-form-urlencoded')
                    .returns(Promise.reject(errorResponse));

                ajaxStub.withArgs('/testurl/beta/WebComponents/login?type=Forms', 'GET')
                    .returns(Promise.resolve(JSON.parse(getResponse)));

                ajaxStub.withArgs('/errorurl/beta/WebComponents/login?type=Forms')
                    .returns(Promise.reject(errorResponse));

                var cacheWriteStub = sinon.stub(Baasic.Cache, 'setItem', setItem);
                var cacheReadStub = sinon.stub(Baasic.Cache, 'getItem', getItem);
                var cacheRemoveStub = sinon.stub(Baasic.Cache, 'removeItem', removeItem);
            });

            beforeEach(function () {
                myEl = fixture('LoginFixture');
                myEl.username = 'testuser';
                myEl.password = 'testpwd';

                localuser = {};
            });

            after(function () {
                Baasic.Ajax.go.restore();
                Baasic.Cache.getItem.restore();
                Baasic.Cache.setItem.restore();
                Baasic.Cache.removeItem.restore();
            });

            describe('starting state', function () {
                it('should have the URL set up', function () {
                    expect(myEl._loginUrl).to.be.equal('/testurl/beta/WebComponents/login?type=Forms');
                });
            });
            describe('login functionality', function () {
                it('should handle a proper login request', function (done) {
                    expect(Baasic.Cache.getItem('baasicUser')).to.be.empty;
                    myEl.logIn();
                    flush(function () {
                        //expectations after successful login.
                        expect(Baasic.Cache.getItem('baasicUser')).not.to.be.null;
                        expect(Baasic.Cache.getItem('baasicUser').accessToken).to.be.equal('testtoken');
                        //subelement visibility
                        expect(myEl.$$('#loginbox').style.display).to.be.equal('none');
                        expect(myEl.$$('#loggedin').style.display).to.be.empty;
                        done();
                    });
                });
                it('should display error message when username and password does not match and reject access', function (done) {
                    myEl.password = 'error';
                    var toast = myEl.$.toastError;
                    expect(toast.hidden).to.be.false;
                    myEl.logIn();
                    flush(function () {
                        expect(Baasic.Cache.getItem('baasicUser').username).to.be.empty;
                        expect(toast.hidden).to.be.false;
                        done();
                    });
                });
            });
            describe('logout functionality', function () {
                it('should clean up after itself and reset the view', function (done) {
                    //run the lifecycle methods
                    myEl.logIn();
                    flush(function () {
                        myEl.logOut();
                        flush(function () {
                            //expectations after successful logout
                            expect(Baasic.Cache.getItem('baasicUser').username).to.be.empty;
                            //subelement visibility
                            expect(myEl.$$('#loginbox').style.display).to.be.empty;
                            expect(myEl.$$('#loggedin').style.display).to.be.equal('none');
                            done();
                        });
                    });
                });
            });
            describe('network error handling functionality', function () {
                var errEl;
                before(function () {
                    errEl = fixture('ErrorFixture');
                    errEl.username = 'testuser';
                    errEl.password = 'testpwd';
                });

                it('should display error message when communication fails', function (done) {
                    var toast = errEl.$.toastError;
                    expect(toast.hidden).to.be.false;
                    errEl.logIn();
                    flush(function () {
                        //show error msg when network error occurs
                        expect(toast.hidden).to.be.false;
                        done();
                    });
                });
            });
        });
    </script>
</body>
</html>

The end result looks rather nice:

Unit testing baasic-login element Unit testing baasic-login element

How to use it

The easiest way to use our demo is to download its source (either by cloning via GIT - git@github.com:Baasic/baasic-demo-polymer.git - or downloading the ZIP). Place the code in a folder of your choice, run bower install to install all dependencies, and run

polyserve -o index.html

Of course, you can use any other web server if you prefer.

If you want to use the individual elements in your existing projects (regardless of the technology), you can install them individually:

bower install --save Baasic/baasic-sdk-polymer-membership-login#^1.0.0
bower install --save Baasic/baasic-sdk-polymer-article-module#^1.0.0
bower install --save Baasic/baasic-sdk-polymer-article-editor#^1.0.0
bower install --save Baasic/baasic-sdk-polymer-core-markdowneditor#^1.0.0
bower install --save Baasic/baasic-sdk-polymer-core-globals#^1.0.0
bower install --save Baasic/baasic-sdk-polymer-core-sessioncontrol#^1.0.0

I hope that you will have as much fun with the Polymer 1.0 as I did. Please do not hesitate to send your questions and comments on this and related topics.

Feel free to leave a comment

comments powered by Disqus