Aurelia alternative for allBindingsAccessor() method on custom bindings


#1

I am converting a custom knockout binding to a custom Aurelia Attribute. When making a custom knockout binding you can use the allBindingsAccessor() to gain access to an observable bound to other bindings and respond to changes in that binding as well.

I was doing this with a highlight binding that looked at the string bound to the text binding and wrapped text that matched a regex expression in a span element that applied styles to the matching text.

I need to do something similar with a custom Aurelia attribute and I am looking for some advice about the best way to do this.

Given this example:

//viewmodel
this.description = "this is example text content";
this.query = "example ";
<!-- view -->
<span textcontent.bind="description" highlight.bind="query"></span>

I would want this to render:

<span textcontent.bind="description" highlight.bind="query">
    this is <span class="highlight">example</span> text content 
</span>

Am I correct in assuming I would need to inject the bindingEngine and use .observerLocator.getObserver() or something similar to gain access to the textcontent binding that would let me subscribe to changes to this.description?


#2

All observers of your view model is defined at __observers__ property. You can do a for .. in to get all of them. If you are concerned about the usage of __observers__ property, then you can do inject ObserverLocator and call private method getOrCreateObserversLookup(obj) https://github.com/aurelia/binding/blob/master/src/observer-locator.js#L73-L75

  getOrCreateObserversLookup(obj) {
    return obj.__observers__ || this.createObserversLookup(obj);
  }

The property __observers__ may change but probably not the method getOrCreateObserversLookup(obj)

Edit:
Re:

Am I correct in assuming I would need to inject the bindingEngine and use .observerLocator.getObserver() or something similar to gain access to the textcontent binding that would let me subscribe to changes to this.description ?

So i see that you want get a hold of textcontent.bind='description' binding, you can hook into created() lifecycle:

class {
  ...
  created(owningView, view) {
    this.view = view;
    this.textcontentDescriptionBinding = view.bindings.find(b => b.targetProperty === 'textcontent')
  }

}

#3

That looks promising however it just returns the first textcontent binding in the view. I only need the textcontent binding for the element that also has my custom attribute on it.

Do I really need to parse every binding on the view to find this binding?


#4

I guess you can do

this.allBindingsNeeded = view.bindings.find(b => b.targetProperty === 'textcontent' && b.target.au && b.target.au.highlight)

#5

Actually, b.target === this.element && b.targetProperty === 'textContent'; seems to filter just fine but I am struggling to get a subscription to work.

I would think that inside of bind I could:

bind(binding, source) {
    if(typeof this.textcontentBinding !== 'undefined') {
      this.textcontentSub = this.textcontentBinding.observerLocator.getObserver(binding, this.textcontentBinding.targetProperty).subscribe((value)=>{
        //do stuff with textcontent value?
      });
    }
}

But this is not being triggered when the property bound to the textcontent binding is changed. What am I doing wrong here?


#6
this.textcontentBinding.observerLocator.getObserver(
  binding,
  this.textcontentBinding.targetProperty
)

Doing that means you are observing targetProperty of binding object, instead of view model. I’m also uncertain about your bind method, is it in your view model, or in your custom binding class. If it was in view model, change it to:

bind(bindingContext, overrideContext) {
    if(typeof this.textcontentBinding !== 'undefined') {
      this.textcontentSub = this.textcontentBinding.observerLocator.getObserver(this, this.textcontentBinding.targetProperty).subscribe((value)=>{
        //do stuff with textcontent value?
      });
    }
}

But you can see that you can easily do the same thing, from within view model using change handler.


#7

Thanks for your help but this still doesn’t work. All of this code needs to work from inside a separate custom attribute class declaration. Please refer to my original question as for what I am trying to achieve. I effectively need to extend the functionality of the textcontent attribute with my custom highlight attribute. This was easy to achieve in knockout but it seems to be very complicated to pull off with Aurelia.


#8

All of this code needs to work from inside a separate custom attribute class declaration

That was not made clear, or I completely missed it.

This was easy to achieve in knockout but it seems to be very complicated to pull off with Aurelia.

From what you have shown so far, I’m pretty sure it can be done easily as well, but I’d need some pseudo code, or some explanation for this, before I can put together an example I effectively need to extend the functionality of the textcontent attribute with my custom highlight attribute


#9

The best way would be to show what knockout allowed:

