-
Notifications
You must be signed in to change notification settings - Fork 1
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
Mxchaeltrxn recursive types 2 #112
base: master
Are you sure you want to change the base?
Conversation
This pull request is being automatically deployed with Vercel (learn more). 🔍 Inspect: https://vercel.com/sketchbook-js/sketchbook/3biilu791 |
@Mxchaeltrxn I haven't done a full review of everything here yet, but I'm having a quick look through on my phone.
I don't quite follow you here. I think it's a bit confusing to apply the open/closed principle because we're using more of a functional programming paradigm rather than OOP, but that aside, I'd still say it conforms to being open to extension — that happens through composition. You could wrap this component in another to extend its functionality (the equivalent to extending a class in OOP). It's also closed to modification — without changing its source code there's no way for someone to change its behaviour. However, I get what you're saying about needing to update it whenever we add a new renderer. No matter what, we're going to have to update something when a new renderer is added. Currently there are multiple places that needs to happen, which isn't ideal, but the setup we really want is for |
@Mxchaeltrxn Sorry, I also should've mentioned in my previous comment that the reason I was passing down |
@haydn Oh that makes a lot of sense and I can see it reducing the overall amount of code in the long run. I'll fix that sometime this week. Also, I made another pull request pointing to the old one because I was trying out stacked pull requests. I thought it might help you review 😅 |
99d2873
to
a67e803
Compare
I decided to just do it since I understood the changes. I've also rebased from master. It's always satisfying deleting code—a little less satisfying when it's your own code though. 😅 Thanks for the feedback! It makes a lot of sense. |
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.
@Mxchaeltrxn Fast work!
I've mocked-up some designs so we have a clearer idea about where we want to get to:
I reckon we can just do something really simple for the navigation (I've just replaced the heading with a back to go back up a level when we need it):
There's one pretty big problem this design introduces with the reordering of lists, but I'll save that for my next round of review. 😄
}); | ||
}} | ||
// Used to determine how to render the list-like option types | ||
displayDepth={optionDepth} |
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.
With the design in mind, I think there's problem with the prop that we're passing down here. We want to use the depth to choose how to render the options based on how deeply they're currently visually nested — not how far they're nested in the data — so I think all we're after here is:
displayDepth={optionDepth} | |
displayDepth={0} |
src/editor/OptionsPanel.js
Outdated
onChange={(newValue, rendererPath) => { | ||
// rendererPath is formatted like path1.path2.path3 etc. | ||
const formattedPath = | ||
`doc.layers.${selectedLayerIndex}.options.` + | ||
rendererPath.join("."); | ||
return setState(currState => { | ||
return immutable.set(currState, formattedPath, 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.
I think there's a better way to handle these updates. Because we've got this nice recursive pattern happening, we can let the ListRenderer
and RecordRenderer
handle the complexity and none of the other components will need to do anything special (they wont need to provide their path when updating). I've put together a little demo of what I mean in CodeSandbox:
https://codesandbox.io/s/modern-darkness-gc6kc
In that example you can see that each component is passed an updateNode
prop that is just an updater function. In the case where we recurse we create a new updater function for each child in created by wrapping the parent's updater function.
This will avoid the tricky problem you're using object-path-immutable
to solve and instead we do something like this (I haven't actually tested this code, probably buggy):
onChange={(newValue, rendererPath) => { | |
// rendererPath is formatted like path1.path2.path3 etc. | |
const formattedPath = | |
`doc.layers.${selectedLayerIndex}.options.` + | |
rendererPath.join("."); | |
return setState(currState => { | |
return immutable.set(currState, formattedPath, newValue); | |
}); | |
}} | |
onChange={(updater) => { | |
setState((current) => ({ | |
...current, | |
layers: current.layers.map((layer) => ( | |
layer.id === selectedLayer.id ? { | |
...layer, | |
options: updater(layer.options) | |
} : layer | |
)) | |
})); | |
}} |
src/options/ListRenderer.js
Outdated
options={option} | ||
onChange={onChange} | ||
onNavigate={onNavigate} | ||
displayDepth={depth} |
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.
displayDepth={depth} | |
displayDepth={depth + 1} |
options={option} | ||
onChange={onChange} | ||
onNavigate={onNavigate} | ||
displayDepth={depth} |
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.
displayDepth={depth} | |
displayDepth={depth + 1} |
I just added some commits to handle the the Some questions for now: I'll do the rest of this a bit later! |
@Mxchaeltrxn Nice! Yeah, so… I'm actually thinking we do away with We were using paths for 4 purposes:
For point 3, we're going to run into real trouble down the line. If we make it possible for users to reorder items in a list (which we should), those paths aren't going to work because they use the index to identify the items in the list. When they get reordered, their path would change and React is going to get confused about which DOM element belongs to which item in state. This is explained in detail in the React docs. This leaves us in a bit of a pickle. What we have to do is give each item in a list a unique ID, so instead of this: const exampleLayer = {
id: "123",
type: "SketchbookComponent",
component: "Example Component",
name: "Layer 1",
x1: 100,
y1: 100,
x2: 750,
y2: 140,
options: {
scalarExample: true,
listExample: [1, 2, 3],
recordExample: {
foo: "bar"
}
}
}; We'd need something like this: const exampleLayer = {
id: "123",
type: "SketchbookComponent",
component: "Example Component",
name: "Layer 1",
x1: 100,
y1: 100,
x2: 750,
y2: 140,
options: {
scalarExample: true,
listExample: [
{ id: "235", value: 1 },
{ id: "423", value: 2 },
{ id: "653", value: 3 }
],
recordExample: {
foo: "bar"
}
}
}; It's a bit gross, but if we're going to be forced down that path for values in lists, I think we may as well do the same thing for all the values: const exampleLayer = {
id: "123",
type: "SketchbookComponent",
component: "Example Component",
name: "Layer 1",
x1: 100,
y1: 100,
x2: 750,
y2: 140,
options: {
id: "933",
value: {
scalarExample: { id: "345", value: true },
listExample: {
id: "325",
value: [
{ id: "235", value: 1 },
{ id: "423", value: 2 },
{ id: "653", value: 3 }
]
},
recordExample: {
id: "775",
value: {
foo: { id: "776", value: "bar" }
}
}
}
}
}; That may seem ridiculous, but it's got a big advantage — we can just use the ID's for all the things we're currently using paths for:
That being said, I don't think we actually want to change the shape of what's persisted in the document ( https://github.com/sketchbook-js/sketchbook/blob/master/src/Editor.js#L127 I've put together a CodeSandbox to demonstrate: https://codesandbox.io/s/cl0m0 Probably worth mentioning that at some point in the future we'll probably use something like Recoil to maintain all this state. In some ways what we're doing here gets us a bit better aligned with that pattern. I know there's a lot to take in there, if you'd like I can add a commit or two to point things in the right direction? |
@haydn That indeed is a pickle. That makes sense to me. It doesn't seem that gross if it's in memory. It'd be gross if it wasn't in memory though. 😁 I think I have enough to work with so you don't have to get me started (unless you'd like for this feature to be done more quickly). Thanks for the code sandbox and examples! I'm assuming that since we don't have a path anymore, we will use something like breadth first search or depth first search to find the option to render? |
Yep, spot on. We can just search the tree to find the node. Even if that is expensive to do we can use useMemo to only recalculate it when the user navigates. |
@haydn Hey I’ve just written up a small plan/todo list since a few things have changed and new things have come up. Let me know how this sounds to you. Let me know if anything seems wrong. Note: I use the term nested option to describe options that can hold other options (lists and records in our case). Changes that we have to make from the current application:
TestingI'm not really sure how to approach testing now that we have unique ids being generated. I considered mocking but I found it would get quite messy to update the ids if we added/removed things to the document in that particular test case. I was thinking we just test the existence of the new values. E.g.
We may just assert that
More changes (function handlers)
I was thinking about how to recursively handle the behaviours. I’m thinking I’ll have to go through the |
@Mxchaeltrxn Awesome breakdown of what needs to be done! It's dawning on me now how much of the app will need to be changed because of those ID's being introduced. I'm thinking we should tackle that first in a separate PR and get it merged into master, then come back to these recursive options. And that gets me thinking… it might be worthwhile doing a bit of a spike before that to see if Recoil is the path we want to take. Moving across to Recoil would probably be just as disruptive, so we should make a decision on that now and save ourselves doubling up on work if that is the path we want to take. In my understanding of Recoil's model, each option will end up being its own "atom", which should solve some of the problems we're facing here. I'll write-up a new issue today for a little spike into Recoil (probably just mocking up something in CodeSandbox to test the hypothesis). Would you be happy to give that a shot? Recoil is pretty new and it's probably not very googleable yet, so it might take a bit of investigating/experimentation to learn all its ins-and-outs.
Yeah, those nondeterministic ID's can be a pain. Just checking they're present is probably enough. If you want to get fancy you could also check if they're valid: https://github.com/uuidjs/uuid#uuidvalidatestr The other thing you could test is if they're all unique. Not sure there'd be much value in that though because it can't be reliably tested (tests might fail one time and then pass the next, depending on how the ID's are being generated). |
@haydn Yeah for sure I'd be happy to take a shot at doing the feasibility study/test! |
@Mxchaeltrxn Awesome! Sorry, didn't get around to it yesterday! Here's the issue, ready to go now: #116 |
@haydn
Based off #111
Note that I'm merging this into the branch you made just to make things (ever so slightly) easier for you to review. I know there's a lot here and I could have done a better job at splitting it up but hopefully it is okay because you have a good understanding of the components involved.
Okay so what I've done is:
TODO:
AbstractRenderer
so that conforms to the Open/close principle? It makes sense to in my head so changes don't have to continually be made toAbstractRenderer
when adding a new renderer. It also reduces the total amount of code.