-
-
Notifications
You must be signed in to change notification settings - Fork 102
WIP feat(bindable): support coercing value #558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@EisenbergEffect if this passes, it will be trivial to support your proposal: class MyViewModel {
@bindable.number numProp
} |
|
Cool. The other thing we'd want to do is support TypeScript metadata, so if they do this... ...we should pick up the metadata and map it to a coerces function as well. |
|
I have added the support for Typescript. Now it supports this: import {mapCoerceForClass} from 'aurelia-templating';
class Point {}
mapCoerceForClass(Point, 'point', function(value) {
return new Point(value.split(' ').map(parseFloat));
});
class LineViewModel {
point1: Point
point2: Point
}Basically you can register how to coerce a class. JavaScript users can also leverage this by setting metadata on property: import {mapCoerceForClass} from 'aurelia-templating';
class Point {}
mapCoerceForClass(Point, 'point', function(value) {
return new Point(value.split(' ').map(parseFloat));
});
class LineViewModel {
@bindable
@Reflect.metadata('design:type', Point)
point1
@bindable
@Reflect.metadata('design:type', Point)
point2
}@EisenbergEffect one thing I'm not sure & confident about is naming of Code for /**@type {Map<Function, string>} */
export const classCoerceMap = new Map([
[Number, 'number'],
[String, 'string'],
[Boolean, 'boolean'],
[Date, 'date']
]);
/**
* Map a class to a string for typescript property coerce
* @param Class {Function} the property class to register
* @param strType {string} the string that represents class in the lookup
* @param converter {function(val)} coerce function tobe registered with @param strType
*/
export function mapCoerceForClass(Class, strType, coerce) {
if (typeof strType !== 'string' || typeof coerce !== 'function') {
LogManager.warn(`Bad attempt at mapping class: ${Class.name} to type: ${strType}`);
}
coerces[strType] = coerce;
coerceClassMap.set(Class, strType);
}It can be even more convenient, if in function class Point {
static coerce(value) {
return new Point(value.split(' ').map(parseFloat));
}
}
// Usage:
mapCoerceForClass(Point, 'point');If static // Decorator usage
@mapCoerceForClass('point')
class Point {
static coerce(value) {
return new Point(value.split(' ').map(parseFloat));
}
}
// Or normal usage:
mapCoerceForClass(Point, 'point'); |
|
Update: sorry I misread it. time for a nap i guess 😢 |
|
Don't worry too much about the api yet. We can tweak that. If you'd like to keep working on this (awesome work btw!) then I'm wondering if you can investigate the binding library's |
src/behavior-property-observer.js
Outdated
| const numCons = Number; | ||
| const dateCons = Date; | ||
| const _isFinite = isFinite; | ||
| const _isNaN = isNaN; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bigopon, could you explain the purpose of these 4 aliases - it looks like adding indirection that makes readability worse without obvious benefits
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is expected that those functions will be called with high frequency, so it is better to put them in the same scope with the calling function. This is to work around bundlers, which will add wrap those functions in many scope layers containing maybe up to hundred of enumerable accessors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's skip this for now and re-visit if we see an issue with bundlers. Makes the code less readable.
src/behavior-property-observer.js
Outdated
| }; | ||
|
|
||
| /**@type {Map<Function, string>} */ | ||
| export const classCoerceMap = new Map([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that instead of the two lines above You can write smth like:
export const classCoerceMap: Map<Function, string> = new Map([
or even better (more precise type):
export const classCoerceMap: Map<(strValue: string) => any, string> = new Map([
as it would be correct way to write type information for TypeScript variables/constants, and at the moment some Babel plugin is used to extract this information to generate API docs and definitions files for TS.
My only concern is that maybe the Babel plugin that extracts TS types doesn't understand so complex TS type expressions as i wrote in the second example
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would also be beneficial when converting this repo to TS (as i understand, some Aurelia repos are already written in TypeScript and some are in middle of transitioning to TS)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right about this, could have inlined type def.
The purpose of this variable is to hold a map between Classes and their coerce type (as string) in real look up table of coerces for later use. In other word, string type is the source of truth. So the type would be Map<Function, string>
src/behavior-property-observer.js
Outdated
| * @param strType {string} the string that represents class in the lookup | ||
| * @param converter {function(val)} coerce function tobe registered with @param strType | ||
| */ | ||
| export function mapCoerceForClass(Class, strType, coerce) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps type information could be moved next to parameters from function documentation - smth like
export function mapCoerceForClass(Class: Function, strType: string, coerce: (strValue: string) => any) {
|
@bigopon, nice work! I added couple of comments tough |
src/behavior-property-observer.js
Outdated
| this.publishing = false; | ||
| this.selfSubscriber = selfSubscriber; | ||
| this.currentValue = this.oldValue = initialValue; | ||
| if (typeof coerce !== 'undefined') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (coerce !== undefined) { (rest of codebase doesn't care about crazy people remapping undefined. 😉 )
src/behavior-property-observer.js
Outdated
| const numCons = Number; | ||
| const dateCons = Date; | ||
| const _isFinite = isFinite; | ||
| const _isNaN = isNaN; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's skip this for now and re-visit if we see an issue with bundlers. Makes the code less readable.
src/behavior-property-observer.js
Outdated
| return a; | ||
| }, | ||
| number(a) { | ||
| var val = numCons(a); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const
src/behavior-property-observer.js
Outdated
| * Map a class to a string for typescript property coerce | ||
| * @param Class {Function} the property class to register | ||
| * @param strType {string} the string that represents class in the lookup | ||
| * @param converter {function(val)} coerce function tobe registered with @param strType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tobe -> to
src/behavior-property-observer.js
Outdated
| */ | ||
| setValue(newValue: any): void { | ||
| let oldValue = this.currentValue; | ||
| let realNewValue = this.coerce ? this.coerce(newValue) : newValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const coercedValue = this.coerce === undefined ? newValue : this.coerce(newValue);
src/behavior-property-observer.js
Outdated
| const _isFinite = isFinite; | ||
| const _isNaN = isNaN; | ||
|
|
||
| export const coerces = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
coercionFunctions ?
src/behavior-property-observer.js
Outdated
| }; | ||
|
|
||
| /**@type {Map<Function, string>} */ | ||
| export const classCoerceMap = new Map([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe name coercionFunctionMap?
src/behavior-property-observer.js
Outdated
| * @param strType {string} the string that represents class in the lookup | ||
| * @param converter {function(val)} coerce function tobe registered with @param strType | ||
| */ | ||
| export function mapCoerceForClass(Class, strType, coerce) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about:
mapCoercionFunction(type: { new:(): any; }, typeName: string, coercionFunction: (value: string) => any)
| if (propType) { | ||
| nameOrConfigOrTarget = classCoerceMap.get(propType) || 'none'; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this turning on coercion by default in any project that uses TypeScript with emit decorator metadata?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to have a setting that allows people to opt into this. Turning it on automatically, at this point, would be a breaking change to people's apps.
|
I have:
Please let me know if I missed something. |
jdanyow
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good... couple minor feedback items. The other thing we'll need are docs and updates to aurelia-binding.d.ts interfaces (unlike the other repos this one is hand written).
src/behavior-property-observer.js
Outdated
| setValue(newValue: any): void { | ||
| let oldValue = this.currentValue; | ||
| const oldValue = this.currentValue; | ||
| const coercedValue = this.coerce !== undefined ? this.coerce(newValue) : newValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit pick- positive case first please. this.coerce === undefined ? newValue : ....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/behavior-property-observer.js
Outdated
| case 'string': | ||
| c = coerceFunctions[coerce]; break; | ||
| default: break; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit pick- does the indentation of the switch block pass the lint rules?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't see it in .eslint.json in aurelia-tools. When I was doing this, I remembered I got error in linting for not indenting the case. And I tested with 1 more indent, it showed error. I think it's following the rule.
| default: break; | ||
| } | ||
| if (c === undefined) { | ||
| LogManager |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could this be put in the default: switch case? Then move the this.coerce assignment into the function and string cases, enabling removal of the let c?
|
I have added typings for I haven't added typings for I don't know if this is the final desired behavior, as currently it still doesn't play nice together with two-way binding. Basically it can't be synced well with input, for current binding notification model. And it would take not trivial amount work to support it properly, consider syncing between a What do you think ? Edit: observable pr adds the base for |
|
@jdanyow @EisenbergEffect doc is ready too. oh docs ...
observable.usePropertyType(true);
bindable.usePropertyType(true);Maybe a |
atsu85
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fantastic work @bigopon. I added few comments tough :)
|
|
||
| #### Usage with metadata for Typescript | ||
|
|
||
| Typescript compiler has an option emit class fields with their types in metadata. `bindable` decorator can work with this via `usePropertyType` function, which is a property of `bindable` decorator: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps instead of "an option emit" you meant "an option to emit"?
|
|
||
| <code-listing heading="extend-coerce"> | ||
| <source-code lang="TypeScript"> | ||
| // import from 'aurelia-binding' if you are writing a plugin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it explained somewhere else, why plugins should import things from original package instead of aurelia-framework that exports things from other packages? I guess they both work, but 'aurelia-binding' should be preferred to avoid excessive dependencies (and to be tree-shaking friendly)?
|
|
||
| #### Fluent syntax | ||
|
|
||
| If you scroll back top a bit, you will notice we had fluent syntax decorator: `@bindable.number`. This, as described above, is a simplified and more expressive form of `coerce: 'number'`. Aurelia also provides a way to build your own fluent syntax `bindable` decorator. You can do this by: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps instead of
"If you scroll back top a bit"
following would be better:
"If you scroll back up a bit"
| // Add `point` coerce function | ||
| coerceFunctions.point = function(incomingValue) { | ||
| return incomingValue.split(' ').map(parseFloat); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhapse there should be a helper to simplify registering fluent coerce syntax? For example instead of
import {coerceFunctions, coerceFunctionMap, createTypedBindable} from 'aurelia-framework';createTypedBindable(..)coerceFunctionMap.set(...)coerceFunctions.... = function...
it could be done for example like this:
import {coerceHelper} from 'aurelia-framework';
coerceHelper.addFuluentBindableExtension('point', Point, (incomingValue) => {
return incomingValue.split(' ').map(parseFloat);
});There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I was feeling this is too much call too, for API designed, I think @jdanyow @EisenbergEffect will need to give call on this.
@atsu85 Thanks for helping, I ran out of energy half way through the docs 😱 , many typos and unneeded stuff
|
@bigopon Can you give me an update on where we are at on this. Seems like it stalled out. I'd love to have types bindables/observables though. |
|
This looks really awesome, @bigopon - great work, and much appreciated! I do have a couple of concerns though:
Now, we might need options to opt-out of some of this default behavior until app code is updated, but I think that will be the best long-term strategy, as opposed to littering the app code with options to opt-in on every bindable property. Thoughts, @bigopon, @EisenbergEffect? |
Are you referring to
View model properties are not supposed to be used with view only, but also interacted within JavaScript / TypeScript. so by default I don't think we can treat
It could be nice if we can avoid it, but for me, value converter are somewhat view related logic, while typed
It's is being discussed here #560 . Ideally we should have it reviewed after this PR is decided to be merged or not
It depends on the target of the binding, if it's a custom element / custom attribute, then it works well and as expected, except some cases like gotchas in the observable PR. For Html built-ins, value are always coerced to string so it's pretty much nothing we can do. |
|
@EisenbergEffect, @jdanyow - i've been really dreaming about this feature since the beginning. Contrary to what @thomas-darling said, this feature is specially useful and easy to use with TypeScript, as reflect metadata is available at runtime and the fact that we'd need to call Fantastic work @bigopon, i hope it will be merged soon! |
|
I have some refactor suggestions, getting inspiration from the validation plugin. So here are my suggestions |
| <source-code lang="ES 2015"> | ||
| export class NumberField { | ||
| @bindable label | ||
| @bindable({ coerce: val => Number(val) }) value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be @bindable({ coerce: Number }) value or is that confusing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks I'll fix it the next commit 👍
|
Since For anyone interested in this feature: |

This PR add support for coercing value, before calling
setValueon property, to address the issue: #96Update:
coerceFunctionsfrom'aurelia-binding'Usage is in observable PR
Details
Usage is similar to @jdanyow 's proposal
To modify / extends the built-in coerce types:
To add more coerce types:
All basic implementations:
To avoid breaking changes/ bug, any fail attempt to get coerce function will result into a identity function, which is
coerces.nonecc @EisenbergEffect @jdanyow