Sessions is temporarily moving to YouTube, check out all our new videos here.

A Fun Hack for Capturing Changes to an Ember Object Graph

Jamie White speaking at Ember London in February, 2017
Great talks, fired to your inbox 👌
No junk, no spam, just great talks. Unsubscribe any time.

About this talk

Step-by-step guide to making direct changes to deep graphs of data in Ember.


All right. I'm going to show you something I've been working on. Let me start with a question. So, who has used Ember-changeset? Yeah? - [Man 2] Yeah. - Is it good? - Yeah. - Yeah. I usually use Elixir and stuff, is all, haven't you seen... - Yeah. - You've used the equivalent Elixir pieces. Does that go for you as well, Alex? - [Alex] Not Elixir, but, you know, changeset. - Ember-changeset. Cool. A lot of people haven't, but have other people used things like buffered-proxy? And I think there are a couple of add-ons that do the same thing. Basically, it's this idea that you've got a form which is rendering a model and you want to make direct changes but without changing the underlying model and you need a layer to capture those changes in between. So, let me show you an example of the kind of thing, so. Oops. There we go. Okay. So... Let's take this bit away. So, it's... oops. So, it's a kind of thing we've got a form like this and you've got some models and some kind of classic master-detail view. And you want to be able to make an edit here without the edit affecting the underlying model, which is listed on the left. And so, things like Ember changeset and back in the day buffered-proxy give you this idea that you have some object standing in between your model and the changes you want to make and that model is going to soak up those changes and then, at some future point, you can grab them and commit them. And that's a really useful thing that most apps need. And the thing I was wondering about, just as a thought experiment, was do these things support changes to arbitrarily deep graphs of data or is it simply the properties at that first level that you're proxying? Now, I think with buffered-proxy, it's just that first level. With changeset, I think it has some awareness of changes to relationships on Ember data models, but I think, again, it can't give you an arbitrarily deep proxy through to changes you want to make. So, this is a thing that I was reaching for and not because it's necessarily something that you need in most apps. But I was wondering, could you build this in Ember, would it be hideous or might it be quite nice? So over here, I've my models coming in from my roots and my model in this case, is one of these tracks here, and a track has a name and then it's got an artist and that artist has a name. So, I should just show you the template. You can see up here. My model is my track, it's got some artwork, it's got a name and then over here, Everyone's familiar with this stuff. And then down here you've got some inputs. And they are directly mutating these properties and this one is mutating a key on the model itself. But down here we're mutating name on artist. So, it's one level deeper in this graph. So, I've got a helper here, patch for which is going to wrap my model in one of these object, proxy object to stand in between and soak up changes. So, I'll show you what patch looks like in just a second, but let's just demonstrate this behavior. So, now you can see, over on the left, it's not changing, on here it is. And down here, you can see the changes that it's absorbing. So something that I thought was important, was to produce changes in a format, where it would be very easy to commit them at the end, where strategies for processing those changes would be really easy to write. So, what I've gone for here is, rather than it producing a delta in the form of another graph, like a graph of the changes, it's producing a flat associative array of key or path, if you like, to the new value. And I don't have any data to demonstrate going another level deeper but it would be trivial to do so and the template wouldn't look any different than if you were editing things directly. And it makes... I'll just show you the... So this is a computer property on my thing that I'm going to call a patch and it's going to tell me if it changes again, it's just this straight dictionary. Doesn't go any deeper, it's just path to change, a path to value and then implementing something like this, a reset button, is just a matter of, in this case, I'm using the Mut helper by just setting the key on the patch back to the old value and then it will throw away the idea that it's got to change there. Okay. So, I'll show you what the patch piece of this looks like. So, in fact, I'll tell you what, I'll show you patch for to begin with because that's the entry point. So, I parsed it some source object, it's going to wrap a patch around it. So, it's got a source and it's got that associative array of the values that it's keeping track of, all these changes that it's absorbing. This is really just a convenience thing, to let me spit out. These might go to Lorem Ipsum there. You have to spit out these things down here. It's not really, it's not an integral part of it. It's just a convenience thing, so... - [inaudible] the main side. - Yes, random side thing. So, hands up if you've come across unknown property and set unknown property in Ember before. So, a few, and actually I think these aren't really talked about very much anymore, but these are how things like a promise proxy are implemented and any proxy part of the system. It dates back to when the Ember object model was created and there was quite a lot of influence from Ruby. And Ruby has method missing, which allows you to implement this same kind of behavior. So, when you call set on an Ember object and it doesn't have that property, your set unknown property method gets a chance to do something similarly with get. So, you'll see here, I'm just delegating straight through to some more specialized methods. So, what does get value do? Get value has a look in it statute of values, statute of changes. And if it's got something it's going to give you that back straight away. If not, it's going to have a look at its source object. And depending on the type of the value on the source objects, it's going to do one of two things. If it's a complex object, so a JavaScript object or an instance of an Ember class, it's going to wrap it in one of these things, a relay, which I'll show you in a moment. Otherwise it's just going to give you that primitive value straight back. And the reason for the relay is so that you can parse these deep paths into your template and both get and set them, and what relay allows you to do is, for all those calls, to head back on up to that route patch object, which is keeping track of the changes. So, a relay... ...looks like this. That's all it is. So, when it's instantiated I've given it a parent, which to begin with, is the patch itself and I've given it a key and it's like, this is the key on your parent at which you live. And so, when anything else encounters a relay in a template and calls get or set on it, again it's going to head into unknown property. Go to this more specialized method and this is just going to call on up to its parent and say, "Hey, do whatever get value is for my key, plus the dot, plus the key that's being asked for." So, if you imagine in the template, when I'm doing Let's just bring that template back up. And actually, probably the more interesting variant is... you know, any of these is fine. So, so model is the patch. We call get on the patch. It encounters an instance of an Ember data model, so this artist key, and it gives back a relay. And then when we ask for name on that relay, name is going to go... Okay, it's trying to get a name. My key is artist, I'm going to glue artist and name together and ask my parent, "Please, can you get me" And so, however deep you went, it would build this path and eventually give it to patch and patch is either going to ask for it from the source or if it's got its associative array of values, it's going to give it straight back from there. So, in fact, if I were to...let's see, maybe even demonstrating that in action. So, let's take a debugger here and try and follow it through. So, if we open up another one of these, it's music, so I'm not sure where in the template this is being asked for. But we're starting off with... ...the key name, so it's going to call get value. Don't actually know how to use WebKit's debugger [inaudible]. And it's get value. So, this object, its own key is artists, the key it's being asked for is name. So, it's going to glue these two things together and ask its parent for them, so let's type parent.getValue, this.key. So, it's going to ask its parent for that, and I think if we just follow on the train we end up here in our patch objects. So, it's going to have a look in its values for this key of Values at this point is just... oops. Now, let me restart that because we've gone on to a less interesting key. So, get value, get value. Step over that. So, values at this point, is just an empty dictionary. So, we're just going to skip over that, if it's going to ask its source which is another data model for the original value for that and it's going to find a primitive string. And so, that's just going to allow it to return that directly. So, a little of this speed on. And then, if I were to change, next time it comes through it's going to be asking for name. And drill on through to the patch for But this time, in our array, we've captured that change with this path. So, you can imagine, if you had a deeper object graph, these things would be [inaudible]. So, the pieces that are missing from this, I think these premises work fine and it's kind of interesting that Ember makes this so easy to make. I think Ember's object model deserves more credit than it gets, I think we're all very keen to switch over to straight JavaScript classes, but actually it's gotten us a long, long way. So, the thing this doesn't know how to do at the moment is erase and enumerable type things, but it would be very easy to write an enumerable relay, which knows how to do special things, for effectively the subscript operator to get a numbered keys. And then, what this doesn't answer is the question of what do you then do with that changeset out the end, that patch? And I think when you've got such a simple dictionary to work with, it'd be very easy to start to think up strategies for stepping through the keys, finding out what the source objects are, where you encounter an Ember data model. You can write a strategy which will apply changes and then give you the opportunity to save them and figure out how to roll them back for you. But I think separating the capturing of changes from the application is going to be key to these kinds of things being successful. Any questions? Yo. - [Man 3] Yo. - I think that that can be composed so the... because we didn't catch that question on the microphone. It was related to when you've got more sophisticated types of relationship, many to many relationships. Things that aren't necessarily easily expressed by a path like I've got here. And I think that you could compose together the thing that does the capturing with the thing that can then interpret what those paths mean and how intermediate objects are understood and instantiated. And then the piece which actually can commit changes, for system, and manage rollback, and things like that. I think actually the... I like to think of it in terms of... I haven't written any code like this, but purely speculating, I quite like the idea that you might have something like get, where there are a battery of different strategies for dealing with particular types of patch. Like, get has a three-way strategy and an octopus strategy, whatever else it's got. And I think, like you say, for these types of graphs you would probably find there are certain strategies which are better suited. And actually something that I don't know how I would go about doing with this is, say you had an album with many tracks, and you wanted to provide an interface using this patch concept to add a new track into the array but you want that behavior where you can instantiate an unsaved Ember data model, and it comes with some default values in it. So, how do you do that without accidentally affecting your store? And that might be a separate concern, to be honest, that might be something which does need to be addressed at the Ember data level. I don't really intend to release this, I think it's just interesting that Ember allows you to do it so easily.