4
votes

What I want

<div amazingattr.bind="foo">
    ${$someValueFromAmazingattr}
</div>

Just like how this works:

<div repeat.for="bar of bars">
    ${$index}
</div>

Where I got stuck

import {customAttribute} from "aurelia-framework";

@customAttribute("amazingattr")
export class AmazingattrCustomAttribute {
    bind(binding, overrideContext) {
        this.binding = binding;
    }

    valueChanged(newValue) {
        this.binding.$someValueFromAmazingattr = newValue;
    }
}

While this works, the $someValueFromAmazingattr is shared outside the custom attribute's element, so this doesn't work:

<div amazingattr.bind="foo">
    Foo: ${$someValueFromAmazingattr}
</div>
<div amazingattr.bind="bar">
    Bar: ${$someValueFromAmazingattr}
</div>

Both of the "Foo:" and the "Bar:" show the same last modified value, so either foo or bar changes, both binding change to that value.

Why I need this?

I'm working on a value animator, so while I cannot write this (because value converters cannot work this way):

${foo | animate:500 | numberFormat: "0.0"}

I could write something like this:

<template value-animator="value:foo;duration:500">
    ${$animatedValue | numberFormat: "0.0"}
</template>

I imagine I need to instruct aurelia to create a new binding context for the custom attribute, but I cannot find a way to do this. I looked into the repeat.for's implementation but that is so complicated, that I could figure it out. (also differs in that is creates multiple views, which I don't need)

1
Would this sytax fit you? ${isBusy | animate: {format: "0.0", duration: 500}} - kabaehr
@kabaehr: The problem with that, is that it uses value converters, thus you cannot do async and multiple update calls. Also, the animate shouldn't really include number formatting or such as it isn't its job. Formatting numbers should be handled outside of the animation. - Zenorbi

1 Answers

1
votes

After many many hours of searching, I came accross aurelia's with custom element and sort of reverse engineered the solution.

Disclaimer: This works, but I don't know if this is the correct way to do it. I did test this solution within embedded views (if.bind), did include parent properties, wrote parent properties, all seem to work, however some other binding solution also seem to work.

import {
    BoundViewFactory,
    ViewSlot,
    customAttribute,
    templateController,
    createOverrideContext,
    inject
} from "aurelia-framework";

@customAttribute("amazingattr")
@templateController //This instructs aurelia to give us control over the template inside this element
@inject(BoundViewFactory, ViewSlot) //Get the viewFactory for the underlying view and our viewSlot
export class AmazingattrCustomAttribute {
    constructor(boundViewFactory, viewSlot) {
        this.boundViewFactory = boundViewFactory;
        this.viewSlot = viewSlot;
    }
    bind(binding, overrideContext) {
        const myBindingContext = {
            $someValueFromAmazingattr: this.value //Initial value
        };

        this.overrideContext = createOverrideContext(myBindingContext, overrideContext);

        //Create our view, bind it to our new binding context and add it back to the DOM by using the viewSlot.
        this.view = this.boundViewFactory.create();
        this.view.bind(this.overrideContext.bindingContext, overrideContext);
        this.viewSlot.add(this.view);
    }

    unbind() {
        this.view.unbind(); //Cleanup
    }

    valueChanged(newValue) {
        //`this.overrideContext.bindingContext` is the `myBindingContext` created at bind().
        this.overrideContext.bindingContext.$someValueFromAmazingattr = newValue;
    }
}