Skip to content
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

Closed
wants to merge 1 commit into from

Conversation

theengineear
Copy link
Collaborator

This refactor attempts to split apart the current /mixins into smaller, more manageable chunks by providing more hooks in properties-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.

* Provides observation and computation hooks for properties.
*/

// TODO: Delay first computation until at least one dependency is defined.
Copy link
Collaborator Author

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.

@@ -0,0 +1,12 @@
/**
* Encodes the order that property effects should take place in.
Copy link
Collaborator Author

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.

@@ -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"}]'
Copy link
Collaborator Author

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.

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
Copy link
Collaborator Author

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() {
Copy link
Collaborator Author

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).

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.
Copy link
Collaborator Author

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.

) {
const { computed, observer, type, reflect } = definition;
const attribute = this.camelToDashCase(property);
static addPropertyAccessor(target, property) {
Copy link
Collaborator Author

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.

@theengineear theengineear force-pushed the refactor-properties-mixins branch 2 times, most recently from f0e89e1 to d0ac9b3 Compare January 27, 2019 03:35
@theengineear
Copy link
Collaborator Author

@klebba this is getting there... still depends on #18, so I'll wait until we get that merged in to bother you about this one.

Copy link
Collaborator

@klebba klebba left a 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?

@@ -0,0 +1,67 @@
export class CyclicGraphError extends Error {}

function _makeGraph(vertex, mapping, edges, vertices, seen) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: function makeGraph

return { edges, vertices };
}

function _topologicalSort(edges, visitedEdges, stack, solution) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: function topologicalSort

const solution = [];
while (solution.length < graph.vertices.length) {
stack.push(graph.vertices.find(v => solution.includes(v) === false));
_topologicalSort(graph.edges, visitedEdges, stack, solution);
Copy link
Collaborator

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

Copy link
Collaborator Author

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) {
Copy link
Collaborator

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

Copy link
Collaborator Author

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)) {
Copy link
Collaborator

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!

return next;
}

static setupProperties(target, properties) {
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

}
}

static propertyDidUpdate(target, property, definition, value, oldValue) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: how about propertyDidChange

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

@theengineear theengineear Feb 3, 2019

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.

);
}

static serialize(target, property, definition, value) {
Copy link
Collaborator

@klebba klebba Jan 31, 2019

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

static propertyDidUpdate(target, property, definition, value, oldValue) {
super.propertyDidUpdate(target, property, definition, value, oldValue);
if (definition.observer) {
// TODO: consider changing order of old/new
Copy link
Collaborator

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 :)

Copy link
Collaborator Author

@theengineear theengineear Mar 8, 2019

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 &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strange indent here

Copy link
Collaborator

@klebba klebba left a 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!

const DASH_TO_CAMEL = /-[a-z]/g;
const CAMEL_TO_DASH = /([A-Z])/g;
const COMPUTED_REGEX = /^(.+)\((.+)\)$/;
const PROPERTIES = Symbol.for('__properties__');
Copy link
Collaborator

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?

Copy link
Collaborator Author

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).

const dependencies = new Map();
const resolvers = new Map();
const observers = new Map();
static initialize(target) {
Copy link
Collaborator

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

Copy link
Collaborator Author

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.

Reflect.deleteProperty(target, property);
Reflect.defineProperty(target, property, { get, set, configurable });
}
target[PROPERTIES_INITIALIZED] = true;
Copy link
Collaborator

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

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...
Copy link
Collaborator

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revisited and comment deleted.

target[property] =
defaultValue instanceof Function ? defaultValue() : defaultValue;
}
static forceUpdateProperty(target, property, definition, value, oldValue) {
Copy link
Collaborator

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

`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) {
Copy link
Collaborator

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

Copy link
Collaborator Author

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.

@klebba
Copy link
Collaborator

klebba commented Jan 31, 2019

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

@theengineear
Copy link
Collaborator Author

@klebba thanks for the review! I've only given it a quick look so far, but it all seems reasonable. I noticed that I had introduced a bug in #18 , so I spent time fixing that and upping our testing game there. If I'm feeling it, I may circle back to this over the weekend. We'll see ;)

@theengineear
Copy link
Collaborator Author

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 👍

@theengineear
Copy link
Collaborator Author

theengineear commented Feb 3, 2019

@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!):

  1. Properties are analyzed during construction
  2. Computations happen only after connectedCallback
  3. The render method is called synchronously after computations.
  4. Reflection & Observation happens.

1. Postpone more work until initial connect (closes #14 & closes #22).
2. Upgrade all properties at initialization time (closes #15).
3. Initialize properties before initial render (closes #17).
4. Improve handling of property effects (closes #19).
3. Robustify test suite.
@theengineear
Copy link
Collaborator Author

theengineear commented Mar 8, 2019

@klebba this PR is getting pretty long/gross. I would like to invite others to comment, so I'm going to close it and create a new PR >> #23

@theengineear theengineear deleted the refactor-properties-mixins branch March 8, 2019 08:32
@theengineear theengineear restored the refactor-properties-mixins branch March 8, 2019 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants