From f48651338b9bdba43b7bdf9772009542f3733692 Mon Sep 17 00:00:00 2001 From: Dmitri Granetchi Date: Sun, 20 Apr 2014 18:48:35 +0300 Subject: [PATCH] initial commit --- .gitignore | 14 ++ haxelib.json | 13 ++ src/Main.hx | 59 ++++++++ src/bindx/Bind.hx | 80 +++++++++++ src/bindx/BindMacros.hx | 183 ++++++++++++++++++++++++ src/bindx/BindSignal.hx | 210 ++++++++++++++++++++++++++++ src/bindx/IBindable.hx | 5 + src/bindx/IBindingSignalProvider.hx | 16 +++ src/bindx/MetaUtils.hx | 69 +++++++++ test.hxml | 5 + test/BaseTest.hx | 138 ++++++++++++++++++ test/InheritanceTest.hx | 47 +++++++ test/TestProperty.hx | 48 +++++++ test/Tests.hx | 14 ++ 14 files changed, 901 insertions(+) create mode 100644 .gitignore create mode 100644 haxelib.json create mode 100644 src/Main.hx create mode 100644 src/bindx/Bind.hx create mode 100644 src/bindx/BindMacros.hx create mode 100644 src/bindx/BindSignal.hx create mode 100644 src/bindx/IBindable.hx create mode 100644 src/bindx/IBindingSignalProvider.hx create mode 100644 src/bindx/MetaUtils.hx create mode 100644 test.hxml create mode 100644 test/BaseTest.hx create mode 100644 test/InheritanceTest.hx create mode 100644 test/TestProperty.hx create mode 100644 test/Tests.hx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72853c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +bin/ +dump/ +out/ +target/ + +*.hxproj +.idea +*.iml +*.ipr +*.sublime-project +*.sublime-workspace + +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/haxelib.json b/haxelib.json new file mode 100644 index 0000000..72d6c51 --- /dev/null +++ b/haxelib.json @@ -0,0 +1,13 @@ +{ + "name": "bindx2", + "url" : "https://github.com/profelis/bindx2", + "license": "MIT", + "tags": ["bind", "binding", "bindings", "cross"], + "description": "Powerful and fast macro-based data binding engine inspired by Flex Bindings with easy-to-use syntax.", + "classPath": "src", + "version": "2.0.0-alpha", + "releasenote": "bindx reification", + "contributors": [ + "deep" + ] +} \ No newline at end of file diff --git a/src/Main.hx b/src/Main.hx new file mode 100644 index 0000000..fccaa37 --- /dev/null +++ b/src/Main.hx @@ -0,0 +1,59 @@ +package ; +class Main { + + static function main() { + trace("tada"); + + bindx.BindSignal.BindSignalProvider.register(); + + var v = new Value(); + var s = {t:0}; + + bindx.Bind.bindx(v.str, function (from, to) {trace('str changed from $from to $to');}); + v.strChanged.add(function (from, to) {trace('str changed from $from to $to');}); + v.str = "12"; + bindx.Bind.bindx(v.int, function (a, b) { trace(b); }); + var unbind = bindx.Bind.bindTo(v.int, s.t); + v.int = 10; + trace(s.t); + unbind(); + v.int = 12; + trace(s.t); + trace(v.str); + + } +} + +@:bindable +class Value implements bindx.IBindable { + + public function new() { + } + + @:bindable(lazySignal=true, inlineSignalGetter=false, inlineSetter=true) + public var str:String; + + @:bindable(force=true, inlineSetter=true) + public var int(default, set):Int; + + private var noBindPrivate:Int; + + function set_int(v):Int { + + if (v < 0) { + int = 0; + toStringChanged.dispatch(); + intChanged.dispatch(v, int); + return int; + } + + intChanged.dispatch(int, int = v); + toStringChanged.dispatch(); + return v; + } + + @:bindable() + public function toString() { + return str + int; + } +} diff --git a/src/bindx/Bind.hx b/src/bindx/Bind.hx new file mode 100644 index 0000000..547f3db --- /dev/null +++ b/src/bindx/Bind.hx @@ -0,0 +1,80 @@ +package bindx; + +#if macro +import haxe.macro.Expr; +import haxe.macro.Type; +import haxe.macro.Context; + +using haxe.macro.Tools; +using bindx.MetaUtils; +#end +using Lambda; + +class Bind { + + @:noUsing macro static public function bindx(field:Expr, listener:Expr):Expr { + return bind(field, listener, true); + } + + @:noUsing macro static public function bindTo(field:Expr, target:Expr):Expr { + var fieldData = checkField(field); + return BindMacros.bindingSignalProvider.getClassFieldBindToExpr(fieldData.e, fieldData.field, target); + } + + @:noUsing macro static public function unbindx(field:Expr, listener:Expr):Expr { + return bind(field, listener, false); + } + + @:noUsing macro static public function notify(field:Expr, ?oldValue:Expr, ?newValue:Expr):Expr { + var fieldData = checkField(field); + return BindMacros.bindingSignalProvider.getClassFieldChangedExpr(fieldData.e, fieldData.field, oldValue, newValue); + } + + #if macro + static function bind(field:Expr, listener:Expr, doBind:Bool):Expr { + var fieldData = checkField(field); + return if (fieldData != null) { + if (doBind) BindMacros.bindingSignalProvider.getClassFieldBindExpr(fieldData.e, fieldData.field, listener); + else BindMacros.bindingSignalProvider.getClassFieldUnbindExpr(fieldData.e, fieldData.field, listener); + } else macro {}; + } + + static function checkField(field:Expr):{e:Expr, field:ClassField} { + switch (field.expr) { + case EField(e, field): + var classType = Context.typeof(e).getClass(); + if (classType == null || !isBindable(classType)) { + Context.error('\'${e.toString()}\' must be bindx.IBindable', e.pos); + return null; + } + + var field:ClassField = classType.findField(field, null); + if (field == null) { + Context.error('\'${e.toString()}.$field\' expected', field.pos); + return null; + } + + if (!field.hasBindableMeta()) { + Context.error('\'${e.toString()}.$field\' is not bindable', field.pos); + return null; + } + + return {e:e, field:field}; + + case EConst(CIdent(_)): + Context.error('can\'t bind \'${field.toString()}\'. Please use \'this.${field.toString()}\'', field.pos); + + case _: + Context.error('can\'t bind field \'${field.toString()}\'', field.pos); + } + return null; + } + + static inline function isBindable(classType:ClassType) { + return classType.interfaces.exists(function (it) { + var t = it.t.get(); + return t.module == "bindx.IBindable" && t.name == "IBindable"; + }); + } + #end +} \ No newline at end of file diff --git a/src/bindx/BindMacros.hx b/src/bindx/BindMacros.hx new file mode 100644 index 0000000..f12f4e4 --- /dev/null +++ b/src/bindx/BindMacros.hx @@ -0,0 +1,183 @@ +package bindx; + +import haxe.macro.Type; +import haxe.macro.Expr; +import haxe.macro.Context; + +using haxe.macro.Tools; +using Lambda; +using StringTools; +using bindx.MetaUtils; + +class BindMacros { + #if macro + static inline var OLD_VALUE = "__oldValue__"; + static inline var NEW_VALUE = "__newValue__"; + + /** + * default value: false + */ + static public inline var INLINE_SETTER = "inlineSetter"; + /** + * default value: false + */ + static public inline var FORCE = "force"; + + static var processed:Array = []; + + static public var bindingSignalProvider:IBindingSignalProvider; + + static public function setBindingSignalProvider(value:IBindingSignalProvider) { + bindingSignalProvider = value; + return macro {}; + } + + macro static public function buildIBindable():Array { + var type = Context.getLocalType(); + if (processed.indexOf(type) > -1) { + return null; + } + processed.push(type); + if (bindingSignalProvider == null) { + bindingSignalProvider = new bindx.BindSignal.BindSignalProvider(); + } + + var classType = type.getClass(); + var fields = Context.getBuildFields(); + + var meta = classType.bindableMeta(); + if (meta != null) injectBindableMeta(fields, meta); + + var res = []; + for (f in fields) + if (f.hasBindableMeta()) { + if (!isFieldBindable(f, fields)) Context.error('can\'t bind field \'${f.name}\'', f.pos); + + bindField(f, fields, res); + } else res.push(f); + + return res; + } + + static function bindField(field:Field, fields:Array, res:Array) { + var meta = field.bindableMeta(); + bindingSignalProvider.getFieldDispatcher(field, res); + + var forceParam = meta.findParam(FORCE); + var inlineSetter = meta.findParam(INLINE_SETTER); + if (forceParam.isNotNullAndTrue()) { + if (inlineSetter != null) + Context.warning('\'$INLINE_SETTER\' ingored. \'$FORCE\' mode', inlineSetter.pos); + res.push(field); + return; + } + + switch (field.kind) { + case FVar(type, expr): + var fieldName = field.name; + var setterName = 'set_$fieldName'; + field.kind = FProp("default", "set", type, expr); + res.push(field); + var setter = macro function foo(value:$type) { + var $OLD_VALUE = this.$fieldName; + if ($i{OLD_VALUE} == value) return $i{OLD_VALUE}; + this.$fieldName = value; + ${bindingSignalProvider.getFieldChangedExpr(field, macro $i{OLD_VALUE}, macro $i{"value"})} + return value; + }; + var setterAccess = [APrivate]; + if (inlineSetter.isNotNullAndTrue()) setterAccess.push(AInline); + res.push({ + name: setterName, + kind: FFun(switch (setter.expr) { case EFunction (_, func): func; case _: throw false; }), + pos: field.pos, + access: setterAccess + }); + + case FProp(get, set, type, expr): + if (inlineSetter != null) + Context.warning('$INLINE_SETTER ingored. Setter already exist', inlineSetter.pos); + var fieldName = field.name; + var setter = fields.find(function (it) return it.name == 'set_$fieldName'); + if (setter == null) return; + switch (setter.kind) { + case FFun(func): + patchField = field; + func.expr = macro { + var $OLD_VALUE = this.$fieldName; + if ($i{OLD_VALUE} == $i{func.args[0].name}) return $i{OLD_VALUE}; + $e{func.expr.map(patchSetter)}; + }; + patchField = null; + case _: + + } + res.push(field); + + case FFun(f): + if (inlineSetter != null) + Context.warning('methods doesn\'t support \'$INLINE_SETTER\'', inlineSetter.pos); + res.push(field); + } + } + + static var patchField:Field; + + static function patchSetter(expr:Expr):Expr { + return switch (expr.expr) { + case EReturn(res): + var fieldName = patchField.name; + + macro { + var $NEW_VALUE = ${res.map(patchSetter)}; + ${bindingSignalProvider.getFieldChangedExpr(patchField, macro $i{OLD_VALUE}, macro $i{NEW_VALUE})}; + return $i{NEW_VALUE}; + } + + case _: expr.map(patchSetter); + } + } + + static inline function injectBindableMeta(fields:Array, meta:MetadataEntry) { + for (f in fields) { + if (f.hasBindableMeta()) continue; + if (f.access.exists(function (it) return it.equals(APrivate))) continue; + + var forceParam = meta.findParam(FORCE); + if (isFieldBindable(f, fields, forceParam.isNotNullAndTrue())) + switch (f.kind) { + case FFun(_): + case _: f.meta.push({name:MetaUtils.BINDABLE_META, pos:f.pos, params:meta.params}); + } + } + } + + static function isFieldBindable(field:Field, fields:Array, force = false):Bool { + if (field.name == "new") return false; + + if (field.access.exists(function (it) return it.equals(AMacro) || it.equals(ADynamic) || it.equals(AStatic))) + return false; + + if (field.name.startsWith("set_") || field.name.startsWith("get_")) { + var propName = field.name.substr(4); + if (fields.exists(function(it) return it.name == propName)) return false; + } + + if (!force) { + var meta = field.bindableMeta(); + var forceParam = meta != null ? meta.findParam(FORCE) : null; + force = forceParam.isNotNullAndTrue(); + } + if (force) + return switch (field.kind) { + case FProp("never", _, _, _): false; + case _: true; + } + + return switch (field.kind) { + case FProp("never", _, _, _) | FProp(_, "never", _, _) | FProp(_, "dynamic", _, _) | FProp(_, "null", _, _): false; + case _: true; + } + } + #end +} \ No newline at end of file diff --git a/src/bindx/BindSignal.hx b/src/bindx/BindSignal.hx new file mode 100644 index 0000000..30349cb --- /dev/null +++ b/src/bindx/BindSignal.hx @@ -0,0 +1,210 @@ +package bindx; + +import haxe.macro.Expr; +import haxe.macro.Type; +import haxe.macro.Context; + +using bindx.MetaUtils; + +class BindSignalProvider implements IBindingSignalProvider { + + macro static public function register() { + bindx.BindMacros.setBindingSignalProvider(new BindSignalProvider()); + return macro {}; + } + + #if macro + + static inline var SIGNAL_POSTFIX = "Changed"; + + /** + * default value: true + */ + static inline var LAZY_SIGNAL = "lazySignal"; + /** + * default value: false + */ + static inline var INLINE_SIGNAL_GETTER = "inlineSignalGetter"; + + public function new() {} + + @:expose static inline function signalName(fieldName:String) return fieldName + SIGNAL_POSTFIX; + @:expose static inline function signalGetterName(fieldName:String) return "get_" + signalName(fieldName); + @:expose static inline function signalPrivateName(fieldName:String) return "_" + signalName(fieldName); + + public function getFieldDispatcher(field:Field, res:Array) { + switch (field.kind) { + case FFun(_): + generateSignal(field, macro : bindx.BindSignal.MethodSignal, macro new bindx.BindSignal.MethodSignal(), res); + case FProp(_, _, type, _) | FVar(type, _): + generateSignal(field, macro : bindx.BindSignal.FieldSignal<$type>, macro new bindx.BindSignal.FieldSignal<$type>(), res); + } + } + + public function getFieldChangedExpr(field:Field, oldValue:Expr, newValue:Expr):Expr { + var args = switch (field.kind) { case FFun(_): []; case _: [oldValue, newValue]; } + return dispatchSignal(macro this, field.name, hasLazy(field.bindableMeta()), args); + } + + public function getClassFieldBindExpr(expr:Expr, field:ClassField, listener:Expr):Expr { + var signalName = signalName(field.name); + return macro $expr.$signalName.add($listener); + } + + public function getClassFieldBindToExpr(expr:Expr, field:ClassField, target:Expr):Expr { + var signalName = signalName(field.name); + return switch (field.kind) { + case FMethod(_): + var fieldName = field.name; + macro { + var listener = function () $target = $expr.$fieldName(); + $expr.$signalName.add(listener); + function __unbind__() $expr.$signalName.remove(listener); + } + case FVar(_, _): + macro { + var listener = function (from, to) $target = to; + $expr.$signalName.add(listener); + function __unbind__() $expr.$signalName.remove(listener); + } + } + } + + public function getClassFieldUnbindExpr(expr:Expr, field:ClassField, listener:Expr):Expr { + var signalName = signalName(field.name); + return macro $expr.$signalName.remove($listener); + } + + public function getClassFieldChangedExpr(expr:Expr, field:ClassField, oldValue:Expr, newValue:Expr):Expr { + var args = switch (field.kind) { + case FMethod(_): []; + case FVar(_, _): [oldValue, newValue]; + } + return dispatchSignal(expr, field.name, hasLazy(field.bindableMeta()), args); + } + + function generateSignal(field:Field, type:ComplexType, builder:Expr, res:Array) { + var signalName = signalName(field.name); + var meta = field.bindableMeta(); + var inlineSignalGetter = meta.findParam(INLINE_SIGNAL_GETTER); + + if (hasLazy(meta)) { + var signalPrivateName = signalPrivateName(field.name); + res.push({ + name: signalPrivateName, + kind: FVar(type, null), + pos: field.pos + }); + + res.push({ + name: signalName, + kind: FProp("get", "never", type, null), + pos: field.pos, + access: [APublic] + }); + + var getter = macro function foo() { + if (this.$signalPrivateName == null) { + this.$signalPrivateName = ${builder} + } + return $i{signalPrivateName}; + }; + var getterAccess = []; + if (inlineSignalGetter.isNotNullAndTrue()) getterAccess.push(AInline); + + res.push({ + name: signalGetterName(field.name), + kind: FFun(switch (getter.expr) { case EFunction (_, func): func; case _: throw false; }), + pos: field.pos, + access: getterAccess + }); + } else { + if (inlineSignalGetter != null) + Context.warning('$INLINE_SIGNAL_GETTER works only with lazy signals', inlineSignalGetter.pos); + + res.push({ + name: signalName, + kind: FProp("default", "null", type, builder), + pos: field.pos, + access: [APublic] + }); + } + } + + inline function dispatchSignal(expr:Expr, fieldName:String, lazy:Bool, args:Array) { + return + if (lazy) { + var signalPrivateName = signalPrivateName(fieldName); + macro if ($expr.$signalPrivateName != null) { + $expr.$signalPrivateName.dispatch($a{args}); + } + } else { + var signalName = signalName(fieldName); + macro $expr.$signalName.dispatch($a{args}); + } + } + + @:expose inline function hasLazy(meta:MetadataEntry) { + return meta.findParam(LAZY_SIGNAL).isNullOrTrue(); + } + #end +} + +class MethodSignal extends Signal Void> { + + public function dispatch():Void { + lock ++; + for (l in listeners) l(); + if (lock > 0) lock --; + } +} + +class FieldSignal extends Signal T -> Void> { + + public function dispatch(oldValue:T = null, newValue:T = null):Void { + lock ++; + for (l in listeners) l(oldValue, newValue); + if (lock > 0) lock --; + } +} + +class Signal { + + var listeners:Array; + + var lock = 0; + + public function new() { + removeAll(); + } + + public inline function removeAll() { + listeners = []; + } + + public inline function dispose() { + listeners = null; + } + + public function add(listener:T):Void { + if (listeners.indexOf(listener) == -1) { + checkLock(); + listeners.push(listener); + } + } + + public function remove(listener:T):Void { + var pos = listeners.indexOf(listener); + if (pos > -1) { + checkLock(); + listeners.splice(pos, 1); + } + } + + @:expose inline function checkLock() { + if (lock > 0) { + listeners = listeners.copy(); + lock --; + } + } +} \ No newline at end of file diff --git a/src/bindx/IBindable.hx b/src/bindx/IBindable.hx new file mode 100644 index 0000000..16e87fc --- /dev/null +++ b/src/bindx/IBindable.hx @@ -0,0 +1,5 @@ +package bindx; + +@:autoBuild(bindx.BindMacros.buildIBindable()) +interface IBindable { +} diff --git a/src/bindx/IBindingSignalProvider.hx b/src/bindx/IBindingSignalProvider.hx new file mode 100644 index 0000000..d9f3bae --- /dev/null +++ b/src/bindx/IBindingSignalProvider.hx @@ -0,0 +1,16 @@ +package bindx; + +import haxe.macro.Expr; +import haxe.macro.Type; + +interface IBindingSignalProvider { + #if macro + function getFieldDispatcher(field:Field, result:Array):Void; + function getFieldChangedExpr(field:Field, oldValue:Expr, newValue:Expr):Expr; + + function getClassFieldBindExpr(expr:Expr, field:ClassField, listener:Expr):Expr; + function getClassFieldBindToExpr(expr:Expr, field:ClassField, target:Expr):Expr; + function getClassFieldUnbindExpr(expr:Expr, field:ClassField, listener:Expr):Expr; + function getClassFieldChangedExpr(expr:Expr, field:ClassField, oldValue:Expr, newValue:Expr):Expr; + #end +} diff --git a/src/bindx/MetaUtils.hx b/src/bindx/MetaUtils.hx new file mode 100644 index 0000000..79eab3e --- /dev/null +++ b/src/bindx/MetaUtils.hx @@ -0,0 +1,69 @@ +package bindx; + +import haxe.macro.Type; +import haxe.macro.Expr; +import haxe.macro.Context; + +using haxe.macro.Tools; +using Lambda; + +class MetaUtils { + + static public inline var BINDABLE_META = ":bindable"; + + static public function findParam(meta:MetadataEntry, name:String):Expr { + if (meta.params == null) { + return null; + } + for (p in meta.params) { + switch (p.expr) { + case EBinop(OpAssign, e1, e2): + if (e1.toString() == name) return {expr:e2.expr, pos:p.pos}; + case _: + Context.warning('Bindable arguments syntax error. Supported syntax: (flag1=true, flag2=false)', p.pos); + } + } + return null; + } + + static public inline function bindableMeta(meta:Metadata):MetadataEntry + return meta.find(function (it) return it.name == BINDABLE_META); +} + +class FieldMetaUtils { + static public inline function bindableMeta(field:Field):MetadataEntry + return MetaUtils.bindableMeta(field.meta); + + static public inline function hasBindableMeta(field:Field):Bool + return bindableMeta(field) != null; +} + +class ClassFieldMetaUtils { + static public inline function bindableMeta(field:ClassField):MetadataEntry + return MetaUtils.bindableMeta(field.meta.get()); + + static public inline function hasBindableMeta(field:ClassField):Bool + return bindableMeta(field) != null; +} + +class ClassTypeMetaUtils { + static public inline function bindableMeta(classType:ClassType):MetadataEntry + return MetaUtils.bindableMeta(classType.meta.get()); + + static public inline function hasBindableMeta(classType:ClassType):Bool + return bindableMeta(classType) != null; +} + +class ExprMetaUtils { + static public inline function isTrue(expr:Expr):Bool + return expr.expr.match(EConst(CIdent("true"))); + + static public inline function isFalse(expr:Expr):Bool + return expr.expr.match(EConst(CIdent("false"))); + + static public inline function isNotNullAndTrue(expr:Expr):Bool + return expr != null && isTrue(expr); + + static public inline function isNullOrTrue(expr:Expr):Bool + return expr == null || isTrue(expr); +} \ No newline at end of file diff --git a/test.hxml b/test.hxml new file mode 100644 index 0000000..5459893 --- /dev/null +++ b/test.hxml @@ -0,0 +1,5 @@ +-main Tests +-cp src +-cp test +-neko bin/bind.n +-D dump=pretty \ No newline at end of file diff --git a/test/BaseTest.hx b/test/BaseTest.hx new file mode 100644 index 0000000..246b47e --- /dev/null +++ b/test/BaseTest.hx @@ -0,0 +1,138 @@ +package ; + +import haxe.unit.TestCase; + +class BaseTest extends TestCase { + + public function new() { + super(); + } + + function test1() { + var b = new Bindable1(); + b.str = "a"; + var callNum = 0; + b.strChanged.add(function (from, to) { + assertEquals(from, "a"); + assertEquals(to, "b"); + callNum ++; + }); + + bindx.Bind.bindx(b.str, function (from, to) { + assertEquals(from, "a"); + assertEquals(to, "b"); + callNum ++; + }); + b.str = "b"; + assertEquals(callNum, 2); + } + + function test2() { + var b = new Bindable1(); + b.str = null; + var callNum = 0; + var listener = function (from, to) { + assertEquals(from, null); + assertEquals(to, ""); + callNum ++; + } + + b.strChanged.add(listener); + bindx.Bind.bindx(b.str, listener); + b.str = ""; + assertEquals(callNum, 1); + + b.strChanged.add(listener); + bindx.Bind.unbindx(b.str, listener); + b.str = "1"; + assertEquals(callNum, 1); + } + + function test3() { + var b = new Bindable1(); + b.str = null; + var callNum = 0; + + bindx.Bind.bindx(b.str, function (_, _) callNum++); + bindx.Bind.bindx(b.str, function (_, _) callNum++); + b.str = ""; + assertEquals(callNum, 2); + + b.strChanged.removeAll(); + b.str = "1"; + assertEquals(callNum, 2); + + b.strChanged.dispose(); + var addError = false; + try { + b.strChanged.add(function (_, _) {}); + } catch (e:Dynamic) { + addError = true; + } + assertTrue(addError); + } + + function test4() { + var b = new Bindable1(); + b.str = null; + var callNum = 0; + var listener = function (from, to) { + assertEquals(from, "1"); + assertEquals(to, "2"); + callNum ++; + } + b.strChanged.add(listener); + b.strChanged.dispatch("1", "2"); + assertEquals(callNum, 1); + + bindx.Bind.notify(b.str, "1", "2"); + assertEquals(callNum, 2); + } + + function test5() { + var b = new Bindable1(); + b.str = null; + var callNum = 0; + b.bindChanged.add(function () callNum++); + + b.i = 10; + assertEquals(callNum, 1); + assertFalse(Reflect.hasField(b, "noBindChanged")); + + bindx.Bind.notify(b.bind); + assertEquals(callNum, 2); + } +} + +@:bindable +class Bindable1 implements bindx.IBindable { + + + public var str:String; + + @:bindable + public var i(default, set):Int; + + @:bindable + private var privateVar:Bool; + + public function new() { + if (this.privateVarChanged == null) + throw "no private binding"; + } + + function set_i(v) { + i = v; + bindx.Bind.notify(this.bind); + return v; + } + + public function noBind() { + + } + + @:bindable + public function bind() { + + } +} \ No newline at end of file diff --git a/test/InheritanceTest.hx b/test/InheritanceTest.hx new file mode 100644 index 0000000..6866173 --- /dev/null +++ b/test/InheritanceTest.hx @@ -0,0 +1,47 @@ +package ; + +import bindx.IBindable; + +class InheritanceTest extends haxe.unit.TestCase { + public function new() { + super(); + } + + function testChild() { + var c = new BindableChild(); + c.i = 0; + c.s = "0"; + var iChanged = 0; + c.iChanged.add(function (from, to) { + assertEquals(from, 0); + assertEquals(to, 1); + iChanged ++; + }); + c.i = 1; + assertEquals(iChanged, 1); + + var sChanged = 0; + c.sChanged.add(function (from, to) { + assertEquals(from, "0"); + assertEquals(to, "1"); + sChanged ++; + }); + c.s = "1"; + assertEquals(sChanged, 1); + } + + +} + +@:bindable +class BindableParent implements IBindable { + public function new() {} + + public var i:Int; +} + +@:bindable +class BindableChild extends BindableParent { + + public var s:String; +} \ No newline at end of file diff --git a/test/TestProperty.hx b/test/TestProperty.hx new file mode 100644 index 0000000..bb00e80 --- /dev/null +++ b/test/TestProperty.hx @@ -0,0 +1,48 @@ +package ; + +class TestProperty extends haxe.unit.TestCase { + public function new() { + super(); + } + + function test1() { + var p = new BindableProperty(); + p.s = "1"; + var callNum = 0; + + p.sChanged.add(function (from, to) { + assertEquals(from, "1"); + assertEquals(to, ""); + callNum ++; + }); + + p.s = null; + + p.sChanged.removeAll(); + + p.sChanged.add(function (from, to) { + assertEquals(from, ""); + assertEquals(to, "1"); + callNum ++; + }); + p.s = "1"; + + assertEquals(callNum, 2); + } +} + +class BindableProperty implements bindx.IBindable { + public function new() { + } + + @:bindable + public var s(default, set):String; + + function set_s(v) { + if (v == null) { + return s = ""; + } + s = v; + return v; + } +} \ No newline at end of file diff --git a/test/Tests.hx b/test/Tests.hx new file mode 100644 index 0000000..7234740 --- /dev/null +++ b/test/Tests.hx @@ -0,0 +1,14 @@ +package ; + +import haxe.unit.TestRunner; + +class Tests { + + static function main() { + var runner = new TestRunner(); + runner.add(new BaseTest()); + runner.add(new InheritanceTest()); + runner.add(new TestProperty()); + runner.run(); + } +} \ No newline at end of file