Any time I forget something more than once, I should probably blog about it. Saves me time looking for a comment on SO I put on an answer I particularly appreciated.

Because I've forgotten this particular something more than once, let's return to our time machine, travel back to AngularJS land again, and leave some notes.


What's an "ampersand binding" in AngularJS?

Essentially, an ampersand binding allows you to mark that you're passing an event handler function to a custom component.

Here's one mention in the AngularJS docs:

We saw earlier how to useย =attrย in theย scopeย option, but in the above example, we're usingย &attrย instead. Theย &ย binding allows a directive to trigger evaluation of an expression in the context of the original scope, at a specific time. Any legal expression is allowed, including an expression which contains a function call. Because of this,ย &ย bindings are ideal for binding callback functions to directive behaviors.

(There may be a better reference in the docs, but man it's hard to google an ampersand. They could have also ensured something that made it easier to find, like if it had a clear name similar to "ampersand component binding", but the AngularJS folk weren't nice enough to do that either afacit.)

Here's some example code [of mine :shudder:] that uses it:

angular.module('app').component('qbTristateCheckbox', {
    template:
        '<input ' +
        'ng-class="tsc.classes" ' +
        'ng-disabled="tsc.isDisabled()" ' +
        'ng-click="tsc.wasChecked($event)" ' +
        'ng-attr-id="{{tsc.checkboxId}}" ' +
        'type="checkbox" />',
        
    controller: QbTristateCheckboxController,
    controllerAs: 'tsc',
    
    bindings: {
        labelDisplay: '@',

        checkState: '<',
        checkboxClasses: '<',
        ngDisabled: '<',
        checkboxId: '<',

        clickHandler: '&', // <<< THIS ONE IS WHAT WE'RE TALKING ABOUT
    },
});

In this case, we've got a custom checkbox component that supports tristate. Specifically, that means it's a wrapper that enables easy access to checked, unchecked, and indeterminate states (three states, not the usual two. Am I over explaining this? ;^D) in an html form checkbox input.

Indeterminate status checkboxes are kinda complicated in vanilla HTML, and this is trying to abstract some of that in a package AngularJS can consume easily.

