-
Notifications
You must be signed in to change notification settings - Fork 15
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
Refactor properties mixins. #13
Conversation
0d91477
to
83913f0
Compare
mixins/computed-properties-mixin.js
Outdated
* Provides observation and computation hooks for properties. | ||
*/ | ||
|
||
// TODO: Delay first computation until at least one dependency is defined. |
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 didn't take on this change here since it would cause a compatibility issue. We should discuss what the appropriate behavior is though.
mixins/property-effects-mixin.js
Outdated
@@ -0,0 +1,12 @@ | |||
/** | |||
* Encodes the order that property effects should take place in. |
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 having a compositing layer here is helpful. Note that reflection is treated here separately. This allows us to easily encode that upon an update (1) compute properties that are now invalid (2) call observers (3) reflect properties. I'm happy to debate this order, but I would like to keep these effects as separate files.
test/test-observed-properties.js
Outdated
@@ -19,21 +19,21 @@ suite('x-element observed properties', async ctx => { | |||
it( | |||
'observers are called when properties change', | |||
JSON.stringify(el.changes) === | |||
'[{"property":"a","newValue":"oh"},{"property":"b","newValue":"hai"},{"property":"c","newValue":"oh hai"},{"property":"b","newValue":"hey","oldValue":"hai"},{"property":"c","newValue":"oh hey","oldValue":"oh hai"}]' | |||
'[{"property":"a","newValue":"oh"},{"property":"b","newValue":"hai"},{"property":"c","newValue":"oh hai"},{"property":"c","newValue":"oh hey","oldValue":"oh hai"},{"property":"b","newValue":"hey","oldValue":"hai"}]' |
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.
Interesting ordering difference here. Probably worth some additional thought.
mixins/observed-properties-mixin.js
Outdated
static afterPropertyUpdate(target, property, value, oldValue) { | ||
super.afterPropertyUpdate(target, property, value, oldValue); | ||
if (target[OBSERVERS].has(property) && target[OBSERVERS_READY]) { | ||
// TODO: consider changing order of old/new |
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 would like to change this to be value, oldValue
...
} | ||
} | ||
|
||
get propertiesInitialized() { |
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 ended up taking this out as it was only being used within this mixin file (as far as i know).
mixins/properties-mixin.js
Outdated
const setter = rawValue => { | ||
// TODO: it would be better if we could compare _raw_ values to | ||
// determine whether or not an update was necessary. If that were the | ||
// case, we would call shouldPropertyUpdate _before_ getPropertyUpdate. |
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 comment is related to the debate of when/how to do type coercion. I still think I fall into the camp of using types for serialization/deserialization only.
mixins/properties-mixin.js
Outdated
) { | ||
const { computed, observer, type, reflect } = definition; | ||
const attribute = this.camelToDashCase(property); | ||
static addPropertyAccessor(target, property) { |
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 how the addPropertyAccessor
turned out. Instead of messing with the property descriptors (getter/setter) in all the mixins, the base getter/setter just provides more hooks so that mixins can add to the base behavior.
f0e89e1
to
d0ac9b3
Compare
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.
Looks great overall, big improvement! Most of my feedback is cosmetic -- I'm guessing when you try to integrate this change in the app you see no difference?
etc/topological-sort.js
Outdated
@@ -0,0 +1,67 @@ | |||
export class CyclicGraphError extends Error {} | |||
|
|||
function _makeGraph(vertex, mapping, edges, vertices, seen) { |
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: function makeGraph
etc/topological-sort.js
Outdated
return { edges, vertices }; | ||
} | ||
|
||
function _topologicalSort(edges, visitedEdges, stack, solution) { |
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: function topologicalSort
etc/topological-sort.js
Outdated
const solution = []; | ||
while (solution.length < graph.vertices.length) { | ||
stack.push(graph.vertices.find(v => solution.includes(v) === false)); | ||
_topologicalSort(graph.edges, visitedEdges, stack, solution); |
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: seems weird to differentiate between these functions with _
, maybe pick a better name for one of them
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.
Just a pattern I picked up somewhere when you have a wrapper around a recursive function. But yah, I can certainly find better names.
} | ||
|
||
static resolveMethodName(target, methodName) { | ||
if (target[methodName] instanceof 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.
we should prioritize static methods here IMO. also I recommend a comment here about how we attempt to call both static and instance level functions
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 disagree. While we should, by convention, use static methods when we can, I think it's natural that instance methods take precedence over static ones. Either way, yes, I'll leave a comment.
const dependencyToDependents = {}; | ||
const dependentToCallback = {}; | ||
|
||
for (const [property, definition] of Object.entries(properties)) { |
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.
nice es6 fu throughout setupComputedProperties
!
mixins/property-effects-mixin.js
Outdated
return next; | ||
} | ||
|
||
static setupProperties(target, properties) { |
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 this is a better place to refactor, like:
for (const [property, definition] of Object.entries(properties)) {
const p1 = super.setupProperties(target, property, definition);
const p2 = this.setupComputedProperties(target, property, p1);
this.setupObserverProperties(target, property, p2);
}
Not a big deal, just hung up on the repeat iteration of properties in each of these functions.
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.
Yah, I hear you. Getting computed properties set up is a little bit of a pain if we don't loop over everything at once since you need to understand the whole dependency graph.
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.
Willing to do another iteration (pun intended) on this though after we agree on the lifecycles and test suites we have.
mixins/property-effects-mixin.js
Outdated
} | ||
} | ||
|
||
static propertyDidUpdate(target, property, definition, value, 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.
nit: how about propertyDidChange
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.
6 of one, half a dozen of the other. I actually had that name at one point and then saw that lit-element uses update
in a lot of places. I can update change it.
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 makes the forceChangeProperty
method sound a bit odd imo, but i'll stop griping :)
EDIT: just going to call it changeProperty
and drop the force
. It will match the fact that we call it render
and not forceRender
.
mixins/property-effects-mixin.js
Outdated
); | ||
} | ||
|
||
static serialize(target, property, definition, 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.
nice, much better and simplifies reflection code. would call this serializeProperty
mixins/property-effects-mixin.js
Outdated
static propertyDidUpdate(target, property, definition, value, oldValue) { | ||
super.propertyDidUpdate(target, property, definition, value, oldValue); | ||
if (definition.observer) { | ||
// TODO: consider changing order of old/new |
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: don't consider it :)
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.
yep, just changed. the reason i was holding off is that i was hoping to just have a drop-in replacement, but we'll now need a slew of changes when we attempt to use this in the app.
EDIT: on second thought, it sorta seems wack to refactor this and break everything such that we will need a tick-tock release on this. While I'm 100% on board to do this later, I think I'd like to make actual interface changes one by one after this settles for a moment.
@@ -10,9 +14,9 @@ suite('x-element computed properties', async ctx => { | |||
'initialized as expected', | |||
el.a === 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.
strange indent here
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.
GitHub collapsed properties-mixin.js
on me, so I guess I have two pending reviews now!
mixins/properties-mixin.js
Outdated
const DASH_TO_CAMEL = /-[a-z]/g; | ||
const CAMEL_TO_DASH = /([A-Z])/g; | ||
const COMPUTED_REGEX = /^(.+)\((.+)\)$/; | ||
const PROPERTIES = Symbol.for('__properties__'); |
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: is PROPERTY_DEFINITIONS
more accurate?
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.
doesn't matter to me. I think this one is a little hard to name since static get properties()
is really a mapping of property to property definition. I can change it though (I did actually have that in an earlier change-set and changed my mind).
mixins/properties-mixin.js
Outdated
const dependencies = new Map(); | ||
const resolvers = new Map(); | ||
const observers = new Map(); | ||
static initialize(target) { |
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.
similar comment from before, can you move for (const [property, definition] of Object.entries(properties)) { ... }
into this function and make an atomic initializeProperty(target, property)
inner function? it would mean a lot less looping over the same list
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.
Going to do a sweep on these again. It will be a balance between keeping some ephemeral state on the target
vs handling it in a closure, I believe.
At any rate, I totally agree and I'll try to fix as best I can.
mixins/properties-mixin.js
Outdated
Reflect.deleteProperty(target, property); | ||
Reflect.defineProperty(target, property, { get, set, configurable }); | ||
} | ||
target[PROPERTIES_INITIALIZED] = true; |
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.
worth a comment on why we have this guard
mixins/properties-mixin.js
Outdated
for (const [property, definition] of Object.entries(properties)) { | ||
const value = initialValues.get(property); | ||
const oldValue = target[property]; | ||
// TODO: revisit this decision about when to set initial values... |
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: add more info about why
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.
revisited and comment deleted.
mixins/properties-mixin.js
Outdated
target[property] = | ||
defaultValue instanceof Function ? defaultValue() : defaultValue; | ||
} | ||
static forceUpdateProperty(target, property, definition, value, 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.
could use a comment about why we need this
mixins/properties-mixin.js
Outdated
`Attempted to write "${attr}" as a reflected attribute, ` + | ||
`but it is not a Boolean, String, or Number type (${type.name}).`; | ||
target.dispatchError(new Error(message)); | ||
static deserialize(target, property, definition, 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.
nit: would still call this deserializeAttribute
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.
sure. it feels funny since attribute
is not passed as an argument, but I can change.
I tried to drop this into a project and noticed that the attribute deserialization was not working, strings were not being coerced to the Number type. Probably a sign that my prior tests are too sparse |
Yah, I haven't done a full round of integration testing on this yet, so I'm not surprised I've missed some stuff. I'll let you know when my confidence in the change goes up 👍 |
d0ac9b3
to
1b4c8ed
Compare
@klebba another iteration is up. I think I got to most of your concerns. I still feel like the code is less than elegant though... Notably, this should all happen as expected, synchronously (emphasis on synchronous!):
|
1b4c8ed
to
d684e63
Compare
This refactor attempts to split apart the current
/mixins
into smaller, more manageable chunks by providing more hooks inproperties-mixin.js
.It also adds a new concept called property effects. Using this terminology, property reflection, computation, and observation are all effects that take place based on changes to properties. Note that by splitting these into separate mixins, we can have full control over the order that effects happen.