Search code examples
data-bindingoperator-overloadingabstracthaxe

Haxe: Binding pattern with abstract fields access methods


I'd like to make wrapper to implement simple data binding pattern -- while some data have been modified all registered handlers are got notified. I have started with this (for js target):

class Main {
    public static function main() {
        var target = new Some();
        var binding = new Bindable(target);
        binding.one = 5;
        // binding.two = 0.12; // intentionally unset field
        binding.three = []; // wrong type
        binding.four = 'str'; // no such field in wrapped class
        trace(binding.one, binding.two, binding.three, binding.four, binding.five);
        // outputs: 5, null, [], str, null
        trace(target.one, target.two, target.three);
        // outputs: 5, null, []
    }
}

class Some {
    public var one:Int;
    public var two:Float;
    public var three:Bool;
    public function new() {}
}

abstract Bindable<TClass>(TClass) {
    public inline function new(source) { this = source; }
    @:op(a.b) public function setField<T>(name:String, value:T) {
        Reflect.setField(this, name, value);
        // TODO notify handlers
        return value;
    }
    @:op(a.b) public function getField<T>(name:String):T {
        return cast Reflect.field(this, name);
    }
}

So I have some frustrating issues: interface of wrapped object doesn't expose to wrapper, so there's no auto completion or strict type checking, some necessary attributes can be easily omitted or even misspelled.

Is it possible to fix my solution or should I better move to the macros?


Solution

  • I almost suggested here to open an issue regarding this problem. Because some time ago, there was a @:followWithAbstracts meta available for abstracts, which could be (or maybe was?) used to forward fields and call @:op(a.b) at the same time. But that's not really necessary, Haxe is powerful enough already.

    abstract Binding<TClass>(TClass) {
        public function new(source:TClass) { this = source; }
        @:op(a.b) public function setField<T>(name:String, value:T) {
            Reflect.setField(this, name, value);
            // TODO notify handlers
            trace("set: $name -> $value");
            return value;
        }
        @:op(a.b) public function getField<T>(name:String):T {
            trace("get: $name");
            return cast Reflect.field(this, name);
        }
    }
    
    @:forward
    @:multiType
    abstract Bindable<TClass>(TClass) {
        public function new(source:TClass);
        @:to function to(t:TClass) return new Binding(t);
    }
    

    We use here multiType abstract to forward fields, but resolved type is actually regular abstract. In effect, you have completion working and @:op(a.b) called at the same time.