Category Archives: Javascript

Implementing correct modal navigation with react-router

I’ve recently been converting a big project from jquery mobile into React and Material-UI. One area that seems pretty weak compared to other frameworks is with regards to having a proper mobile-focused infrastructure for navigation. One of the big issues for me was that of modals (for example a dialog, alert or popup page components). For example when you see an alert you want to be able to click on the ok button to dismiss it. If you are on android you also expect to be able to press the back button to dismiss. However if you have separate routes for your modals such that the url changes when they are open, if you refresh the page at that point you will have a modal but no previous state to explore. There seem to be certain hacks with react-router-dom which allow you to change the page but keep the URL the same, assuming you are using BrowserRouter, however because this was a legacy project I want to keep on using HashRouter. So, I whipped up a quick HOC hack to wrap around a modal which will allow both the back navigation to work as expected, and also for refreshes to go to the main page rather than opening the modal.

I already had a standard base class which had the handleClose() method to close off a dialog and signal to the parent that it was done, so I expanded it to include a state listener as below

function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

// Shows some sort of dialog which has a handleClose method to close it,
// optionally calling props.onClose and also handles history appropriately
export const DialogMixin = Base => class extends Base {
    displayName = `withDialogMixin(${getDisplayName(Base)})`;

    __close = () => {
        if( window.location.hash == this.__start_hash ) {
            window.removeEventListener('popstate', this.__close);
            this.setState({ closed: true });
            if( this.props.onClose )
                this.props.onClose();
        }
    }

    handleClose = () => window.history.back();

    render() {
        // A component could be mounted but not showing any dialog - only hook
        // the history if actually showed something.
        const render = super.render();
        if( !this.state.closed && !this.__start_hash && render ) {
            this.__start_hash = window.location.hash;
            window.location.hash += '?dialog';

            window.addEventListener('popstate', this.__close);
        }
        return render;
    }
}

You then just wrap your component with this – for example

export const Alert = DialogMixin(_Alert);

This works because react-router-dom doesn’t attempt to parse query strings etc, so when the app is first loaded you want to just have something to remove any of the ?dialog path hacks:

window.location.hash = window.location.hash.replace(/\?.*/, '');

Creating flexible highlight rules in Ace that change according to editor mode

I’ve only recently started using the excellent Ace editor in projects, but I’m really enjoying it so far. It has very flexible and well designed custom highlighting rulesets that developers can extend to various different syntax. However it does not currently support in-editor spell-checking (I believe this functionality is planned for a release soon though – hopefully!).

One of the situations I’m using the editor in is site which has input from many different languages and character sets. As part of this I wanted to ensure that the language that a given article is claimed to be written in matches the character set of various content sections of the editor. For example if the article is in Thai, but you are using Latin script characters it should highlight them as errors. A big issue we have is that there is a lot of Cyrillic content, but as a number of characters render the same or very similarly in Cyrillic and Latin (for example Р and P), some of the users input mostly Cyrillic, but with the occasional character of Latin. This wouldn’t be a problem if we were just rendering the text, but we are also unidecoding it for searching – in this case Latin P goes to p, but the Cyrillic character Р encodes to r (as it is pronounced). This means that it throws off the searching.

I wrote a script which parses the CLDR data’s exemplarCharacters data to get the expected character sets for a language, adds a few in (as the CLDR is not totally complete unfortunately, especially for languages that use extended Cyrillic sets such as Karakalpak, lacks Traditional Mongolian entirely, and is incomplete for some ideographic languages such as Chinese). It then adds some general punctuation and other characters and generates a javascript regexp for matching characters that should not exist. For example for Armenian, the regexp is /[^\u0020-\u0022\u0025-\u0029\u002b-\u003b\u003f\u005b-\u005d\u00ab\u00b4\u00b8\u00bb\u0531-\u0556\u055a-\u055f\u0561-\u0587\u058a\u2030]/.

So far so good, but how to integrate this with Ace editor? Usually you only have a mode for Ace which specifies the language (eg PHP, HTML), but I don’t want to create a new mode for each language/script that this app wants to support.

Initially I tried basing some code on this code which adds spell checking outside of Ace, however there are a number of limitations with this approach namely it doesn’t integrate with the existing highlighting system so if you are mean to be editing eg HTML document containing only Thai characters it doesn’t know which parts are HTML and which parts should be checked, without redoing the whole highlighting run a second time. It is also needs to reprocess the entire document every change.

Digging around in the Ace source I found that you can actually pass an object into the setMode function which enables you to pass new parameters, such as the invalid-characters regexp for the current language. However the highlighting functions are usually static which means it is complex to update this on-the-fly, especially after normalizing it for something based on the Text Highlight Rules. My solution is as follows:

define('ace/mode/my_highlight_rules', ... {
    var MyHighlightRules = function(regex) {
        var rules = function () {
            this.$rules = {
                start: [ ... ],
                words: [ ... something with regex ... ],
                ...
            this.normalizeRules();
        };

        rules.metaData = { ... };

        oop.inherits(rules, TextHighlightRules);
        return rules;
    };

    exports.MyHighlightRules = MyHighlightRules;
});

define("ace/mode/my_mode", ... {
    var Mode = function (opts) {
        this.HighlightRules = MyHighlightRules(opts.regex);
        ...
    };
});

You can then just do .setMode({ path: 'ace/mode/my_mode', regex: /.../ }) changing the regex for each different language or character set that you wish to validate.

Awesome Angular 4 form validator routine

One of the things I like the most about Angular is the ability to make even complex forms relatively simple. In Angular 1 I had a library of form helpers that I wrote, the basic idea was initially show a blank form, when user fills in (dirties) an entry it should do validation. When the user click on the submit button it should do validation of all items on the form, submit the form if no errors and show any errors if there were any. This sounds simple enough but in reality it was a few hundred lines of code to do it correctly.

Angular 2+ (Angular 4 on Ionic 3 in this case) make life a lot easier, especially once you’ve got to terms with the FormBuilder framework. To force whole-form validation (including any subforms) simply create a module called form-tools.ts looking like:

// Force a form to show any invalid entries and return if form is valid or not. Recurse through any subforms.
export const validateForm = (form) => {
    Object.keys( form.controls ).forEach( control_name => {
        let control = form.controls[control_name];

        control.markAsTouched();
        if( 'controls' in control )
            validateForm( control );
    });

    return form.valid;
};

Then you can easily use it from another class as follows:

import { FormBuilder } from '@angular/forms';
import { validateForm } from '../../form-tools';

class ...Page {
    constructor(private fb : FormBuilder ) {
        this.userForm = this.fb.group({
            form elements...
        });
    }

    submit_form() {
        if( !validateForm(this.userForm) )
            return;
        send the form request to the server(this.userForm.value)
    }
}

Element scrolling within Angular (1) pages

Back to Angular 1 for today’s post as I’ve been doing some work on an older project today. As we all know, in the old days of the web, you could scroll to items within the page by using an <a> element with a hash reference, like:

<a href="#thing-to-scroll-to">scroll here</a>

<h2 id="thing-to-scroll-to">My title</h2>

With the advent of single-page sites and multiple ajax pages under them however, the hash section of a query parameter is increasingly unable to be used. On Angular 1 with angular-route this becomes impossible.

So what if for example you want to have some references at the top of a page within an Angular application which will scroll the user down to certain sections below, just like the hash references used to do? There are a number of suggestions on the internet for this but they are not very reusable, so I decided to create a simple directive that does this:

app.directive('clickScrollTo', function () {
    return {
        restrict: 'A',
        scope: {
            clickScrollTo: '@'
        },
        link: function (scope, el, attr) {
            el.click(function(event) {
                var main_page = $('.main-page');
                event.preventDefault();
                main_page
                    .stop()
                    .animate(
                        { scrollTop: main_page.scrollTop() + $(scope.clickScrollTo).offset().top },
                        500,
                        'linear'
                    );
            });
        }
    }
});

This is slightly complicated by the fact that I am using an element with class="main-page" with overflow: auto set to contain the scrollable section of the app. If you don’t have a layout like this just replace the $('.main-page') part with $('body').

Then you can easily create elements like:

<span class="a" click-scroll-to="#more-benefits">click here</span>
...
<div id="more-benefits">...</div>

Ionic 3 – Allowing back button action to be specified on a page-by-page basis

Ionic 3 is great as I’ve said in previous posts, however there are a few issues with its out-of-the box handling of back button on Android. Normally it just goes back to the previous page which is usually what you want, however this is not what you want when you are in a popup or modal on the page, or in some other cases you may want to capture the back event and do something else with it on a page-by-page basis. Fortunately it’s pretty straight forward to override this as I discovered.

Firstly, we want to override the back button action for the whole app. Open up app/app.component.ts:
and in the constructor:

import { App, Platform } from 'ionic-angular';
...
  constructor( ..., private app: App, private platform : Platform ) {
    platform.registerBackButtonAction(() => {
        let nav = app.getActiveNavs()[0];
        let activeView = nav.getActive();

        if(activeView != null){
          if(nav.canGoBack())
            nav.pop();
          else if (typeof activeView.instance.backButtonAction === 'function')
            activeView.instance.backButtonAction();
          else
            nav.parent.select(0); // goes to the first tab
        }
      });
  }

This basically defaults to going back if it is possible to, if not then it will take you to the first tab if it is a tab view. However if your active page has a backButtonAction() function, it will delegate to that.

So for example in a modal class you can add something like:

import { ViewController } from 'ionic-angular';
...
    constructor( private viewCtrl : ViewController ) {}

    backButtonAction() {
        this.viewCtrl.dismiss();
    }

which will dismiss the modal and go back to the page that called it, rather than the default action of simply going back a page.

CKEditor Plugin Validation for Multiple Requirements

I’ve been working on a few CKEditor plugins recently for some projects. When building the elements of a dialog you create a data structure like:

elements: [{
    type: 'text',
    id: 'book',
    label: 'Book',
    validate: CKEDITOR.dialog.validate.notEmpty( "Book field cannot be empty" ),
}]

This is great, but what if you want multiple validation criteria for example it must be a number and not empty? There are a handful of hacks and examples of this online, but looking through the code I found an undocumented (and otherwise unused) function which enables you to combine multiple validation functions:

validate: CKEDITOR.dialog.validate.functions(
        CKEDITOR.dialog.validate.notEmpty(),
        CKEDITOR.dialog.validate.integer(),
        "(Start) chapter must be a number and is required",
    ),

You can pass as many validation functions into this as you want and it will combine them, but only allows you to specify one validation message unfortunately.

Angular 4 API service with automatic retries and Ionic 3 integration

In the bad old days of the web, you’d submit a form and if there was a problem with your internet connection it would loose the form and display an error page in the browser. These days you don’t need to worry about this quite so much, but handling errors with sending AJAX form-submits or other API requests is still a difficult topic. Fortunately, the way that Angular 4 uses Observables makes retrying requests quite a bit easier.

In the app I was building for a client recently, we wanted the default process flow to be as follows. Any API request should display a spinner (via Ionic 3), and send the request to the server. If we got an error like login failure then it should return this error to the client. If the error is with the network connection timing out it should automatically retry a couple of times. For other errors such as internal server (ie API side) or not connected at all, it should fail straight away. However if it was an API or network connection failure, it should display a popup prompting the user to opt to retry or cancel the request (eg ‘Turn your internet connection on and hit retry’) rather than making them hit a form resubmit button again.

As Observables remember all the data and options they were submitted with, it’s pretty easy to retry the request and there are a number of bits of code on the internet for this. However I couldn’t find any good examples of this being written in a reusable fashion, and with options of asking prompting the user without forgetting the request. So, here is an example of how you can do this within the framework of Ionic, however it should work in general for anything based on Observables especially under Angular 2+. Below I’ll walk through some of the harder parts of this code.

Create the API service (app/api.service.ts) looking like:

import { Injectable } from '@angular/core';
import { Http, Headers, Response } from '@angular/http';

import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/scan';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/retryWhen';
import 'rxjs/add/operator/finally';
import 'rxjs/add/operator/delayWhen';
import 'rxjs/add/operator/timeout';
import 'rxjs/add/observable/throw';
import 'rxjs/add/observable/onErrorResumeNext';
 
@Injectable()
export class APIService {
  public inprogress_requests : Subject<any> = new Subject();

  public error_handler : (message :string, err:any) => Observable<any> = (err) => Observable.throw(err);

  private requests_active :number = 0;

  constructor (private http: Http) {}
 
  request(path: String, data = {}, options :any = {}): Observable<any> {
    if( !options.headers )
        options.headers = new Headers();
    options.headers.append('Content-Type', 'application/json');

    let base_url = this.config.baseApiUrl();

    let timeout = options.timeout || 10000;
    let max_retries = 'retries' in options ? options.retries : 3;

    let url = `${base_url}/api/${path}`;
    let request = this.http.post(
            url, JSON.stringify(data),
            {
                headers: options.headers,
            }
        )

        // Add a timeout and retry the request after specified time and 3
        // attempts (but only if it is a timeout error)
        .timeout(timeout)
        .retryWhen((errors) =>
            errors.scan( ( errorCount, err ) => {
                if( errorCount < max_retries && err.name == 'TimeoutError' )
                    return errors.delay(500);
                throw err;
            }, 0)
        );

    if( options.interceptor )
        request = request.do( options.interceptor );

    request = request.map(this.extractData);

    // User-visible error handler now
    if( options.auto_fail )
        request = request.onErrorResumeNext();  // enable mergeMap etc to keep working
    else
        request = request.retryWhen( (errors) =>
            errors.delayWhen( (error) => {
                let message = this.log_error(error);
                console.error( `URL was: ${url}, request body: ${stringified_data}` );
                return this.error_handler(message, error);
            })
        );

    if( !options.nonblocking ) {
        this.add_blocking_request( options.loading_msg ? { reason: options.loading_msg } : {} );
        request = request.finally( () => this.finish_blocking_request() );
    }

    return request;
  }

  private add_blocking_request(details :any = {}) {
    // Re-issue a request if the details have been updated
    if( this.requests_active++ == 0 || Object.keys(details).length ) {
        details.active = true;
        this.inprogress_requests.next( details );
    }
  }

  private finish_blocking_request() {
    if( --this.requests_active == 0 )
        this.inprogress_requests.next( { active: false } );
  }

  private extractData(res: Response) {
    // Decode errors will be handled automatically by Observable
    let body = res.json();
    return body || {};
  }

  private log_error(error: Response | any) {
    let errMsg: string;
    if (error instanceof Response) {

      // Ignore any decode errors
      let body :any = {};
      try {
        body = error.json() || {};
      } catch(e) {}

      const err = body.error || JSON.stringify(body);
      errMsg = `${error.status} - ${error.statusText || ''} ${err}`;

      // No internet, probably
      if( error.status == 0 ) {
        console.error(errMsg);
        errMsg = 'Your internet connection is offline. Please connect and hit retry';
      }
    } else {
      errMsg = error.message ? error.message : error.toString();
    }
    console.error(errMsg);
    return errMsg;
  }
}

Lets walk through some potentially confusing bits of this service.

The main request observable is the request variable, we perform actions on this (saving the result in the request variable again) as the user requests, Initially we just set the request to have a timeout (several multiples of time of the maximum time you expect the API to respond in, otherwise you may get multiple resubmissions of the same request if the API gets a bit laggy).

Then, we come to this piece of code:

        .retryWhen((errors) =>
            errors.scan( ( errorCount, err ) => {
                if( errorCount < max_retries && err.name == 'TimeoutError' )
                    return errors.delay(500);
                throw err;
            }, 0)
        );

This basically keeps a log of all the errors that occurred and each time there is an error with the request, it first checks to see how many times we already retried, and ensure that it was actually a timeout error (as opposed to an internal server error or so). If that was the case then it waits 500ms and retries, otherwise it re-throws the error which will cause the Observable to continue as an error response.

    if( options.auto_fail )
        request = request.onErrorResumeNext();  // enable mergeMap etc to keep working

If the user passes an auto_fail option to the request, we want the request to happily silently fail (perhaps we are just sending some usage stats to the server and we don’t want errors popping up about them). This basically returns a successful Observable whether or not it was actually a success so that it doesn’t short-circuit anything due to an error being raised.

However, under normal circumstances we want to raise a frontend error:

        request = request.retryWhen( (errors) =>
            errors.delayWhen( (error) => {
                let message = this.log_error(error);
                console.error( `URL was: ${url}, request body: ${stringified_data}` );
                return this.error_handler(message, error);
            })
        );

This code says to shell out to an external function (the error_handler function reference which can be set somewhere in the main code that builds the API) with the error, and expects it to return an item such as a Subject or a true/false value indicating whether the whole of the above work should be retried again or not. This is a bit messy – you should perhaps have multiple different instances of API depending on whether you want this functionality or not, but because the API is a global service and we want a standard piece of retry code I thought to put it like this. However because it needs to interact with the frontend, I set this elsewhere as I’ll show in a bit.

Finally, we want to wrapper most requests with some code to display a spinner (optionally with a message), unless it is a non-blocking request:

    if( !options.nonblocking ) {
        this.add_blocking_request( options.loading_msg ? { reason: options.loading_msg } : {} );
        request = request.finally( () => this.finish_blocking_request() );
    }

The add_blocking_request and finish_blocking_request issue an Observable message (via this.inprogress_requests) when there are requests active or when the last active request finishes, which avoids having the spinner popping on and off again every time a request is redone or a sub-request is triggered.

Finally, in the main app constructor we hook into these two Observables to do the UI-facing work (app/app.component.ts in ionic – this is ionic-specific but you should be able to replace with your own framework easily enough). Firstly, the spinner:

    // Loader needs creating each time it is displayed in ionic...
    let loader; 
    api.inprogress_requests.subscribe(
        details => {
            if( loader )
                loader.dismiss();
            loader = null;  
                            
            if( details.active ) {
                let loader_options :any = {};
                if( details.reason )
                    loader_options.content = details.reason;
                loader = loadingCtrl.create(loader_options);
                loader.present();
            }           
        }           
    );

Simple enough – if there is a loader get rid of it, and if there should be one then create it with the message. This enable us to update the message displayed easily enough although I’ve not really used this functionality much in the code I’ve written.

Finally, lets look at the dialogs presented to the user to prompt retries. This handler should be simple enough providing different dialogs and messages depending on what the error was exactly. Note that we are returning a Subject which we effectively use like a Promise to handle the asynchronous nature of user interaction with the dialog:

    // Handle errors with a popup and offer retry functionality
    api.error_handler =
        (message, error) => {
            // Just in case it is the first request..
            this.hide_splashscreen();

            let retry_subject = new Subject();
            let retry;

            // Unauthorized
            if( error.status == 401 ) {
                retry = alertCtrl.create({
                    title: 'Logged Out',
                    message: `You have been logged out and need to log in again`,
                    buttons: [
                        {
                            text: 'OK',
                            handler: () => {
                                retry.dismiss().then( () => this.navCtrl.push('login') );
                                retry_subject.error( error );
                                retry_subject.complete();

                                return false;
                            }
                        },
                    ]
                });
            } else {
                let title = 'Server Error';
                let display_message = `We got an error from the remote server: ${message}. Do you want to retry?`;

                // 400's are nicer errors - not a server code issue but a user input problem most likely
                if( error.status == 400 || error.status == 0 ) {    // 0 = no internet
                    display_message = `${message}. Do you want to retry?`;
                    title = "Error";
                }
                retry = alertCtrl.create({
                    title,
                    message: display_message,
                    buttons: [
                        {
                            text: 'No',
                            handler: () => {
                                retry.dismiss();
                                retry_subject.error( error );
                                retry_subject.complete();
                                return false;
                            }
                        },
                        {
                            text: 'Retry',
                            handler: () => {
                                retry.dismiss();
                                retry_subject.next( 1 );
                                retry_subject.complete();
                                return false;
                            }
                        }
                    ]
                });
            }
            retry.present();
            return retry_subject;
        };

Prompt before opening an external link in AngularJS

On a recent project of creating an Angular app which would be both a website and a cordova-packaged app, we had a number of links which opened to external websites (terms and conditions, links to some process flows which couldn’t be contained within the app, etc). However because some of the branding on the sites was very similar to the app itself some test users were getting confused about whether they were still in the app, or had been redirected into a browser.

Because of these issues the client wanted us to create a small popup for some external links that would prompt the user to see if they wanted to move off the site/app. Below is a small angular directive that does this. Usage like:

<a href="https://..." target=_blank prompt-before-open="Do you want to go to an external web page?">...</a>

app.directive('promptBeforeOpen', function($window) {
    return {
        link: function(scope, elem, attr) {
            $(elem).click(function(event) {
                if( !$window.confirm( attr.promptBeforeOpen ) )
                    event.preventDefault();
            });                      
        }                            
    };                               
});

Angular Smart Table and drop-down select

The reason I wrote my previous post explaining my difficulties with selects was because I am currently using the excellent Smart Table angular module to bring some interactivity to tables in a project I’m working on. Smart Table seems to just work although I still find the st-table and st-safe-src attribute requirements a little strange.

One issue I have found is with filtering. The following does not filter properly (doesn’t filter anything), even though the option values seem to be set correctly:

<select st-search="field" st-input-event="change" st-delay=0 ng-model="dummy" class="form-control input-sm" ng-options="item.field as item.field for item in items track by item.field">
                    </select>

Instead, you have to write out the select using an ng-repeat which seems to do the trick nicely:

                    <select st-search="field" st-input-event="change" st-delay=0 ng-model="dummy" class="form-control input-sm">
                        <option value="">
                            All items
                        </option>
                        <option ng-repeat="item in items" value="{{ item.field }}">
                            {{ item.field }}
                        </option>
                    </select>

<select element problem in AngularJS

So I just spent 10 minutes trying to figure out why the following wasn’t working in AngularJS:

<select ng-options="item.name for item in items">
</select>

I spent ages checking my syntax and that the data was correct. Finally I discovered the reason – <select REQUIRES ng-model.

<select ng-model="dummy" ng-options="item.name for item in items">
</select>

works just fine…