.NET Core: Lowering the log level of 3rd party components
Entity Framework: High performance querying trick using SqlBulkCopy and temp tables

Mimicking $interpolate: An Angular 2 interpolation service

In an Angular 1 application we have been creating for one of our customers we used the $interpolate service to build a simple templating engine. The user was able to create snippets with placeholders within the web application to use these message fragments to compose an email to reply to a support request.

In Angular 2 there is no such service like $interpolate - but that is not a problem because we have got abstract syntax tree (AST) parsers to build our own interpolation library. Let’s build a component that takes a format string (with placeholders) and an object with properties to be used for replacement of the placeholders. The usage looks like this:

// returns ‘Hello World!’
interpolation.interpolate(‘Hello {{place.holder}}!’, { place: { holder: ‘World!’}});

At first we need to inject the parser from Angular 2 and we need to create a lookup to cache our interpolations.

constructor(parser: Parser) {
    this._parser = parser;
    this._textInterpolations = new Map<string, TextInterpolation>();
}

The class TextInterpolation is just a container for saving the parts of a format string. To get the interpolated string we need to call the function interpolate. The example from above will have 2 parts:

  • String 'Hello '
  • Property getter for {{place.holder}}

 

class TextInterpolation {
    private _interpolationFunctions: ((ctx: any)=>any)[];

    constructor(parts: ((ctx: any) => any)[]) {
        this._interpolationFunctions = parts;
    }

    public interpolate(ctx: any): string {
        return this._interpolationFunctions.map(f => f(ctx)).join('');
    }
}

Before we can create our TextInterpolation we need to parse the format string to get an AST.

let ast = this._parser.parseInterpolation(text, null);

if (!ast) {
    return null;
}

if (ast.ast instanceof Interpolation) {
    textInterpolation = this.buildTextInterpolation( ast.ast);
} else {
    throw new Error(`The provided text is not a valid interpolation. Provided type ${ast.ast.constructor && ast.ast.constructor['name']}`);
}

The AST of type Interpolation has 2 collections, one with strings and the other with expressions. Our interpolation service should support property-accessors only, i.e. no method calls or other operators.

private buildTextInterpolation(interpolation: Interpolation): TextInterpolation {
    let parts: ((ctx: any) => any)[] = [];

    for (let i = 0; i < interpolation.strings.length; i++) {
        let string = interpolation.strings[i];

        if (string.length > 0) {
            parts.push(ctx => string);
        }

        if (i < interpolation.expressions.length) {
            let exp = interpolation.expressions[i];

            if (exp instanceof PropertyRead) {
                var getter = this.buildPropertyGetter(exp);
                parts.push(this.addValueFormatter(getter));
            } else {
                throw new Error(`Expression of type ${exp.constructor && exp.constructor.name1} is not supported.`);
            }
        }
    }

    return new TextInterpolation(parts);
};

The strings don’t need any special handling but the property getters do. The first part of the special handling happens in the method buildPropertyGetter that fetches the value of the property (and the sub property) of an object.

private buildPropertyGetter(exp: PropertyRead): ((ctx: any) => any) {
    var getter: ((ctx: any) => any);

    if (exp.receiver instanceof PropertyRead) {
        getter = this.buildPropertyGetter(exp.receiver);
    } else if (!(exp.receiver instanceof ImplicitReceiver)) {
        throw new Error(`Expression of type ${exp.receiver.constructor && (exp.receiver).constructor.name} is not supported.`);
    }

    if (getter) {
        let innerGetter = getter;
        getter = ctx => {
            ctx = innerGetter(ctx);
            return ctx && exp.getter(ctx);
        };
    } else {
        getter = <(ctx: any)=>any>exp.getter;
    }

    return ctx => ctx && getter(ctx);
}

The second part of the special handling is done in addValueFormatter that returns an empty string when the value returned by the property getter is null or undefined because these values are not formatted to an empty string but to strings 'null' and 'undefined', respectively.

private addValueFormatter(getter: ((ctx: any) => any)): ((ctx: any) => any) {
    return ctx => {
        var value = getter(ctx);

        if (value === null || _.isUndefined(value)) {
            value = '';
        }

        return value;
    }
}

The interpolation service including unit tests can be found on GitHub: angular2-interpolation

Comments

Feed You can follow this conversation by subscribing to the comment feed for this post.

Verify your Comment

Previewing your Comment

This is only a preview. Your comment has not yet been posted.

Working...
Your comment could not be posted. Error type:
Your comment has been posted. Post another comment

The letters and numbers you entered did not match the image. Please try again.

As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

Having trouble reading this image? View an alternate.

Working...

Post a comment

Your Information

(Name and email address are required. Email address will not be displayed with the comment.)