We have talked about the theory behind Web components technology, and stated that one of the main goals of the Baasic project is to enable advanced functionality inside new or existing Web projects in an absurdly simple way. Now it’s time to get our hands dirty and write some code. Even if you don’t have any previous experience with Web components and Polymer, our demo components are simple enough to get an easy grip.

How it looks

Before we start, you might want to get the source code for all components from our GitHub repository - Polymer repo names are prefixed with baasic-sdk-polymer. You can currently use 8 components that together provide simple content management functionality, including article preview and editing, authorization, and session expiration control.

Polymer demo site Polymer demo site home page

Our Polymer demo site currently showcases everything on a single page. It uses a collapsible panel that holds a login component, and a couple of editable content areas placed amidst static HTML content. The anonymous user cannot distinguish between our editor elements and ordinary content, but things change once we are logged in. Of course, you will need to have a live Baasic account to try this out. Open the login panel and enter your username and password.

Polymer login element Polymer login element in action

If everything goes well, you’ll notice that an Edit article button becomes visible below each editable section.

Polymer article editor Edit article button becomes active in the admin mode

Click on it, and use the markdown editor that opens up to enter your content. You can also define roles that are allowed to view and edit each article by clicking on checkboxes next to the role name. Hit Save to store the changes in the Baasic backend storage, and you are done!

Polymer article editor Baasic markdown editor

This is an intentionally simple content management scenario, but if you look at the code, you’ll see that we’ve covered a lot of topics, including basic Polymer component creation techniques, communication with REST-based Web services, cache and local storage support, asynchronous lookup data loading via promises, and much more.

Users that only want to use Baasic Web components in their own projects do not need to know more - they can just grab the existing components and start using them in their own projects.

bower install baasic-sdk-polymer-core-sessioncontrol
bower install baasic-sdk-polymer-membership-login
bower install baasic-sdk-polymer-article-module

See the source code of the demo page to see how these components are used. For those of you who still want to know more, here are some low-level implementation details.

Digging deeper

Let me describe the basic techniques used in demo components. We will go through each of them and identify non-trivial sections.

Baasic-globals

This Web component holds application-wide configuration values, and takes care of async loading of various lookup values, including system roles and actions that each user can take on each article (Create, Read, Update, Delete, whatever) - all of these values are already defined within your Baasic application, but need to be transferred to the client and displayed within different components. Here is how it looks (the declaration and the template of the component are not shown for brevity, please check our GitHub repo for details):

<script>
    (function () {
        /**
        * Global object storing application-wide configration settings.
        * 
        */
        var config = {
            //Add additional admin roles here
            adminRoles: ['Administrators'],
            //base Baasic service URL
            baseUrl: "https://api.baasic.com",
            //Baasic version
            version: "beta",
            //Baasic application
            application: "WebComponents",
            //cache expiration time for lookup storage in seconds
            cacheExpiration: 7200
        };
        Polymer('baasic-globals', {
            ready: function () {
                //assignment of the global singleton variable to the object instance variable per http://stackoverflow.com/questions/24867741/polymer-global-variables that fixes the behavior from http://www.polymer-project.org/docs/polymer/polymer.html#global
                this.config = config;
            },
            get baasicUser() {
                var toReturn = Baasic.Cache.getItem('baasicUser') || {};
                return toReturn;
            },
            get roles() {
                return this.getFromCache(this.$.axRoles, 'baasicRoles', 'item');
            },
            get actions() {
                return this.getFromCache(this.$.axActions, 'baasicActions', 'accessAction');
            },
            getFromCache: function (ajax, cacheKey, collectionKey) {
                var _this = this;
                //Loading of lookup data from the service is implemented via promises, as baasic-ajax implemented a request method that returns a promise to support async loading. 
                //An alternative way would be to use the standard ajax element with xhrArgs object { sync: true}, but that blocks the execution until the response from a remote server is received
                //Browsers that do not support HTML5 Promises (mainly IE at this moment) can use the polyfill from https://github.com/jakearchibald/es6-promise, it is included in the globals-import.html list of imports.
                return new Promise(function (resolve, reject) {
                    if (!Baasic.Cache.getItem(cacheKey)) {
                        ajax.request().then(function (response) {
                            Baasic.Cache.setItem(cacheKey, response.response[collectionKey], {
                                expirationAbsolute: new Date(new Date().getTime() + (_this.config.cacheExpiration * 1000)),
                                expirationSliding: null,
                                priority: Cache.Priority.HIGH,
                                callback: null
                            });
                            resolve({ data: response.response[collectionKey] });
                        }, function (error) {
                            reject({ data: null });
                        }.bind(_this));
                    }
                    else {
                        resolve({ data: Baasic.Cache.getItem(cacheKey) });
                    }
                });
            }
        });
    })();
