-
-
Notifications
You must be signed in to change notification settings - Fork 104
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鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(BindableProperty): Reflect prop to attribute #560
base: master
Are you sure you want to change the base?
Conversation
@AshleyGrant If the original request was to have the ability to reflect the prop to every element bound, not just the custom element itself, maybe it better suits a new binding command <div prop.attr='someThing'></div>
<div prop.bind='someThing & reflectToAttribute'></div> |
src/bindable-property.js
Outdated
@@ -35,7 +35,13 @@ export class BindableProperty { | |||
* Creates an instance of BindableProperty. | |||
* @param nameOrConfig The name of the property or a cofiguration object. | |||
*/ | |||
constructor(nameOrConfig: string | Object) { | |||
constructor(nameOrConfig: string | { |
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 for typing that Object
!
Should we have a named interface for that rather? BindableOptions
? That would make the code cleaner + give a place to comment on members... I know what attribute
does but it's not totally obvious, especially now with reflect
in the mix.
src/bindable-property.js
Outdated
@@ -35,7 +35,13 @@ export class BindableProperty { | |||
* Creates an instance of BindableProperty. | |||
* @param nameOrConfig The name of the property or a cofiguration object. | |||
*/ | |||
constructor(nameOrConfig: string | Object) { | |||
constructor(nameOrConfig: string | { | |||
defaultBindingMode?: number, |
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.
number
:(
Don't we have an enum to match? Can we create one?
src/bindable-property.js
Outdated
constructor(nameOrConfig: string | Object) { | ||
constructor(nameOrConfig: string | { | ||
defaultBindingMode?: number, | ||
reflect?: boolean | {(el: Element, name: string, newVal, oldVal): any}, |
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 bike-shedding but I'm not a fan of the name.
reflect
is a little too generic for my taste, makes me think of Reflect
first.
Polymer uses, reflectToAttribute
, maybe we should play along? If someone sees this pop up in IntelliSense, its meaning is somewhat clear. reflect
not as much.
src/bindable-property.js
Outdated
constructor(nameOrConfig: string | Object) { | ||
constructor(nameOrConfig: string | { | ||
defaultBindingMode?: number, | ||
reflect?: boolean | {(el: Element, name: string, newVal, oldVal): any}, |
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'm not convinced by the API design of the function part.
It feels like it takes too much... the element and old value and a name (of what)?
This is meant to just reflect a value in an attribute. The callback here really feels like changeHandler
. Accepting a bool | string
with the string allowing to reflect to a different attribute name seems simpler.
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 we restricted it to only choose the name, whjat would we do if it is not always 1:1 prop->attr ?
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.
Do you mean like one binding updates 2 attributes? For example condition
boolean property is reflected into the presence of either a true
or a false
attribute on target element?
Personally I would say this is narrow use cases that can be filled today by using changeHandler
, which has practically the same signature.
Making the API surface larger and more complex means a bigger Aurelia, more docs and more stuff to support and maintain in the future.
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's the case where value of property is an object, we dont want [object Object]
in the attribute.
Another motivation, is that pretty much everything in Aurelia is intercept-able, So I would choose go with the rest on this.
But we can limit it by accepting a function that will take value input an return a string, instead of passing in the whole 4 params like what I did. Can call it serializer
src/bindable-property.js
Outdated
if (descriptor) { | ||
this.descriptor = descriptor; | ||
return this._configureDescriptor(descriptor); | ||
} | ||
|
||
return undefined; | ||
} | ||
|
||
_configureReflection(target) { | ||
if (target.__reflectionConfigured__) return; |
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.
Aurelia includes a Symbol
polyfill.
Is the __xxx__
field how we want to roll when hacking into objects we don't own? Shouldn't we use symbols instead?
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.
Some thing I couldn't think of. Can only 馃憤
src/bindable-property.js
Outdated
if (hasHandler) { // avoid ternary to make it consisten with the rest ? | ||
alteredHandler = function propertyChanged(name, newVal, oldVal) { | ||
onChanged.call(this, name, newVal, oldVal); | ||
let { __element__, __reflections__ } = this.__observers__; |
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.
Not sure, so just asking: is accessing __observers__
directly the way to go?
Isn't that an implementation detail of observation and an API should be used here rather?
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 one is really silly of mine, was too hasty to see this. 馃憤
How would you fix this though, there is already getObserver
in the scope, in which it access __observers__
directly. Maybe i should create getObserverLookup
and point getObserver
there. Or just leave getObserver
alone to avoid performance cost of fn calling ?
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 did not see that getObserver
function. I think the right thing to do is to use it to not break the abstraction level here.
If you want to maximize perfs and make sure getObserver
gets optimized by JIT you can split it in two so that it is small and only contains the happy path (lookup !== undefined
) and put the initialization code in a second function.
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 I will move the whole block of reflection out of that function, to a separate fn
Great comments from @jods4! |
src/bindable-property.js
Outdated
configurable: true, | ||
value: alteredHandler | ||
})) { | ||
throw new Error(`Cannot setup property reflection on <${this.name}/> for ${target.name}`); |
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 we go for throw
on failure, why don't use Object.defineProperty
. Unlike Reflect
it exists on older browsers, does the same thing, and throws when it fails.
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 because my lack of knowledge about compiler result. I didn't know if 'use strict'
will always be there so i tried to do something.
src/bindable-property.js
Outdated
* @param {any} newValue | ||
*/ | ||
function propToAttr(element, propertyName, newValue) { | ||
if (newValue == null) { |
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 like this, but I think the rest of the team is linting for strict ===
always.
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 will be fixed. Was trying to roll it out so a bit lazy for ||
and ===
馃槃
src/bindable-property.js
Outdated
* @param {string} propertyName | ||
* @param {any} newValue | ||
*/ | ||
function propToAttr(element, propertyName, 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.
It seems to me this doesn't take into account the camelCase to kebab-case between properties and html attributes?
Also in HTML a few weird properties do not map directly to same-name attribute. I think we have a kind of map somewhere in Aurelia that converts some of those so that it "just work" when binding HTML attributes, shouldn't we re-use it here so that it just works for those?
Should this API interact with the attribute
binding option? It allows users to map a property to a different name attribute, wouldn't they expect reflectToAttribute
to take that into account? Seems more intuitive.
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.
No camelCase to kebab-case is a bad mistake. It should be fixed.
Code for the special pairs belong to templating-binding
, should I copy it over ? I'm not sure how to handle this, because props of custom element in Aurelia, donot necessarily map to attribute like built-in props.
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. This is for custom attributes, it has nothing to do with built-in HTML attributes.
Forget my remark about the 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.
For the attribute
, it sounds really good. Maybe i'll go for it and update the PR doc.
src/html-behavior.js
Outdated
if (propertyName in reflections) { | ||
throw new Error(`Reflection for ${propertyName} was already registered`); | ||
} | ||
if (typeof instruction !== 'function') { |
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.
Shouldn't parameter validation go first, before mutating this
?
Judging by the _
prefix, this is a private API, do we validate parameters on private APIs?
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 should be removed.
src/html-behavior.js
Outdated
} | ||
|
||
/** | ||
* @param {Element} element |
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.
Why use Typescript-like type annotations in bindable-property.js
and JSDoc comments here?
I had a quick look at the code. Not enough to validate the PR but the main mechanic looks good to me. I am a little confused when you say it addresses aurelia/binding#347. What/how does it do that, I think I missed something? <input magical-bool-binding /> As described in this comment aurelia/binding#347 (comment) |
Thanks @jods4 , throughout as expected.
|
The thing about aurelia/binding#347 is that it's not just about updating attributes when the property changes. It's also about having a shortcut notation where I think I don't fully grasp what use cases you want to address by allowing |
I totally missed that point of original request. I don't have a use case for Edit 1 Not 1:1 always is for the case of object,toString, it will become |
@bigopon let's see what others think of the proposed function My opinion is that we shouldn't introduce complexity unless it has strong pay-off. In this case:
So my vote would be to wait. Ship |
src/bindable-property.js
Outdated
* @param newValue | ||
* @param oldValue | ||
*/ | ||
function callRefelection(instance: Object, propertyName: string, newValue, oldValue) { |
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.
Typo here: Refelection
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/bindable-property.js
Outdated
if (newValue == null) { | ||
element.removeAttribute(propertyName); | ||
function propToAttr(element: Element, propertyName: string, newValue: any) { | ||
if (newValue === null || newValue === void 0) { |
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.
OK I might be nitpicking but I prefer newValue === undefined
, for clarity.
Minifiers will do the job of using the smallest code.
I know users could redefine undefined
but a lot of other parts won't work if they do.
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 was trying to do minifier job, will avoid next PRs 馃憤
src/bindable-property.js
Outdated
let hasHandler = !!onChanged; | ||
|
||
let alteredHandler; | ||
if (hasHandler) { // avoid ternary to make it consisten with the rest ? |
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 thought about this part some more...
It does something very similar to createObserver
(same file).
Wouldn't it be more efficient and more clear to integrate callReflection
inside createObserver
?
So this doesn't play nice with lifted custom elements ( |
@jods4 Thanks for your review, and I'm thinking of closing this PR. since it doesn't really add much value, like you suggested. (Wonder if you have thought of any scenarios this could be useful beside saving few lines of code). And after realized I failed to address Ashley's case making me agree with you more. I feel like there is no point adding this once, as it is a bit complex to deal with inheritance and if anyone ever wanted this, it would just be a simple injection and reflect in But I'd like to add that about your point of complex function as a value for Maybe @EisenbergEffect can put a some thoughts before we close this PR ? |
@bigopon I don't know why I gave you the impression that this PR "doesn't really add much value". I honestly never intended that. I personally think that the function parameter to But as for I also like the extra typings that you put in this PR. It makes it much easier to work with Aurelia code. 鉂わ笍 |
I think an Before this is closed, are there any use-cases or pain points that this |
Part of this addresses the issue with making bindable properties have the same capability as built-in elements, the potential to automatically reflect property changes to attributes. This includes boolean attributes (bindables that add/remove the attribute from the element altogether), which we need for the UX library. If we add the attr command, that would need to work with bindable properties, since there's little reason to create a bindable where you don't want the property to be updated. So, it's not just an attr update in this case. Beyond that, attr reflection should be independent of one-way or two-way capability. I'm not sure how this would be handled, even though it's a rare case. |
Also: this feature is copied from Polymer. |
Apologies- when I was reading the discussion I got the impression that @bigopon was closing this. Still not clear on the use-case though. I see polymer has it, what's the purpose in the aurelia context? To enable targeting states with css attribute selectors? What's the aurelia-ux use-case? |
Yes, that's definitely one of them. |
I was thinking of it, as I'm not sure if it's worth the lines. @jdanyow Also I was concerned by the fact that there may be confusion in the Maybe we can have type ReflectInstruction = string | {
attr: string,
converter: (newValue, oldValue) => any
}
reflectToAttribute: ReflectInstruction |
I've followed @jods4 's suggestion. // also support (element, attrName, value): any
// the reason is object -> [object Object] when calling toString
// Or should it just be gone ?
@bindable({
reflectToAttribute: 'boolean' | 'string',
attribute: 'given-name' // this means prop firstName will be reflected to given-name
})
firstName |
Does this work in both directions? If someone calls |
Is it this feature request #458 you are referencing ? It's would be nice to have, but @EisenbergEffect didn't give any comment about resolution. I think you commented somewhere in some related issue about using |
This PR addresses:
Aurelia should support boolean values for attributes.聽binding#347@AshleyGrant (sorry Ashley 馃槃 )Related:
Note:
containerless
elementnull
/undefined
value, so to achieve add / remove attribute behavior, setnull
/undefined
. This is to align with normal behavior of special built-in properties. Another way is to create a common instruction, where it treat empty string /false
values as a sign to remove attribute.Usage with
bindable
decorator:cc @EisenbergEffect @jdanyow