3

In Aurelia, let's say I have a string containing an interpolation expression "Today at ${value | date: 'time'}" and some object representing the binding context for this { value: new Date() }. Is there any way to, outside of a view, just take that string and that object, and get the resulting formatted string, i.e. "Today at 13:44"?

I looked at the tests, but they all involve creating an HTML element, binding, and then unbinding - I'm wondering what the performance overhead of all that might be, and whether there is an easier way to achieve this? It would be really awesome if there was a light-weight way to just evaluate such a string against a context object, without setting up and tearing down bindings, etc.

Jeremy Danyow
  • 26,470
  • 12
  • 87
  • 133

2 Answers2

4

Here's an example: https://gist.run?id=a12470f6e9f7e6a605b3dd002033fdc7

expression-evaluator.js

import {inject} from 'aurelia-dependency-injection';
import {ViewResources} from 'aurelia-templating';
import {Parser, createOverrideContext} from 'aurelia-binding';

@inject(Parser, ViewResources)
export class ExpressionEvaluator {
  constructor(parser, resources) {
    this.parser = parser;
    this.lookupFunctions = resources.lookupFunctions;
  }

  evaluate(expressionText, bindingContext) {
    const expression = this.parser.parse(expressionText);
    const scope = {
      bindingContext,
      overrideContext: createOverrideContext(bindingContext)
    };
    return expression.evaluate(scope, this.lookupFunctions);
  }
}

app.js

import {inject} from 'aurelia-dependency-injection';
import {ExpressionEvaluator} from './expression-evaluator';

@inject(ExpressionEvaluator)
export class App {
  message = 'Hello World!';

  constructor(evaluator) {
    this.message = evaluator.evaluate('foo.bar.baz | test', { foo: { bar: { baz: 'it works' } } });
  }
}

Edit

I missed the fact that you need to parse an interpolation expression, not a regular binding expression...

There's an example of this in aurelia-validation: https://github.com/aurelia/validation/blob/master/src/implementation/validation-message-parser.ts

Jeremy Danyow
  • 26,470
  • 12
  • 87
  • 133
  • Thanks Jeremy, this was a great help! We initially tried to construct a single expression, similar to how it's done in aurelia-validation, but we ran into some trouble with that - turns out that `Binary` and `Conditional` do not pass along the lookup functions when they are evaluated, which means that it they cannot be used with value converters. I'm not sure if this is a bug or intentional, but it caught us by surprise - luckily though, we could get by with a simpler solution, posted here for future reference :-) – Thomas Darling Dec 23 '16 at 15:35
  • I'll open a bug for this- does seem to limit our ability to compose arbitrary expressions – Jeremy Danyow Dec 23 '16 at 15:54
1

Thanks to Jeremy's answer, this became our final solution:

import { autoinject, BindingLanguage, Expression, ViewResources, createOverrideContext } from "aurelia-framework";

// Represents the sequence of static and dynamic parts resulting from parsing a text.
type TemplateStringParts = (Expression | string)[];

// Cache containing parsed template string parts.
const cache = new Map<string, TemplateStringParts>();

/**
 * Represents an interpolation expression, such as "The price is ${price | currency}",
 * which when evaluated against a binding context results in a formatted string,
 * such as "The price is 42 USD".
 */
@autoinject
export class TemplateString
{
    private text: string;
    private viewResources: ViewResources;
    private bindingLanguage: BindingLanguage;

    /**
     * Creates a new instance of the TemplateString type.
     * @param text The text representing the interpolation expression.
     * @param viewResources The view resources to use when evaluating the interpolation expression.
     * @param bindingLanguage The BindingLanguage instance.
     */
    public constructor(text: string, viewResources: ViewResources, bindingLanguage: BindingLanguage)
    {
        this.text = text;
        this.viewResources = viewResources;
        this.bindingLanguage = bindingLanguage;
    }

    /**
     * Evaluates the interpolation expression against the specified context.
     * @param bindingContext The context against which expressions should be evaluated.
     * @param overrideContext The override context against which expressions should be evaluated.
     * @returns The string resulting from evaluating the interpolation expression.
     */
    public evaluate(bindingContext?: any, overrideContext?: any): string
    {
        let parts = cache.get(this.text);

        if (parts == null)
        {
            parts = (this.bindingLanguage as any).parseInterpolation(null, this.text) || [this.text];
            cache.set(this.text, parts);
        }

        const scope =
        {
            bindingContext: bindingContext || {},
            overrideContext: overrideContext || createOverrideContext(bindingContext)
        };

        const lookupFunctions = (this.viewResources as any).lookupFunctions;

        return parts.map(e => e instanceof Expression ? e.evaluate(scope, lookupFunctions) : e).join("");
    }

    /**
     * Gets the string representation of this template string.
     * @returns The string from which the template string was created.
     */
    public toString(): string
    {
        return this.text;
    }
}

/**
 * Represents a parser that parses strings representing interpolation expressions,
 * such as "The price is ${price | currency}".
 */
@autoinject
export class TemplateStringParser
{
    private resources: ViewResources;
    private bindingLanguage: BindingLanguage;

    /**
     * Creates a new instance of the TemplateStringParser type.
     * @param resources The view resources to use when evaluating expressions.
     * @param bindingLanguage The BindingLanguage instance.
     */
    public constructor(resources: ViewResources, bindingLanguage: BindingLanguage)
    {
        this.resources = resources;
        this.bindingLanguage = bindingLanguage;
    }

    /**
     * Parses the specified text as an interpolation expression.
     * @param text The text representing the interpolation expression.
     * @returns A TemplateString instance representing the interpolation expression.
     */
    public parse(text: string): TemplateString
    {
        return new TemplateString(text, this.resources, this.bindingLanguage);
    }
}

To use this in a view model, simply inject the TemplateStringParser and use it to create a TemplateString instance, which may then be evaluated against a context object.