ko.bindingHandlers.highlight = {
    update: function(element, valueAccessor, allBindings) {
        // First get the latest data that we're bound to
        var value = valueAccessor();
 
        // Next, whether or not the supplied model property is observable, get its current value
        var valueUnwrapped = ko.unwrap(value);
 
        // Grab some more data from another binding property
        var text = allBindings.get('text') || "";
        var textUnwrapped = ko.unwrap(text);
        // Now call a function passing in the text query and the source text binding string to highlight and return highlighted html string
        var highlighedtext = regexHighlight(valueUnwrapped, textUnwrapped);

          // update value of element
         element.innerhtml = highlighedtext;
    }
};

Is it possible to I recreate this functionality with a custom Attribute using Aurelia?


#10

A crude translation, to make it familiar:

import { Binding, View } from 'aurelia-framework';

@inject(Element)
@customAttribute('highlight')
export class Highlight {

  // This is magic property of all custom attribute,
  // by default if no primary bindable specified
  value: string;
  // reference of bindings we want to deal with
  relevantBindings: Binding;

  constructor(public element: Element) {}

  // owning view is the view of the custom element where this attribute
  // resides in
  created(owningView: View) {
    this.relevantBinding = owningView.bindings.find(b => b.target === this.element && b.targetProperty === 'textContent');
  }

  /**
   * 
   */
  valueChanged(newValue) {
    let binding = this.relevantBinding;
    let textUnwrapped = binding.sourceExpression.evaluate(binding.source, binding.lookupFunctions);
    let highlighedText = regexHighlight(newValue, textUnwrapped);
    this.element.innerHTML = highlightedText;
  }
}

A better version of this would be to pass the description to the custom attribute, so it can handle the highlighting all by its own, instead of go looking for the binding and evaluate its expression value:

<span highlight="query.bind: query; text.bind: description"></span>
import  { customAttribute, inject, bindable } from 'aurelia-framework';

@inject(Element)
@customAttribute('highlight')
export class Highlight {

    @bindable() query: string;
    @bindable({
      primaryProperty: true
    })
    text: string;

    constructor(public element: Element) {}

    /**
     * implement bind to stop initial change handler invokations
     * (2 of them, because of 2 bindables specified in the view)
     */
    bind() {
      this.propertyChanged();
    }

    propertyChanged() {
      let highlighedText = regexHighlight(this.query, this.text);
      this.element.innerHTML = highlightedText;
    }
}

Now whenever query or description change, you get what you want. Can you try it?

Note that we primaryProperty: true for text is to make the public API of the custom attribute more intuitive


#11

In your first example, does calling evaluate create a subscription?


#12

No, it just does what it says: evaluates the value, based on view model and an expression specified in the view.


#13

Then it does not recreate the custom knockout binding which updates whenever the value of the text binding changes. When you call the ko.unwrap() it creates a subscription that will trigger the custom binding’s update method to achieve this.

Please keep in mind that the example I gave was also a “crude” representation of what I am hoping to achieve. I have much more functionality in this and other custom attributes that I also have to support and what I am looking for is a comparable alternative to Knockout’s allBindingAccessor() in Aurelia that won’t require me to refactor a few thousand lines of code…

*edit
I very much appreciate your help with this question :slight_smile: thank you for taking the time to respond this late in the afternoon!


#14

Did the code I suggested work for you?

I’m not familar with knockout, but I’m still sure that what you hope to achieve can be translated fairly easily. We may just need more than some code exchange. I’m happy to help in a remote session with the help of a microphone. And some speaker or earphone.


#15

Sorry, but I already thought of the work around you suggested. As I tried to explain in my previous comment, that idea presents other problems. I have other custom attributes that work with but also independently from my custom highlight binding which help with search filtering, sorting, grouping and other nifty interactions. Refactoring the highlight binding to also control the default rendering of the text could be a massive undertaking. Not an ideal solution.

All I need is the ability to subscribe to changes of an elements attribute binding (any of them, not just textcontent) from a separate custom attribute binding.
I don’t think a call is necessary but I am open to it if helps solve this problem.


#16

You can have a look at view.bindings, it contains all bindings of current view in the custom element. Maybe you will find what you want there, if it’s the binding that you want to find.

However, it sounds to me it’s the value of custom element view model, which ultimately all bindings / custom attributes are pointing to, is what you are after, so maybe just subscribe to it directly in your highlight custom attribute:

