Search code examples
data-bindingaureliatemplate-engine

binding to bindable attributes of dynamically generated component/element


I am developing a custom attribute on aurelia to let user select from a list while typing in a textarea. For example, the usage will be something like this:

<textarea value.bind="description" placeholder="your description here" auto-complete></textarea>

and as you probably noticed, the auto-complete is the attribute. Now when I want to show the hints, I want to do this in a custom-element to keep it simple. So the attached method of the attribute will be something like this:

attached() {
    this.containerElement = document.createElement('div');
    this.containerElement.style.position = 'relative';
    const ce = document.createElement('autocomplete-menu');
    this.containerElement.appendChild(ce);
    const ceView = this.templatingEngine.enhance(ce);
    ceView.attached();
    const currentParrent = this.element.parentElement;
    currentParrent.replaceChild(this.containerElement, this.element);
    this.containerElement.appendChild(this.element);
}

now it opens and shows the hint area successfully. The screen-shot:

result

The problem gets started when I want to communicate to the generated element from the attribute view-model. For example, I want to send a data to its view-model or bind some object to a bindable property of that. For this issue I have found these solutions:

https://discourse.aurelia.io/t/dynamically-add-custom-attribute-to-element/1400/6 https://ilikekillnerds.com/2016/01/enhancing-at-will-using-aurelias-templating-engine-enhance-api/

and also read the last part of this:

https://aurelia.io/docs/binding/how-it-works#abstract-syntax-tree

and figured out that, I have to introduce an object for the element's view-model as its bindingContext or overrideContext. So if I am right, I have tested the solutions below:

this.containerElement.appendChild(ce);
let vm = { test: 1 }
const ceView = this.templatingEngine.enhance({ element: ce, bindingContext: vm });
ceView.addBinding(vm);
ceView.attached();

and

this.containerElement.appendChild(ce);
let vm = { test: 1 }
const ceView = this.templatingEngine.enhance(ce);
ceView.bind(vm);
ceView.attached();
console.log(ceView);

but on the attached lifecycle-hook of the element, I have logged the view-model and noticed that the bindingContext properties are not present on this.

Now there are two questions:

  1. What's the problem with the above solutions and how can I send this kind of data to an enhanced element?
  2. Is there a way to do this with the known method of bindables? I mean defining a bindable property on the element view-model and bind to it after the enhance method is done. Instead of working with bindingContext and overrideContext?

Solution

  • Fortunately the problem has been solved. The solution was not that much complex but 1. it is very very useful for me (as I will describe next) 2. the lack of aurelia documentation made this simple issue hard to solve.

    The problem was about I was misunderstanding the meanings of bindingContext and container. I was thinking that bindingContext will refer to view-model of the child element and I must point its container to the parent context (which is the attribute). But I found that I should point the bindingContext to the context of attribute. I still don't know enough about those two meanings but the solution is as simple and beautiful as the following sample:

    this.containerElement = document.createElement('div');
    this.containerElement.style.position = 'relative';
    this.containerElement.style.display = 'flex';
    this.containerElement.style.flexDirection = 'row-reverse';
    this.ce = document.createElement('autocomplete-menu');
    this.ce.setAttribute('filter.bind', 'filter');
    this.ce.setAttribute('show.bind', 'showMentionPicker');
    this.ce.setAttribute('parent-height', '${element.clientHeight}px');
    this.ce.setAttribute('view-model.ref', 'mentionPickerViewModel');
    this.ce.setAttribute('on-select.call', 'complete(mentionPickerViewModel.getRemainingOfHint())');
    const ceView = this.templatingEngine.enhance({ element: this.ce, container: this.container });
    ceView.bind(this);
    ceView.attached();
    this.containerElement.appendChild(this.ce);
    const currentParrent = this.element.parentElement;
    currentParrent.replaceChild(this.containerElement, this.element);
    this.containerElement.appendChild(this.element);
    

    and the bindings are referring to the attribute's this context which has the following properties:

    filter = '';
    showMentionPicker = false;
    mentionPickerViewModel; 
    

    If the sample is not enough for your problem ask me for more information.