Skip to content

Commit 36f18dc

Browse files
committed
[WIP] Align implementation of bind({path}) and bind({all})
1 parent e90fd70 commit 36f18dc

File tree

9 files changed

+307
-310
lines changed

9 files changed

+307
-310
lines changed

doc/databinding/@bind.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,6 @@ In JavaScript the only difference is how - if at all - the decorated property is
4949
myNumber;
5050
```
5151

52-
Change events are fired for the decorated *component property* when the *target element* fires change events.
53-
54-
> See example apps ["bind-two-way-change-events"](../../examples/bind-two-way-change-events) (TypeScript) and ["bind-two-way-change-events-jsx"](../../examples/bind-two-way-change-events-jsx) (JavaScript/JSX).
55-
56-
As with one-way bindings, setting the *component property* to `undefined` resets the *target property* to its initial value for when the binding was first established.
57-
5852
## @bind(config)
5953

6054
Like [`@bind(path)`](#bindpath) or [`@bindAll(bindings)`](./@bindAll.md), but allows to give additional options as supported by [`@property`](./@property.md).
@@ -104,3 +98,20 @@ A [`typeGuard`](./@property.md#configtypeguard) may be given to perform value ch
10498

10599
A [`type`](./@property.md#configtype) may be given to enforce type checks in JavaScript.
106100

101+
### Properties eligible for bindings
102+
103+
Any *component property* can be used for two-way bindings, unless it's explicitly implemented with a setter and getter, or with `Object.defineProperty`. These are not supported. The target property needs to generate change events for the two-way binding to work. This is already the case for all built-in properties of Tabris.js widgets.
104+
105+
> See example apps ["bind-two-way-change-events"](../../examples/bind-two-way-change-events) (TypeScript) and ["bind-two-way-change-events-jsx"](../../examples/bind-two-way-change-events-jsx) (JavaScript/JSX).
106+
107+
If the target widget itself is a custom component the recommended way to implement change events is using [`@property`](./@property.md). Note that there is no need to explicitly create an event API, `@bind` can 'talk' directly to `@property`. However, an explicit implementation is also possible.
108+
109+
## Edge Cases
110+
111+
As with one-way bindings, setting the *component property* to `undefined` resets the *target property* to its initial value for when the binding was first established. The component property will also adopt that value, so both stay in syc.
112+
113+
If the *component property* converts or ignores the incoming value of the *target property*, the target property will follow and also bet set to the new component property value.
114+
115+
If a *target property* converts or ignores the incoming value of the *component property*, the component property will ignore that and keep its own value. The two properties are out-of-sync in this case.
116+
117+
If either property throws when set, the error will be propagated to the caller that originally caused the value change. In this case the two properties *may* end up out-of-sync.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"build": "webpack --mode=production && npm run dts",
1414
"dts": "dts-generator --name tabris-decorators --project ./tsconfig-dts.json --out ./dist/index.d.ts --main tabris-decorators/index",
1515
"test": "npm run lint && ts-mocha -p ./tsconfig-test.json ./test/*.spec.ts ./test/*.spec.tsx ./test/*.spec.js ./test/*.spec.jsx",
16-
"test:file": "ts-mocha -p ./tsconfig-test.json",
16+
"test:file": "ts-mocha -p ./tsconfig-test.json --bail",
1717
"lint": "eslint --color --f visualstudio --ext .js,.jsx,.ts,.tsx src test examples",
1818
"safePublish": "npm test && npm run build && npm publish",
1919
"start": "run-script-os",

src/decorators/bind.ts

Lines changed: 56 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import {Composite} from 'tabris';
22
import {property} from './property';
33
import {Injector, injector} from '../api/Injector';
4-
import {createBoundProperty} from '../internals/createBoundProperty';
5-
import {processTwoWayBindings, TwoWayBindingPaths} from '../internals/processTwoWayBinding';
4+
import {initAllTwoWayBindings, initSingleTwoWayBinding} from '../internals/processTwoWayBinding';
65
import {applyDecorator, getPropertyType, isPrimitiveType} from '../internals/utils';
7-
import {checkIsComponent, checkPropertyExists, isUnchecked, parseTargetPath, postAppendHandlers, TypeGuard, UserType, WidgetInterface} from '../internals/utils-databinding';
6+
import {checkIsComponent, checkPropertyExists, isUnchecked, parseTargetPath, postAppendHandlers, TargetPath, TypeGuard, UserType, WidgetInterface} from '../internals/utils-databinding';
87

98
export interface BindAllConfig<ValidKeys extends string> {
109
typeGuard?: TypeGuard;
@@ -21,15 +20,19 @@ export interface BindSingleConfig {
2120
path: string;
2221
}
2322

24-
export type TwoWayBinding = {
25-
baseProto: WidgetInterface,
26-
baseProperty: string,
27-
path: string | null,
23+
export type BindSuperConfig = {
24+
componentProto: WidgetInterface,
25+
componentProperty: string,
26+
targetPath: TargetPath | null,
2827
all: TwoWayBindingPaths | null,
2928
typeGuard: TypeGuard | null,
3029
userType: UserType<any> | null
3130
};
3231

32+
export type TwoWayBindingPaths = {
33+
[sourceProperty: string]: TargetPath
34+
};
35+
3336
export type BindAllDecorator<ValidKeys extends string> = <
3437
PropertyName extends string,
3538
Target extends {[P in PropertyName]: {[SubProperty in ValidKeys]: any}} & Composite
@@ -81,51 +84,52 @@ export function bind<ValidKeys extends string>(config: BindAllConfig<ValidKeys>)
8184
export function bind(...args: any[]): any {
8285
return applyDecorator('bind', args, (baseProto: WidgetInterface, baseProperty: string) => {
8386
const isShorthand = typeof args[0] === 'string';
84-
const binding: TwoWayBinding = {
85-
baseProto,
86-
baseProperty,
87-
path: isShorthand ? args[0] : args[0].path,
87+
const pathString = isShorthand ? args[0] : args[0].path;
88+
const binding: BindSuperConfig = {
89+
componentProto: baseProto,
90+
componentProperty: baseProperty,
91+
targetPath: pathString ? parseTargetPath(pathString) : null,
8892
all: parseAll(isShorthand ? null : args[0].all),
8993
typeGuard: isShorthand ? null : args[0].typeGuard,
9094
userType: isShorthand ? null : args[0].type
9195
};
9296
checkParameters(binding);
93-
applyTwoWayBinding(binding);
94-
setTimeout(() => {
95-
try {
96-
checkIsComponent(baseProto);
97-
} catch (ex) {
98-
console.error('Can not apply @bind to property ' + baseProperty, ex);
99-
}
100-
});
97+
preCheckComponentProperty(binding);
98+
configureComponentProperty(binding);
99+
postAppendHandlers(binding.componentProto).push(createInitializer(binding));
100+
scheduleIsComponentCheck(binding);
101101
});
102102
}
103103

104-
function applyTwoWayBinding(binding: TwoWayBinding) {
105-
if (binding.path) {
106-
createBoundProperty(binding);
107-
} else {
108-
checkBasePropertyType(binding);
109-
const propertyConfig = {
110-
typeGuard: createTypeGuard(binding),
111-
type: binding.userType
112-
};
113-
property(propertyConfig)(binding.baseProto, binding.baseProperty);
114-
postAppendHandlers(binding.baseProto).push(base => processTwoWayBindings(base, binding));
115-
}
104+
function configureComponentProperty(binding: BindSuperConfig) {
105+
const propertyConfig = {
106+
typeGuard: binding.all ? createBindAllTypeGuard(binding) : binding.typeGuard,
107+
type: binding.userType
108+
};
109+
property(propertyConfig)(binding.componentProto, binding.componentProperty);
116110
}
117111

118-
function checkParameters(binding: TwoWayBinding) {
119-
if (binding.path && binding.all) {
112+
function createInitializer(binding: BindSuperConfig): (instance: WidgetInterface) => void {
113+
return binding.all
114+
? base => initAllTwoWayBindings(base, binding)
115+
: base => initSingleTwoWayBinding(base, binding);
116+
}
117+
118+
function checkParameters(binding: BindSuperConfig) {
119+
if (binding.targetPath && binding.all) {
120120
throw new Error('@bind can not have "path" and "all" option simultaneously');
121121
}
122-
if (!binding.path && !Object.keys(binding.all).length) {
122+
if (!binding.targetPath && !Object.keys(binding.all).length) {
123123
throw new Error('Missing binding path(s)');
124124
}
125125
}
126126

127-
function checkBasePropertyType(binding: TwoWayBinding) {
128-
const {baseProto, baseProperty, userType} = binding;
127+
function preCheckComponentProperty(binding: BindSuperConfig) {
128+
if (binding.targetPath) {
129+
// Will be checked on initialization
130+
return;
131+
}
132+
const {componentProto: baseProto, componentProperty: baseProperty, userType} = binding;
129133
const type = userType || getPropertyType(baseProto, baseProperty);
130134
if (isPrimitiveType(type)) {
131135
throw new Error('Property type needs to extend Object');
@@ -143,15 +147,15 @@ function parseAll(all: {[key: string]: string}): TwoWayBindingPaths | null {
143147
return bindings;
144148
}
145149

146-
function createTypeGuard(binding: TwoWayBinding) {
150+
function createBindAllTypeGuard(binding: BindSuperConfig) {
147151
const sourceProperties = Object.keys(binding.all);
148-
const baseProperty = binding.baseProperty;
152+
const baseProperty = binding.componentProperty;
149153
return (value: any) => {
150154
if (value) {
151155
if (!(value instanceof Object)) {
152156
throw new Error('Value needs to extend Object');
153157
}
154-
const className = binding.baseProto.constructor.name;
158+
const className = binding.componentProto.constructor.name;
155159
for (const sourceProperty of sourceProperties) {
156160
checkPropertyExists(value, sourceProperty, 'Object');
157161
if (isUnchecked(value, sourceProperty)) {
@@ -171,3 +175,16 @@ function createTypeGuard(binding: TwoWayBinding) {
171175
return binding.typeGuard ? binding.typeGuard(value) : true;
172176
};
173177
}
178+
179+
function scheduleIsComponentCheck(binding: BindSuperConfig) {
180+
setTimeout(() => {
181+
try {
182+
checkIsComponent(binding.componentProto);
183+
} catch (ex) {
184+
const target = binding.all ? JSON.stringify(binding.all) : binding.targetPath.join('.');
185+
console.error(
186+
`Binding "${binding.componentProperty}" <-> "${target}" failed to initialize: ` + ex.message
187+
);
188+
}
189+
});
190+
}

0 commit comments

Comments
 (0)