00:00:00
I never know how to start these things. Thank you all for coming to the 75th JavaScript talk at RailsConf. I have seen some really great presentations this week so far. Did anyone see the Three Mile Island disaster presentation? I really liked that one; it had really great slides.
00:00:13
Let me introduce myself. My name is Graham Conzett. I've been a software developer for 10 to 15 years, and I really want to leave time at the end of this for questions or discussion. I've run through this presentation a couple of times, and it's always right up until the wire, so I'm going to try and go quickly because I want to talk to everyone. I don't do Twitter, but I do have a GitHub.
00:00:41
Because it's RailsConf, this presentation is actually a Rails project, so you can clone it right now and follow along. It'll save me some time from having to alt-tab between demos and slides. It doesn't have to click the buttons at the bottom, though. Hopefully, that makes up for my bad quality slides. Before we talk about what I mean by 'old-school JavaScript', I wanted to provide some background on why you might want to go down this road.
00:01:21
This is not a 'modern JavaScript sucks' talk, nor is it about how 'React is terrible'. It's more about picking the right tool for the right job. There’s no silver bullet. I am heavily involved in the React user group in Columbus and use it on several large projects. I actually do like JavaScript.
00:01:54
So, story time: I work for a company called Orange Barrel Media out of Columbus, Ohio. We are an outdoor advertising company, not the text-based kind like Google AdWords. We're actually a large format outdoor advertising company that also operates interactive kiosks, which are like giant, location-aware content aggregators. Just think of a giant iPad sticking out of the ground, but it has local content and events that cities manage and coordinate.
00:02:37
The operating system you might see on the screen there is just the tip of the iceberg. There is a lot of content that cities and admins have to manage. Recently, we have had significant growth in this area, and we have a couple of years-old Rails app that served the backend well when it was small, but as our team grows, the Rails backend isn't quite cutting it anymore.
00:03:08
We’re using an admin framework that was good because it allowed us to focus on features and provided a consistent user interface, with some nice user experience niceties, like drag-and-drop, undo, and copying. However, it was becoming really inflexible for what our users and admins wanted to build. There were also lots of dependencies and plugins, which made it hard to upgrade Rails when you have so many dependencies.
00:03:55
So, we went looking for something else and wanted to keep what we liked about the admin framework—those top three green checkmarks—but see if we could do better in other areas. After going back and forth, we settled on using Rails. This makes sense because Rails provides a lot of what we need out of the box.
00:04:52
Let's focus on the features that end up in the operating system on the front end of those kiosks. Rails provides a consistent user interface, whether we’re bringing in Bootstrap, a component library, or just relying on partials. It’s flexible, and we can build anything we want for our users.
00:05:28
The question mark is around these user experience niceties. If we’re just sticking with vanilla Rails out-of-the-box, how do we achieve those nice drag-and-drop, WYSIWYG-style features that users have come to expect? We were curious how far we could get with just the JavaScript library that Rails itself provides.
00:06:12
Modern browser support has come a long way, and you might not need to bring in React or Angular to get the job done. When I talk about 'old-school JavaScript', I refer to using the out-of-the-box UJS library that Rails provides to achieve functionality. In the context of this talk, 'old-school' means using the Rails request-response lifecycle with some JavaScript that executes when the page renders.
00:07:05
We’re not even using jQuery here. There’s nothing stopping you from using ES6 or Babel or anything like that, but it’s more about leveraging some modern browser technologies. It really depends on what your target audience is. When I say 'what works in your users' browsers or whatever works best for your users', I mean it.
00:07:38
Some of you who have used Rails for a long time probably remember RJS, which was dropped or supplanted by UJS. I won’t go into that too much, but basically, RJS involved writing Ruby code to execute JavaScript on the client side, and we’ll be doing a little bit of that in these demos.
00:08:04
This is a basic example, somewhat contrived, of what one of our admin panels looks like for displaying posters that rotate on the front of the kiosks. We basically have a playlist, which we call it, and the posters inside it are called playlist items. They get a duration and an order for how long they are displayed.
00:08:32
This is just a very basic HTML version we’re building on throughout the talk. You can select items to add them to the list, delete them, and you can see that the page is refreshing each time. This is all just vanilla Rails under the hood so far—just basic Model View Controller and nothing fancy.
00:09:10
Let’s take a quick look at the controller to prove that this is not too complex. This is the same controller for all the examples. The only thing we’ve added is this format.js call in our respond_to block, and that’s because by default, in Rails when you’re using these remote asynchronous actions, it will just call the HTML format block.
00:09:43
We want to do special stuff, so this should all be fairly familiar. There are some to-dos in there, but it’s just the few basic actions that now have the format JSON. I kind of lied in the first example—remote create is actually in Rails by default now when you use 'form_with' in Rails 5.
00:10:03
You actually have to specify local: true in order to make it just do a basic HTML post anymore, so while that page was actually just a post-redirect-get loop, we had to make it do that out of the box. If you want your form to be asynchronous, you don’t have to do anything.
00:10:32
It used to be you would put remote: true in, but 'form_with' does that by default. Now, let’s look at what’s new in here, taking a look at the 'create.js.erb' file that comes back. We’re basically going to render our playlist item, and you will see a special 'J' character that serves as a shortcut for escaping JavaScript.
00:11:20
That's going to build HTML that allows it to be inserted into the page when the request comes back. We're going to assign that to a variable and add it to the end of our playlist items list. You notice we’re not using jQuery or anything here. Once again, browsers have come a long way, so we can leverage things like 'insertAdjacentHTML' and 'querySelector'.
00:12:10
At the end here, you can see we’re just adding the nicety of clearing the form once the user has submitted it. So, let's take a look at what that looks like—it will look pretty similar to before, except it's a little bit faster.
00:12:54
This demo is running locally, so even the HTTP version with the redirect is going to be fairly fast. When that response view gets executed, it gets inserted into the table. You’ve probably seen this before, and I promise we’ll get to more detailed examples of leveraging this, but you can see when we destroy it, it’s still synchronous. You saw a flash of the page there. We’re going to fix that next.
00:13:52
Looking at remote destroy, this is another fairly common action if you’ve read through the Rails guides. We’re just going to change our delete link to have remote: true, and this will submit it via Ajax now with a media type that lets it know it should respond with the JavaScript partial. That’s all you have to do there. We added a little bit of 'disabled' functionality; we'll go into that in a bit.
00:14:39
Let’s take a look at the actual view we’re rendering for our destroy action. We’re going to introduce some helpers here, and once again we’re using all of Rails—utilizing partials quite a bit in our application. We have a method that hides the item once it’s deleted. We’re going to render a flash message as well.
00:15:29
This strategy is similar to old RJS in that we’re using Ruby code to do JavaScript things. In this case, it’s just partials and not Ruby that’s been transpiled to JavaScript. In our application helper, we've got this 'hide' method that returns a raw string that finds our model by the DOM ID.
00:16:05
To display none, 'dom_id' is a helper included in Rails that constructs an ID based on the model name and its ID from the primary key. Now, if we have our new item here, we can delete it and notice that we have nice flash messages, and everything deletes quickly.
00:16:50
Once again, it might be hard to see the difference because we’re on localhost and it’s fast, but it is much faster for the user as they work. The one thing we did change is we added this 'render_flash' method. I think this is worth pointing out because this is a pattern that keeps coming up where we’re sharing a partial between an HTML view and a JavaScript view.
00:17:54
This is useful because if someone has JavaScript disabled—which, in this day and age, is rare—you still want to share the partials between your JavaScript rendering and server-side rendering. 'render_flash' just takes a partial that we've extracted and renders it into the flash container in the layout.
00:18:45
Now we’re continuing with the CRUD theme. This action is probably the most straightforward, but I couldn’t leave it out since you’re already doing create and delete. It’s not really showing a whole lot in the Rails guides, but we’re just changing the duration we saw in the table to a form that we can update.
00:19:12
We’ll just put them in line, and you can see that the update works. I can show you that it actually updated and not just kept the previous value in the display. I promise there are other cool features to showcase beyond the CRUD operations.
00:20:07
So let’s talk about soft deletes for a moment. I'm taking a digression to mention soft deletes in RESTful context. We’re embracing all of Rails, including REST and HTTP, so we’re going to add a destroyed playlist items controller.
00:20:38
The reason we’re doing this instead of just having an action that deletes or undeletes is that we need to respect the 404 in HTTP. If we delete an item, we shouldn't still be able to refresh and have that ID return a 200 response—instead, it should be gone. However, how do we undelete it via a REST API if we can't find it anymore?
00:21:21
The solution I like is to add a different controller and different route—basically treat these as different resources and consider them deleted playlist items. If you have undelete functionality across your app, you might end up with several controllers to maintain.
00:22:30
In our strong parameters, the only thing we're permitting is a destroyed_at timestamp. In real life, you would want to make this a virtual attribute. But, for the sake of the demo and simplicity, just went with that. The undo functionality itself in the UI modifies our flash message to return a special button.
00:23:15
This button acts as a remote function that updates the record. You can see that in the second argument, we’re passing 'destroy: nil' to reset, essentially undeleting the playlist item. We’re sending a special undo partial that includes a link for this action.
00:24:13
Now, let’s try to delete a playlist item. Once we do, you can see we have our new method rendered, and it says 'undone' to show it has been restored back in there. This gives a nice user experience, similar to what Gmail offers for undeleting.
00:24:57
So far, we have quite a bit of functionality implemented with very little JavaScript written. No frameworks and not even jQuery. You notice the flash message says specifically 'undone' because we have a unique controller representing this action, giving us the opportunity to customize the response when a user performs it.
00:25:47
In the update view for the destroy playlist item, we simply state that it’s undone, and then we call our show helper to unhide any deleted record that was hidden as part of the delete process. We avoid removing it from the DOM, maintaining records for reference.
00:26:06
The next thing we wanted to bring into the CMS was the notion of copying. There are a lot of repetitive tasks in this application, and we aimed to ensure users could perform those actions easily without having to repeatedly type things.
00:26:55
We’re going to add a simple function that returns a hash of attributes to copy. You can also add this to the application record, grabbing a model's attributes while avoiding timestamps and primary keys. This is usually sufficient for most models.
00:27:45
In the case of our playlist items, we want to copy the duration and poster ID. The changes we make to the playlist item are straightforward. We’re creating another button to copy a record with the specified parameters.
00:28:21
This is simple, similar to the undo functionality—just a different set of parameters and a different method. So we have our delete functionality, our copy action, and it’s working pretty well.
00:29:07
Now, let’s discuss reordering, as that’s the first time we really do custom JavaScript here. I focus more on the approach than the JavaScript implementation. Playlist items need to be reordered.
00:29:53
We could reach for a jQuery plug-in or something, but browsers have come a long way in 2018 with built-in functionality for dragging and dropping. We’ll add functionality to an HTML element based on data attributes.
00:30:28
Here, we’ve given a URL to our table body of playlist items, which our JavaScript code will use when posting the reordering update. On the backend, we can keep a position attribute on the playlist item that allows us to manage the ordering.
00:31:09
Drag and drop will work nicely, returning a reorder flash message upon updating. This example exemplifies how using the request-response lifecycle pattern ensures synchrony across the front and back ends.
00:31:59
When updating a playlist item's position on the backend, we make sure that all the other playlist items remain consistent. Client-side reordering often risks getting out of sync with backend updates. So we are opting for a backend refresh for this view.
00:32:30
Even though it's more verbose, I wanted to share that deterministically checking if a record has changed after an update is critical for maintaining accuracy in displaying the order.
00:33:07
In our code, if the record's position did change, we rerender the entire list of records and stick them into the table to guarantee that we display a consistent view. If someone updates users concurrently, the whole list would refresh to keep the order correct.
00:33:47
As we reach the end of what I wanted to demonstrate, I did cut some parts of the presentation to allow for questions and discussions. I had begun implementing a combo box with the HTML5 data list component, which is interesting but carries its complications.
00:34:35
However, it is possible to provide a nice out-of-the-box type-down AJAX-based experience without relying on plugins or libraries. I want to emphasize leaning on standards, showcasing longevity in technology.
00:35:16
Lastly, I wanted to discuss what we didn’t implement here. Obviously, a lot of things were touched on. I would love to hear discussions about what UI/UX features we can perform with this kind of request-response lifecycle design.
00:35:59
Single-page applications are still the best solution for intense interactivity. If I were building something like Photoshop in the browser, this method wouldn’t be my choice. We also didn’t discuss paging, sorting, and filtering.
00:36:48
These topics present significant edge cases related to managing parameters while keeping the state of URLs consistent. While merging parameters can be done in JavaScript, I recommend using regular old GET or POST requests to leverage Rails' built-in capabilities with Turbolinks for performance.
00:37:28
That’s all I have. I managed to remain within my time limit. Thank you for attending another JavaScript talk at RailsConf, and please let me know if you have any questions.