</script>

As you can see, we are using the standard global variable pattern to provide a central place where all global settings will be stored. This comes handy for API endpoint configuration and similar details, as they are needed in almost every component, and copying them all around the application would be difficult and error-prone.

Apart from this, Baasic-globals component exposes a couple of properties for fetching role and action collections received from the Baasic server. These are lazy-loaded on the first property access and stored inside local storage via simple and elegant jscache library. This is where things start to get interesting - due to the nature of Web component’s lifecycle methods and the need for asynchronous fetching of server-side data, javascript promises were chosen as the best tool for the job. If the collection cannot be retrieved from the local strorage, an Ajax call is triggered to get them directly from the server. Experiences Polymer developers will quickly notice that core-ajax component does not come with support for promises out-of-the box, so let’s take a look on how we solved this problem.

Baasic-ajax

Our Ajax component inherits from core-ajax and takes care of two things: authorization (it adds proper Authorization header to each request) and promise-based functionality which is used by other components, as described above. Simple and powerful stuff - here is an interesting article if you want to learn more about inheritance and composition with Polymer.

<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../baasic-globals/globals-import.html">
<link rel="import" href="../core-ajax/core-ajax.html">

<polymer-element name="baasic-ajax" extends="core-ajax" hidden>
    <template>
        <shadow>
        </shadow>
    </template>
    <script>
        Polymer('baasic-ajax', {
            go: function () {
                var headers = this.headers || {};
                var baasicUser = Baasic.Cache.getItem('baasicUser');
                if (baasicUser && baasicUser.accessToken) {
                    headers['Authorization'] = 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 if (!headers['Authorization']) {
                    headers['Authorization'] = '';
                }
                this.headers = headers;

                return this.super();
            },
            /**
           * The `request` method will return a Promise that will resolve or reject. Author: https://github.com/spirosikmd/ajax-promise/blob/master/ajax-promise.html
           *
           * @method request
           * @return {Promise} Returns a Promise that will either resolve or reject.
           */
            request: function () {
                var _this = this;
                return new Promise(function (resolve, reject) {
                    var xhr = _this.go();
                    xhr.onload = function () {
                        // Resolve could be called even on 404 etc so we should check the status.
                        if (_this.isSuccess(xhr)) {
                            var response = _this.evalResponse(xhr);
                            resolve({ response: response, xhr: xhr });
                        } else {
                            var response = xhr.status + ': ' + xhr.responseText;
                            reject({ response: Error(response), xhr: xhr });
                        }
                    };
                    // Handle network errors
                    xhr.onerror = function () {
                        reject(Error("Network Error"));
                    };
                });
            }
        });
    </script>
</polymer-element>

Baasic-crud

Most of our Web components need to work directly with Baasic REST service endpoints and perform standard CRUD operations. Things can quickly get messy if we start implementing this type of functionality on top of the existing core-ajax or baasic-ajax components. Instead of that, we have packaged most commonly used CRUD endpoint access code within a single component. It uses a separate Ajax component for each operation:

<polymer-element name="baasic-crud" attributes="getUrl postUrl putUrl deleteUrl">
<template>
    <baasic-ajax id="axGet" 
             url="{{getUrl}}"
             handleAs="json"
             contentType="application/json"
             method="GET"
             on-core-response="{{onGetCompleted}}"
             on-core-error="{{onGetError}}"></baasic-ajax>
    <baasic-ajax id="axPost"
             url="{{postUrl}}"
             handleAs="json"
             contentType="application/json"
             method="POST"
             on-core-response="{{onPostCompleted}}"
             on-core-error="{{onPostError}}"></baasic-ajax>
    <baasic-ajax id="axPut"
             url="{{putUrl}}"
             handleAs="json"
             contentType="application/json"
             method="PUT"
             on-core-response="{{onPutCompleted}}"
             on-core-error="{{onPutError}}"></baasic-ajax>
    <baasic-ajax id="axDelete"
             url="{{deleteUrl}}"
             handleAs="json"
             contentType="application/json"
             method="DELETE"
             on-core-response="{{onDeleteCompleted}}"
             on-core-error="{{onDeleteError}}"></baasic-ajax>
</template>

The rest of it is pretty straightforward - we are just passing the parameters and firing off appropriate calls. While this is still no production-quality code, we have frequently discussed if this is the right approach. In my opinion, you should not be worried about using multiple Ajax components (which in turn wrap good old XMLHttpRequest objects) on the same page , as GC will take care of disposing them when they are not needed. These posts on SO seem to concur with this approach - post 1, post 2.

<script>
    Polymer('baasic-crud', {
        getUrl: '',
        postUrl: '',
        putUrl: '',
        deleteUrl: '',
        get: function (params) {
            var ax = this.$.axGet;
            ax.params = params;
            ax.go();
        },
        onGetCompleted: function (e) {
            this.fire('get-completed', { response: e.detail.response });
        },
        onGetError: function (e) {
            this.fire('get-error', { response: e.detail.response });
        },
        post: function (params, body) {
            var ax = this.$.axPost;
            ax.params = params;
            ax.body = body;
            ax.go();
        },
        onPostCompleted: function (e) {
            this.fire('post-completed', { response: e.detail.response });
        },
        onPostError: function (e) {
            this.fire('post-error', { response: e.detail.response });
        },
        put: function (params, body) {
            var ax = this.$.axPut;
            ax.params = params;
            ax.body = body;
            ax.go();
        },
        onPutCompleted: function (e) {
            this.fire('put-completed', { response: e.detail.response });
        },
        onPutError: function (e) {
            this.fire('put-error', { response: e.detail.response });
        },
        delete: function (params) {
            var ax = this.$.axDelete;
            ax.params = params;
            ax.go();
        },
        onDeleteCompleted: function (e) {
            this.fire('delete-completed', { response: e.detail.response });
        },
        onDeleteError: function (e) {
            this.fire('delete-error', { response: e.detail.response });
        }

    });
</script>

Baasic-sessioncontrol

This is the last supporting/infrastructure component we are going to cover today. It tracks the session state on the client side (keeping it in sync with the server), and issues a visible warning when session is going to expire due to long period of inactivity. In addition, it fires a signal to other components, so they can redraw themselves, taking into account the current status of the user. If the user is logged off, the login components needs to show username and password fields; editor component needs to hide the Edit button, etc.

<script>
    Polymer('baasic-sessioncontrol', {
        baseUrl: '',
        version: '',
        application: '',
        interval: 60,
        warningInterval: 300,
        signalSent: false,
        warningDisplayed: false,
        hasBeenloggedIn: false,
        ready: function () {
            this.globals = this.$.globals;
            if (!this.baseUrl) this.baseUrl = this.globals.config.baseUrl;
            if (!this.version) this.version = this.globals.config.version;
            if (!this.application) this.application = this.globals.config.application;
            setInterval(this.checkSession.bind(this), this.interval * 1000);
        },
        checkSession: function (t) {
            var baasicUser = Baasic.Cache.getItem('baasicUser');
            if (baasicUser && baasicUser.expiresAbsolute) {
                this.hasBeenloggedIn = true;
                if (!this.warningDisplayed && (new Date(baasicUser.expiresAbsolute) - (new Date())) < (this.warningInterval * 1000)) {
                    this.warningDisplayed = true;
                    this.$.warningDialog.toggle();
                }
            }
            else {
                //send signal to other elements if user is not logged in anymore
                if (this.hasBeenloggedIn && !this.signalSent) {
                    this.signalSent = true;
                    this.fire('core-signal', { name: 'user-login', data: { baasicUser: {} } });
                }
            }
        },
        refreshSession: function () {
            //call an arbitrary API method to renew the token
            this.warningDisplayed = false;
            this.signalSent = false;
            this.$.axRoles.go();
        }
    });
</script>

That would be all for this time - the second part will cover more complex interactive components that build upon the functionality described here, including login and editor modules. See you soon!

Feel free to leave a comment

comments powered by Disqus