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

Related Articles

babylonjs
Integrating BabylonJS 3D engine into an Angular business application - Part 2 - Performance optimization
Welcome to the second part of this blog post series. If you want to learn how to basically integrate BabylonJS into Angular (or vice versa) please take a look at part 1. In this post, our goal is to make your application and the interactions fast! What to expect in part 2 A…
Max Schulte
pwa
Advanced Progressive Web Apps - Push notifications under control - Part 1: Notifications API
For sure, Progressive Web Apps (PWA) are one of the hottest topics on the web today. A PWA should feel like a real app, including the ability to show push notifications. In this blog series, you will learn how to implement push messaging in your PWA or website using the Push API…
Christian Liebel
babylonjs
Integrating BabylonJS 3D engine into an Angular business application - Part 1 - Integration basics
In this two parts article, I will demonstrate how easy it can be to integrate a 3D engine like BabylonJS into a business application built with a SPA framework like Angular. You will see how those two frameworks can interact with each other and where possible performance pitfalls…
Max Schulte
blazor
Re-Using Angular components in a Blazor WebAssembly application using Angular Elements - Web Components custom elements, FTW!
Setting the scene - Blazor integrations Microsoft's Blazor WebAssembly toolkit is one of the new kids on the block in WebAssembly land. For many developers WebAssembly (or WASM) is the origin for a new revolution in the web. We can finally(?) use other languages and frameworks…
Christian Weyer