Skip to main content

21 Message Templates

Episode 122 · June 7, 2016

Learn how to use a template model to populate forms and other objects with

ActiveRecord Javascript


Transcripts

Loading...

What's up guys, this episode I want to talk about creating message templates in your Rails app, and this is an example of something that Github recently released, where if you have like, for example GitHub projects often have like community guidelines for issues or pull requests, and so they have this kind of standard template of a comment that they end up posting, and so they've added these saved replies that you can see down here, and basically you just click on one of these and it will go and auto populate the comment box in order to save you some time by just automatically posting the message template and you can move on with your life and manage your project a little bit easier, so I want to talk about how we can go and create those message templates in our own application. This is something that you'll probably see a lot in things like Gmail or similar places where you would end up spending a lot of time creating these templates and reusing them. So what we've got is a little Rails application that just has a comment scaffold, and each comment has a text box and that's about it.

What we're going to do is we're going to add a button on there, and that's going to allow you to create a new saved reply or auto populate this text box with one of the ones that already exist.

Let's just dive in and take a look at how to build this. Now of course the first thing that we need to do is to create that secondary model for the saved replies, message templates, whatever you would like to call it, and I've already gone and created that very simply, so I created a saved message model, and it has a title and a body, and that's it. So the title is really just the way for you to reference this, a little easier, so you might give it a name, and then that's going to allow you to look it up in the dropdown without having to read that text, because that text could be multiple paragraphs long, so if you give it a title it's easier to display in the UI, and then we can go ahead and drop it in a lot faster, so what we need to do is modify that comment form, and we need to be able to create a new message template, so we can "Add a template" or "Add a saved reply", whatever you want to call this, and so you can just do that accordingly, so in my models, I called it saved message and we're going to just call that the same thing then, so new_saved_message_path, and then also, once we have added some saved messages, then we'll be able to add a select box that we can use with a little bit of JavaScript in order to prepopulate that text field. So let's go ahead and try this out. We'll have "Add a saved message", we'll give it a "Community Guideline" title, some instructions, would go there, create that same message, and then we can go back to the issues here. We'll have this on the database now, and we just need to build a select box in order to display that, now of course you can also add the user_id onto the saved messages so that these are grouped by the user, so other users can have their own templates, you can also do this if you have projects or something, you could group them by the project so that you or anyone else on the project could also use the same messages. So depends on how you want to build that in your application, but you could scope those accordingly relatively easily. So here we need to just build a select tag, and this is going to be our select tag for creating just the dropdown of those existing saved replies.

We're going to do the query, the database query inside the view because we don't really need to do any of this in the controller, it's fine to go ahead and do this because we need to prepopulate it with all of the messages every single time, so we'll just do that in a view and keep this really succinct.

If you haven’t used select_tag before, this is the regular select_tag, so it's not the form helper for it, and this basically just takes a name for the field, and then your option tags, and so you can use these options from collection for select in order to generate your dropdown options, so that's what we're going to use. Let's just give this a name of "saved_messages", and we'll use option_from_collection_for_select in order to do that. So we actually need to make sure that we can add data attributes in there, because what we're going to do is we're going to have the field that you see, the name will be the title, and then the value of that, well we could do the value in a couple different ways. We could have the value as the id, or we could have the value as the text of the body. So this is simple in our case because we have just one attribute that we want to insert into the message the comment below, but if you had multiple attributes and you wanted to set up a couple things, you might actually want to use data attributes, and I believe that in the docs here, in the comments below, there is a data attribute thing, so that allows you to put data attributes on the select_tag itself, but the options for collection, let's see if we can find that options_from_collection_for_select, we want to be able to add data attributes into this, and it maybe isn't the one that allows you to do that.

I know that if you use the options_for_select instead, and you do the mapping yourself, you can add data attributes in there. So I think what we'll do is use the options_for_select instead, so options_for_select is really simple, it just takes an array, and that means that we just want to load up all our saved messages, and map that to an array, and so this array is really simple, the array is what you want to display, and the actual value. So you'll see that dollar shows up as a text in the option, and value is set to the dollar symbol.

So what we'll want to do, is that we'll want to have the

<%= select_tag "saved_messages", options_for_select(SavedMessages.all.map{ |sm| [sm.title, sm.id, data: {body: sm.body}] }) %>