As a freakin' huge proponent of props down, events up, the tristate checkbox is a very black box.

  • You tell it the state it should be in by passing down the value carried by the prop checkState (here, for simplicity's sake, 0, 1, or 3 [sic]).
  • When it's clicked, it tells you that's happened by calling the event handler contained in clickHandler.

checkState down. clickHandler up.


Where the ampersand binding bites you

The problem is that the ampersand binding doesn't give a function for your component's controller that you can call in a conventional sense.

If clickHandler's signature on the tristate checkbox component's parent looks like this...

vm.checkHandler = function (permission, permissionType, newCheckState) {

... you'd never guess how you pass newCheckState from the child custom component to the function.

Here's an example (again, this is a real-world example. It's a little more use case specific than I'd like for a perfect demo):

<qb-tristate-checkbox
    ng-disabled="!qpc.canAssign(permission, 'Write')"
    checkbox-id="'write-' + index"
    check-state="permission.Write"

    label-display="''"

    click-handler="qpc.checkHandler(permission, 'Write', newCheckState)"
></qb-tristate-checkbox> 

permission is from an ng-repeat parent tag:

<li
    class="instructorPermissions text-center list-group-item row no-gutters justify-content-between"
    ng-if="qpc.questionIds && qpc.questionIds.length"
    
    ng-repeat="(index, permission) in qpc.processedPermissions | orderBy: ['-Owner','UserName']"
>
<!--                   ^^^^^^^^^^ that permission right there -->

'Write' is obviously just a static string.

But newCheckState comes from the component. It's the only value coming from the tristate checkbox that we care about.

How do I pass it?

Like this:

vm.clickHandler({
    newCheckState: value,
});

Inside of an object literal? Wait, WTFOMGBBQNSFW!!!!!


At first, I would've expected the event handler registration in the template to look like this:

<qb-tristate-checkbox
    click-handler="qpc.checkHandler" <<< Purely passing a reference
></qb-tristate-checkbox> 

... and the call from within the component to look like this...

// again, this would be within the QbTristateCheckboxController
vm.clickHandler(permission, 'Write', value)

But it's not.

I would have also accepted something in the component that lets you know you're calling a weirdly wrapped event handler prop, something like...

vm.callAmpersandBinding('clickHandler', permission, 'Write', value);

That's funky, but it has the benefit of letting you know something funky is going on. I mean, if clickHandler is a prop, and it has a function assigned to it, calling that reference should call the literal function, right?


Um, no. No. Not at all. Instead, we have to pass that value by embedding in an object literal. In fact, if we wanted to pass all three from the component, the code would look like...

<!-- template -->
<qb-tristate-checkbox
    click-handler="qpc.checkHandler(permission, staticString, newCheckState)"
></qb-tristate-checkbox> 

and

vm.clickHandler({
    newCheckState: value,
    permission: vm.processedPermisions[someMagicIndex], // <<< How do I find this?
    staticString: 'Write' // Outside of the template, neither makes sense
});

Instead of letting us call the handler directly, AngularJS provides some weird monkeypatching that makes clickHandler in our controller call a wrapped version of the event handler.


The advantages of the AngularJS ampersand binding convention

It's not all bad. What's neat about this is that it does allow us to combine jive from the template scope, like our permission from the ng-repeat, into an event handler call with variables sent from the component.

a visual representation of the relationship of the template, component parent, and component scopes to the event handler call

That is, by not limiting the call to the event handler from within the component to the literal event handler signature, we've got a lot more flexibility. We can combine two scopes, custom component controller and template contexts.

At the same time, though passing an object literal that maps to parameters named only in a prop definition in a template file might be more flexible, it's an extremely unintuitive, indiscoverable convention.

That we were establishing a new convention and why really didn't click for me until I understood how the new convention was useful -- precisely so that we could mix parent template and component controller scopes like this.


Put another way...

If that didn't click, here's a good explanation from superluminary on SO of the [unintuitive] way an ampersand binding in AngularJS works, and is what finally made how things worked click in my head. Once I had this convention straight and was able to combine it with the advantages of in-template parameter mixing, I was finally an ampersand binding expert. (Until I forgot.)

Anyhow, from that answer:

TL;DR;ย - You are assuming that the bound function is being passed to the child component. This is incorrect. In fact, AngularJS is parsing the string template and creating a new function, which then calls the parent function.

This function needs to receive an object with keys and values, rather than a plain variable. [emphasis mine -mfn]

...

Say I create a component like this:

const MyComponent = {
  bindings: {
    onSearch: '&'
  },
  controller: controller
};

This function (in the parent) looks like this:

onSearch(value) {
  // do search
}

In my parent template, I can now do this:

<my-component on-search="onSearch(value)"></my-component>
...

In myComponent controller, I need to do something like:

handleOnSearch(value) {
  if (this.onSearch) {
    this.onSearch({value: value})
  }
}

It's almost like AngularJS said, "I see your event handler, and I'm going to raise you this monkeypatch..."

componentController.clickHandler = function(paramsAsObjectLiteral) {
    parentScope.clickHandler({
        paramsAsObjectLiteral['newCheckState'] || templateContext.newCheckState,
        paramsAsObjectLiteral['staticString'] || templateContext.staticString,
        paramsAsObjectLiteral['permission'] || templateContext.permission,
    });
};

(Actually, that's functionally speaking exactly what AngularJS does, though there's another round of mapping after you compiled the template to code before you can have templateContext and I'm not sure which, the component or template's version of variables, really takes precedence (which goes on the left side of the ||s). The template would make more sense, I guess.)


What people usually end up doing... and why you shouldn't

The weird part is that you can skip this new convention entirely if you pass the event handler by reference. It's cheating, I'm pretty sure the AngularJS wishes you wouldn't, and you lose a lot of flexibility, but I can see why people do it. The ampersand binding convention simply isn't easily intuitive.

Here's one such example of that "my mind is blown; gimme simple" mentality from krawaller.se:

Becauseย &ย is so convoluted, many useย =ย to do output [...but...] A nicer solution than all of the above is toย useย <ย to create outputย byย passing in a callback!

We create the callback in the outer controller...

$scope.callback = function(amount){
$scope.count += amount;
}

...and pass it to the component:

<output out="callback"></output>

The component now simply calls it accordingly:

app.component("output",{
bindings: { out: '<' },
template: `
<button ng-click="$ctrl.out(1)">buy one</button>
<button ng-click="$ctrl.out(5)">buy many</button>`

});

Very similar toย &, but without the convoluted magic!

Then, in your controller, you'd simply call vm.out(myAmount) somewhere, where myAmount is a variable inside of your custom child component's scope.

But recall that you now can't do anything clever with your template's scope! As long as the template is something trivial like the <output out="callback"></output> we have here, fine, sure, you're golden. But as soon as you're one ng-repeat in, you're going to want to brush off that &.


Anyhow, I've known the ampersand binding convention long enough to use it and forget it once already, so there's a refresher. Hope that's helpful if you too have been sucked INTO AN ANACHRONISTIC ANGULARJS TIME BOB OMB!!!!1!!!

Labels: ,