Virtual properties are a great way to add some semantics to your data layer and abstract any shortcomings of the underlying schema. This lesson demonstrates virtual getters and setters in Mongoose, and how to integrate their use in your Express server.
So our user schema has a name object which has a handful of properties, one of which is this full name. A lot of times your data is not going to have a full name, it's just going to have a first and a last. We're also using that full name here in our template by just binding to user.name.full but I've actually gone back to our data file that used to set up our initial database, and I've removed the full name field from that. So we've just title, first, and last in our name object.
We're then going to come over here and use the Mongo import command to sort of repopulate our database using that data file that has that full name removed. So we're going to run that, and that's going to reset our database and our collections. Now we can start up our dev server with npm run dev, and go over here and we'll see that our names are in fact missing. So we have removed that property, it's no longer visible in the app.
We'll go ahead and remove it from our schema here since it's not actually a concrete part of it, but we're going to recreate that down here using the virtual properties capability of Mongoose. To add the virtual property we're just going to say userschema.virtual and then give it the name or the path to the virtual property, which in this case is name.full. Then we're going to say get, and we're going to give it a function that will be used to get that property.
So in our case, this is just a function that concatenates the first name and the last name, and puts a space in between them. So if we save this and then go run our app again, we'll see that we do in fact get names back in the app, so we know things are working. Now you'll notice that everything is lower case, just because that's how it is in the data, so we're going to use lodash to convert these to a case that makes a little more sense.
We'll just wrap our whole return value in _.startcase and that will give us proper capitalization that we would expect to see in a name. So now we've got our app back to where it was, and we can go in here and see that it does work on the detail pages as well.
So just to recap, we were able to create a virtual property that doesn't actually exist in the structure of the data by calling the virtual method on the schema object that we have, giving it the path that we want to use for that virtual property and then providing a function that will be used to get the value for that virtual property.
So that's how virtual properties work in terms of getting values, but you could also use virtual setters, and to show that we're going to go over to our template here, and we'll just duplicate the section of the template that we have here for the street and we'll change that to be the name so we'll update the label there, we'll update our binding to user.name.full and we'll give our input the ID of name so that we can identify that.
Now, if we go down to our code that actually handles the saving of the form, we need to modify the structure a little bit, because currently we're just sending back the location data. So the first thing we'll do is we'll add in our new name value here, so the name for the value that we're going to get out of the name field, and then we're going to go ahead and stick all of this location data in a location property, just to sort of keep it all grouped together.
Now that that code is updated to send the data back in a format that we can use, we'll go back to our server side code, and this is our put handler, so this is what gets called when we call save on this function. Again, just to review, we're sending back a name property and a location property which is an object.
So we're going to update this handling code, so that instead of just directly setting location to the request.body, we're going to say location is the request.body.location property that we just defined, so if we go back to the app and we go to edit this guy, we'll just change the state to Texas, hit save, and in fact that does work. We know that we haven't broken anything with the existing functionality. Now that we know that works, we can go ahead and implement our virtual setter.
So we're just going to duplicate our virtual getter code here, but in this case we're going to define our function to accept a value, which is going be able to get sent back from the page, and then we'll parse that value, so we're going to just split on a space here which is obviously not super robust code, but for these purposes it's fine. So we're going to split this into bits, and will just do a plain assignment of these values based on their indexes.
So this.name.first = the first item in the bits array, and then this.name.last is going to be the second item from that array. So you can see this is super straightforward logic, it's just the inverse of what our getter dose, and that's really all we need to do here in the schema. Less obvious, and what was a big stumbling block for me when initially learning this topic, is that you can't actually use the find one and update method of Mongoose.
We can see if we go over here and we try to update the name, it's not actually going to work and I don't know the exact underlying reasons for that, other than find one and update is sort of a specialized helper method that Mongoose provides, and it's not compatible with virtual setters. So if you want to use a virtual setter, you have to sort of revert to the base method of find one.
We'll see how to do that here, so instead of just passing in the object that gets merged with the document that gets found, we're just going to say find one, give it the criteria of how to find that, which we're going to keep the same just search on the user name, but then when that document comes back in this callback as our user object here, we're then going to set things directly on that user object.
So we can say user.location = request.body.location and then now we can actually use our name setter and say user.name.full = request.body.name, so that's going to take that single field and apply it to the property that we've defined in our virtual setter.
Now in order to actually commit these changes to the database, we're going to then call the save function of the user document, and pass it a callback so that we know when that save has actually completed, and then we'll go ahead and end the response, so that the calling page knows that that operation has finished.
So we're just setting the property directly like any other property, it's not obvious to this code that user.name.full is a virtual setter, and then we're calling and then we're assigning the location property, and then we're just calling save on that document and ending the response when that callback function fires. So now if we go back to our app and refresh, we should be able to update this name and have it stick.
So we'll just change this whole name to Tom Jones, Tom Jones obviously lives in Las Vegas, so we'll save those changes and in fact they do persist there between refreshes, we know they've been applied to the database.
So just to recap, we've got a virtual property getter and setter defined, they're both very straightforward and look like any getter/setter that you would see in a number of languages where the getter returns some computed value, and the setter decomposes some value and sets the individual values that's necessary.
We also needed to update our request handling code and use the built-in find one method rather than the specialized helper of find one and update, and then fall back to that document.save function and use that callback to indicate that the response has finished.