Let's check this out, and see if that worked. We can go back to our code, and we'll see that I complied templates, saved messages. We shouldn't have pluralized that, so we should access the model. So we have the community guidelines here, and this is good, and if we inspect the option, what we're looking for is that data-body, so this is the data-body that we can use and then you can add your own other attributes in there, just by passing in data-body, and so if you pass in a third option into this array, the options for select interprets that as extra attributes to put in the html, so you could put in whatever else you would like to show up in the attribute on that html tag, the option tag.

This is cool, this is making good progress for us, we also need to make sure that we include a blank option. So we need to be able to select one of these, we don't want the default one being the first one that's already set up there. So what we're looking for is... We can go back to the select_tag itself, and we need to go back to the original select tag, and this is going to have an option for prompt or include blank, so we could use either one of those to display a default that has no value, and I think what we're going to do is we're just going to set the prompt, and so this prompt is going to go in the options hash here, and prompt is just going to be: "Select a saved message", and I realize I made a typo here, and we need to close the parenthesis on the options for select before the prompt so that this prompt gets passed into the select tag.

If we refresh this page now, we should see: "Select a saved message", and we can do that, and all we need to do now is add a little bit of JavaScript in order to take that selected item, and then grab the body attribute, data-body attributes and then populate the body itself. We're going to use the approach that I've used in the past of creating a JavaScript class in CoffeeScript that wraps the form that adds this functionality in, so we can use the data-behavior attributes in order to organize all of this easily, and the reason for that is because if you have multiple comment boxes on the page, and you use ids or even classes, when you click on this "Saved Message", you would actually be inserting that to everything on the page, or only the one, or you'd get weird conflicts and stuff, so what we do is, the reason for using this data-behavior stuff at the class level is to say: If we have a form, let's only care about the stuff inside of this form, so we'll scope all of our JavaScript to work on this form itself, so it doesn't matter if we have multiple forms on the page, they all operate independently with the JavaScript. So we'll do, is we will go set some html here and we'll set data behavior, and this will just be has-saved-messages, or allow-saved-messages, whatever you'd like, so we'll use that in order to write our JavaScript to detect the forms on the page, each one of those will operate independently, so we'll create a new instance of a has-saved-messages JavaScript class, and then we will go add a

data: {behaviour: "saved-messages-select"}

and then the body will have

data: {behaviour: "comment-body"}

This is all we need to do in order to set up our JavaScript. Before we move on, make sure that you close all of your curly braces at the top.

Now let's open and create an app/assets/javascripts/saved_messages.coffee, and here we'll just have jQuery, and when this loads, let's create a class called HasSavedMessage, and the we'll be able to define our classes in here and instantiate these saved message classes after we go look them up on the page. So what we're looking for is really just the data behavior tags on a page that match that tag that we just set. So let's pull up the form side by side so you can see that we want the: has-saved-messages, and this will allow us to grab all of those on the page, and let's just console.log this, so we can see that. And I'm going to comment out this JavaScript.

What you'll see if you go to the console, you will see that you get this line and you get these items, so what we see here is that there is one record in this item number zero, and it is that form that we selected, so if you click on it, it will actually take you to the form element, which means that we know that we grabbed that correctly and we can go ahead and start instantiating the has-saved-messages class, we can pass in the element that we're looking for, so we loop through each of those, create a new class for that, and then add our JavaScript. This part is pretty straightforward and what we're going to do is pretty much the same as my previous episode on refactoring your JavaScript, what we're going to do is map the array, so this selector returns an array of elements on the page that match that attribute, so we're going to map that array, convert that into a list of elements that are actually instances of the HasSavedMessage class.

new HasSavedMessage(elem)

and then this is going to need a constructor that accepts the element, and we'll just save that element and then we'll be able to go ahead and instantiate all of that. So this is going to map all of those, and then if you want to, you can save this. So you could say this is like: saved_meesage_forms if you need that; in this case we don't actually need that, we just need to make sure that we create all of those when the page loads, and so here now we can add in our callbacks or listeners or any of that stuff, so our element here is going to be important. We'll set that equivalent there, and we'll set setCallback method, and we'll call that, setCallbacks, call that when it gets set up, and so this is really just going to be simple, and this is going to just define when you click on the select tag, let's grab the currently selected item, and let's go then find the body, and then go ahead and add in the text from the select tag into that. First of all, we need to make sure that this is a thin arrow function, the reason you would want to do a thick arrow with the equal sign here is that if you were doing a callback from an AJAX method or something like that, anytime you're using a callback, you can actually use coffeescript's thick arrow in order to keep the same this variable, so you're in the previous context. We're not going to be doing that, but we do need to make sure that this element is a jQuery object so we can use jQuery with it, and so here we just want to say the element, we need to find inside of that element, we need to get:

/app/assets/javascripts/saved_messages.coffee

setCallbacks: -> 
    @element.find("[data-behavior='saved-message-select']").on "change", @handleChange 

So we'll make a handleChange method here, and this time we'll use a thick arrow because we want to stay in the context of this object instead of the called app, so we should be able to access the event here, so we'll have that, and let's just print that out and see if clicking on, and changing that dropdown actually sets that. So let's click on this, and you'll see that it does, and we get the event and everything works. Now we should be able to say when this changes, let's go set the selected item, so here we could say:

/app/assets/javascripts/saved_messages.coffee

handleChange: (e) =>
    saved_message_text = @element.find("[data-behavior='saved-message-select']").find(":selected").data("body")
    comment_body = @element.find("[data-behavior='comment-body']")
    comment_body.val comment_body.val() + saved_message_text

By doing that, we should be able to now click community guidelines, and see that our text gets automatically inserted into the body. You can make any adjustments that you would like, and that will automatically work, so we can go add a new saved message, and this could be just a test, and it has different texts and we can go back to the home page here, and then we can click test, and it will insert that, you can also go and add in multiple of these, we should probably prepend a new line at the beginning or something like that, or a new line at the end, actually. So let's go and do that, let's just say:

The code above is already updated

We'll use the sting interpolation that CoffeeScript gives us, and add a new line at the end, and so if we refres, now we'll be able to add in one of these, and another one, and it will just append it without merging those two lines together, this is cool and allows you to go build those saved messages, and then automatically have all of that functional, and then this will also allow you to do things, like if we go to the comment/index, we can render this form multiple times, and each one of these will work completely independently of the others.

This is neat, because it allows us to build a little bit of JavaScript, not much, and it's pretty well organized, we can have this working independently of each other, you can take this and kind of work with that and build a lot more extra stuff. Now this is an example of using message templates that are sort of dynamic, and they insert onto the page, what we could have also done, is had you as a user go to the saved messages first, and then you could add something to the URL so that when your comments controller does the Comment.new, you can actually pass in your attributes here if you wanted, so if you had in the URL the saved message that you wanted to prepopulate with, you could actually go ahead and do that with just using your Ruby code to set the attributes of the new. But that is something that's a little less common, because often times you want this to be dynamic on the front end, and you want this to be relative to the form that you're looking at. So you don't want to force a user to go somewhere else to the saved messages first, and then try and come back and create that comment. We want to do it all in the one comment itself. So that's an example on how to do this. If you're interested in talking about a little more advanced stuff where we could talk about building templates for documents, so a good example of that might be themes or anything like that for Google Docs, or PowerPoint or Keynote, they have a whole lot of settings that you have to go choose first, and then you create your document, whereas this you create your document or your comments first, and then you insert those prepopulated fields, so Google Docs, and PowerPoint and Keynote kind of do it in reverse, where you pick the template first, so if you're interested in seeing that approach, let me know in the comments below, we can do that in a future episode. 'Till then, talk to you in the next one. Peace v

Transcript written by Miguel

Discussion


Gravatar

super keen to see this the other way - so you pick the inputs before generating the template. Thanks Chris


Gravatar

thanks Chris love this one, got it working in mins.


Gravatar

Chris, what are you thoughts on keeping the template records inside the same table instead of a separate one? I know this wouldn't work for all situations, but I have a situation where 5 tables each have templates, so I have 10 tables to maintain and they're essentially duplicates of each other (one for the live data and the other for templates).

In order to reduce the DB complexity and cut down on maintenance, I'm thinking about combining the template tables into the normal tables and adding a `template:boolean` field.

That way there's only 1 version of the models to maintain and DB changes don't have to be kept in sync between multiple tables.

Gravatar

Yep, you can do that and use scopes in order to filter those as necessary.

Another alternative is a single Template table that stores templates for any object, where you store all the values in a serialized text, hstore or json column. The downside to this is that sometimes you can run into issues with data types not being consistent in the serialized ones.


Gravatar
It works well with text area, but if I use rich text editor, the text wont show on the text editor.

Login or create an account to join the conversation.