Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom element's Binding Context - what is it exactly, how to access parent VM

I couldn't find the answer in the docs, so I'm asking here. What exactly is the binding context passed to the bind method of custom element. Is it simply equal to router's currently active ViewModel? At least, that's what I've found out so far.

Why isn't it the element's parent (in the terms of DOM) VM?

with this code

@customElement("myelem")
@inlineView("<template><content></content></template>")
export class MyElem{
    bind(ctx){
      console.log(ctx);
    }
}

// welcome.html
<myelem>
    <h3>inside myelem</h3>
    <myelem>
      <h4>inside inside ... </h4>
    </myelem>
  </myelem>

the output in the console is just current viewmodel (Welcome) printed twice.

I'd expect it to be Welcome for the first (outer) myelem, but MyElem for the second (inner) occurance...

Please explain why am I wrong here, and how can the inner custom element be aware of it's actual context (by which I mean the outer one in the case above), without using ugly hacks like creating secret property in the "shared" context (the one which actually gets passed to both of them)

like image 927
migajek Avatar asked Sep 25 '15 07:09

migajek


1 Answers

In terms of data-binding, both elements are bound to the same binding context. Consider this example:

<div foo.bind="bar">
  <div foo.bind="bar"></div>
</div>

You would expect both <div> elements to have the same binding context right? Both element's foo property should be bound to the same model's bar property. The same holds true in this scenario:

<myelem foo.bind="bar">
  <myelem foo.bind="bar"></myelem>
</myelem>

Both instances of <myelem> are bound to the same binding context / model.

If I understand the question correctly, you would like an elegant way to give the inner MyElem class instance a reference to the outer MyElem class instance. Luckily, you're using Aurelia so there is a very nice way to do this... declare it as a dependency using the inject decorator:

import {inject, Parent} from 'aurelia-dependency-injection';
import {customElement} from 'aurelia-framework';

@customElement("myelem")
@inject(Parent.of(MyElem))
export class MyElem {
  constructor(parent) {
    this.parent = parent;
  }
  ...
}

There's one caveat however...

The Aurelia dependency-injection container's default behavior is to create an instance of the requested item if an instance is not found in the container. This means that @inject(Parent.of(MyElem)) is not quite what we want. In situations where there is no parent MyElem instance the container will create one for us instead of returning null. Normally we'd use @inject(Optional.of(MyElem)) to tell the container to give us the instance, only when it exists in the container. I don't know of a way to combine Parent.of and Optional.of. I'll create an issue in the aurelia dependency-injection repository so we can get this feature added.

In the meantime, we can easily create our own Resolver that combines the behavior of Parent.of and Optional.of:

import {resolver} from 'aurelia-dependency-injection';

@resolver()
export class OptionalParent {
  constructor(key) {
    this.key = key;
  }

  get(container) {
    if (container.parent && container.parent.hasResolver(this.key, false)) {
      return container.parent.get(this.key)
    }
    return null;
  }

  static of(key) {
    return new OptionalParent(key);
  }
}

So the new version of our MyElem class would look like this:

import {inject} from 'aurelia-dependency-injection';
import {customElement} from 'aurelia-framework';
import {OptionalParent} from './optional-parent';

@customElement("myelem")
@inject(OptionalParent.of(MyElem))
export class MyElem {
  constructor(parent) {
    this.parent = parent;
  }
  ...
}

Here's a working example. Check the console for log messages showing the result:

https://gist.run/?id=1a84e0a466fb928aa075

like image 120
Jeremy Danyow Avatar answered Sep 18 '22 16:09

Jeremy Danyow