bind(customAttribute, overrideContext) {
  const customElementViewModel  = this.owningView.bindingContext;
  bindingEngine.propertyObserver(customElementViewModel, 'description').subscribe(newValue => {
    // is it??
  });
}

#17

So I figured out a somewhat simple solution. Injecting the TargetInstruction to get the bindingExpression for the target element seems to be more efficient than performing a find on the bindings array. Why does thetextcontent have to be a two-way binding to work?

//viewmodel
this.description = "this is example text content";
this.query = "example ";
<!-- view -->
<span textcontent.two-way="description" highlight.bind="query"></span>
//custom highlight attribute
import {inject, BindingEngine, customAttribute, bindable, bindingMode, TargetInstruction} from 'aurelia-framework';

@customAttribute('highlight')
@inject(Element, BindingEngine, TargetInstruction)
export class Highlight {
  @bindable({ primaryProperty: true }) match;

  constructor(element, BindingEngine, TargetInstruction){
    this.element = element;
    this.bindingEngine = BindingEngine;
    this.bindingExpression = TargetInstruction.expressions.find((b)=> {
      return b.targetProperty === 'textContent';
    });
  }
  
  matchChanged(newValue, oldValue){
    this.update();
  }

  bind(binding, source) {
    let bindingExpression = this.bindingExpression;
    this.textcontentSub = this.bindingEngine.propertyObserver(binding, bindingExpression.sourceExpression.name).subscribe((value)=> {
      this.update(value);
    });
    this.update();
  }

  unbind(binding, source) {
    this.textcontentSub.dispose();
  }

  created(owningView, view) {
    this.view = view || owningView;
  }

  get text() {
    let bindingExpression = this.bindingExpression;
    if (bindingExpression) {
      return bindingExpression.sourceExpression.evaluate(this.view, bindingExpression.lookupFunctions);
    } else if (this.element) {
      return this.element.innerHTML;
    } else {
      return "";
    }
  }
  update(text){
    if(this.active !== false) {
      text = text || this.text
      let match = this.match;
      if (match instanceof RegExp) {
        regex = match;
      } else {      
        let matchString = match;

        matchString = matchString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
        matchString = matchString.replace(/[a-zA-Z0-9\?\&\=\%\#]+s\=(\w+)(\&.*)?/, "$1");
        matchString = matchString.replace(/\%20|\+/g, "\|");

        regex = new RegExp("(" + matchString + ")(?=[^>]*(<|$))", "ig");
      }

      let highlightedText;
      //Call function that highlights the text
      highlightedText = highlightText(text, regex);
      let isTextHighlighted = highlightedText !== false;

      if (isTextHighlighted) {
        this.element.innerHTML = highlightedText;
      }
    }
  }
}

function highlightText(text, regex, css, style) {
  var tempText;
  var attrText;
  var matched;

  matched = false;
  tempText = text;

  attrText = ' style="font-weight: bold;"';
  // Do regex replace
  // Inject span with class and styles set with the binding

  text = tempText.replace(regex, function (match, p1, p2, offset, stringArg) {//p1, p2 Corresponds to $1, $2
    var returnReplace;
    if (typeof match === "string" && match.length > 0) {
      matched = true;
      returnReplace = '<span' + attrText + '>' + p1 + '</span>' + p2;
    } else {
      returnReplace = p1 + p2;
    }
    return returnReplace;
  });

  return matched ? text : false; //return false if nothing was changed
}

This is a trimmed down, working example. Is there a better way to do this?


#18

Oh, I think i gave you the wrong example. Is this the property description you wanted to target, but you got textcontent from what I suggested?


#19

No, no, no, no… somewhere there is a miscommunication between what I am posting and what you are understanding.

Thank you for your help. While attempting to implement some of your suggestions I actually discovered what I needed. My previous post provides you a working example of the functionality I was looking to build. And I believe it is a reliable pattern for converting custom knockout bindings that rely on the allBindingsAccessor to the Aurelia framework.

Is there is anything wrong with the solution I found?


#20

Maybe I should read the whole thing again. Maybe I was too focusing on the details. Glad you got it working though.

For your code example:

In your .bind(), you can rename it to :

  bind(highlightCustomAttribute) {
    // ...
  }

And you can see you are just listening to property description of it, which seems weird to me.