Skip to main content

24 Select Or Create Field With Selectize.js

Episode 178 · March 21, 2017

Select a record or create a new one for an association in a form using the Selectize.js library

Javascript


Transcripts

Hey guys, this episode we're talking about creating a select or create box in your Rails app.

One of the user experiences that you'll have in a lot of applications, is that you need to have a multi-select box, and you sometimes need to be able to create new records along with that. So for example, this is maybe a blog, and we need to be able to add categories here, and it would be dumb of us to ask the user to create categories first before they create the blog post. It's intuitive for them to write their blog post, and then add categories, but they might not have those categories already in the database, so what do we do then?

Often times, we'll have a select box like this, and we can check our items off, and add them to it, and there's no good way of adding a create along with it, so sometimes you will see this box gets shorter, and they stuff a "Create" button next to it, on the side, or underneath it, or something like that. And that can work, and you can click that button, and loads a modal or whatever, and displays a new for the category, and once that form is submitted, then it adds that to the select box. Now that is ok, but it's not as intuitive, especially when you have a whole lot of options. For example here, if we were to add JavaScript, it would be more intuitive for us to have an interaction like this, where you could say: Add JavaScript in the select box which will trigger a modal that you can fill out, and then submit this, and have it added to the categories list.

That's what we're going to be talking about in this episode today to create, or select an all-in-one, so that it's more intuitive for the user to add new categories to their form.

First off, let's take a look at the database structure that I have for this application, so that we can see how the form is going to work. This is the way that I've structured it, you might actually put categories immediately on to those records or something like that, but I have a joined table here, which is going to easily allow us to make that other form, for the categories, and it allows you to type in their name and their description, and then we associate the categories to each other using the

**app/models/post_category.rb

class PostsCategory < ApplicationRecord 
  belongs_to :post 
  belongs_to :category 
end 

We have a has_many :through, and that allows us to use the same category on multiple posts, and every post can have many categories as well. So we have a many to many relationship here, and that is all we need.

Now the JavaScript library that we're using for this, is actually one called Selectize, and we have probably talked about selectize in the past. This JavaScript library allows us to add the nice select box here for us, and this can do all kinds of things. You can have extra descriptions, like names and emails, you can have ones that don't have emails, and have that extra description in it. And I believe here somewhere at the bottom, the JavaScript for the GitHub option has a lot of extra stuff, including the language, and the number of watchers, and forks and all kinds of things, which can be useful if you were wanting to build a much much more robust dropdown. So this is the gem, or the library that we're using, and we're using the gem for it, so that we can include that in Rails easily. Once we've added that gem to our Gemfile, we can run bundle install to install it, and we can go over to our application.js while that is installing, and we can require selectize, and we can go into our application.css and we can

*= require selectize 
*= require selectize.default

The default file will be the theme that we'll use, which is that selectize default. Once we have both of those in, and we have our gem installed, we can restart our Rails app, and we should have access to that CSS and JavaScript in the browser. So before we check this out in the browser, let's take a look at our app/views/posts/_form.html.erb which is a typical scaffold form, we have the form for the posts, we have the error stuff at the top, and we have our fields for each one of those items that we want to display. Now our category id's are for that join table, and we have loaded all of the categories, and plugged the name and the id, and that will create an array of name and id for each category, and make those as options on that form. So we can refresh our browser, and see that form, but we'll see that it is obviously not using selectize, and using the built-in field for the select with the multiple is true option.

This is what we want to make sure that we can selectize, and we can add a class to this called "selectize" and while this won't do anything out of the box, we can use that selector for the class in some JavaScript to actually make sure that selectize gets loaded properly for that select box. So let's go do that. Since this is posts/_forms.html.erb, I'm going to edit:

apps/assets/javascripts/posts.js

$(document).on("turbolinks:load", function()  {
  $(".selectize").selectize();
});

This should initialize our selectize for us, and because our javascripts/application.js does a require tree, we know that posts.js is going to get loaded, and then we should be able to go into our browser, and see that it did. Everything is working out of the box, but we don't get that option to actually add new items here, and that is what we're going to work on next. So we can try spaces or commas, and it doesn't add that new item, so we need to be able to go and add those options into selectize to pull this off. Now there's a lot of stuff that we have to do in preparation for that, for example, we want to make sure that we have the category form on this page somewhere, so that we can hide that by default, and then show it whenever you click create on the selectize option. So we have a lot to set up to pull all of this off, but bear with me, we'll get through this, and you will have that cool functionality in your app.

First thing's first, I want to go back to the posts/_form.html.erb, and at the bottom here, I want to paste in some code for the category form. So this is actually a bootstrap modal, all the html for that, and this is going to be a category modal, there's probably a better name for that, but this is basically just a simple html modal that comes from bootstrap, that will be hidden by default, and inside of the modal body, we have a form_for and a new category, and you can type in the name and the description, and there is a submit button. What we want is to be able to save this inside that form partial, because we always need that as well, so we can keep it in the same partial, and make sure that any time that that is added to the page, they're both included together. Then we can use JavaScript to take a look at this and call the modal and make it show, so if we go to our browser we can check and make sure that that's working by saying

$(".category-model").modal()

and that will trigger the modal to show up, so our JavaScript line for that was super short, we just called modal on it, and that does the toggle on and off.

We need to actually trigger that modal to happen whenever our selectize field is creating a new item. So we can pass in some options into selectize, namely a creep function, which can accept a callback and an input. And both of those can serve different purposes, the callback is especially important. If we were to save this, and go back to our browser and refresh, and we type in Java or JavaScript or whatever, if we click that, we can no longer edit this field. I can type a bunch and nothing is going to happen, and that's because selectize knows you want to go do a create somewhere else, for example in a modal, and it's waiting for you to actually finish that, and return that value of the new item you created. So you actually have to make sure you call this callback, and you can pass in nothing if you want, or you can pass in the value and the id of the new item that you created. So for example, if we do nothing, and we just say "callback", then we can refresh this page, and type in "Java", and click that button, and we can continue typing, which is great. So that means that it knows that we're done creating a new record because we called that callback. So of course, normally we don't want to call this callback with an empty object, we want to actually pass in the new category that we created from that form, so we have to set that up, and so this process is going to be a little tricky to set up, because we also want to make sure that we trigger that modal, and we want to do several things with that. We want to take the data from the modal, submit it, receive the JSON back, then take the JSON and call this callback with that, and if any of that fails, or the user clicks out of the modal, we want to be able to call this callback with no values and reset that form and allow the user to go about their business.

So let's start by triggering that modal, so we'll have

apps/assets/javascripts/posts.js

$(document).on("turbolinks:load", function()  {
  $(".selectize").selectize({
  create: function(input, callback) {
  $(".category-modal").modal();
  }
  });
});

When you type in JavaScript, we want this to trigger the modal, but we also want to default that name to being JavaScript, and we also want to make sure that we clear out the old data. So let's take a look at this, and show you what I mean. So when we go into this, and we say: JavaScript, we click this. We want that name field to be filled out, so we could actually inspect this, and we could say: We're looking for that ID of category name, and we want to fill out that field, so we can say:

apps/assets/javascripts/posts.js
$("#category_name").val(input)

If we do this, and we refresh our page, we can type in JavaScript, click that button, and then get JavaScript in there, which is AWESOME :thumbs_up:

The next piece is that we of course need to listen to the submit of that new category form, so we have that new category if we inspect the html, you can see of course that that is the name of the form inside of the modal so if we dig into that, we can then grab that ID from our category form, and that's what we're looking for here, and we want to put an

$("#new_category").on("submit", function(e){

})

we want to prevent default so that we don't get the standard submit, and we can actually use an AJAX submit, so if we do AJAX, we can use jQuery to create an AJAX request for us, and we want to say method is POST, and the URL for this of course will be that same action, so we're going to get the adder for the action on it, so if you're following, this is using this variable, which represents the new category form. We're looking for the attribute on there called action, and then we also want to do `this.serialize()`, take all the fields, and create a serialize string so that we can pass that over as the data for our POST request. 

And finally we have our success callback which will take a function and it will get the response from the server side, and that is what we want to process here, so we want to call the callback with the new item here, and then we want to close the modal and do anything that we need to do to clean up, but the most important thing is to make sure that we call that callback from above so that we can have that set up here. So this response value is actually going to be the JSON that we get back from the server side on the categories create action.

>Ok, recap up to here for the **app/assets/javascripts/posts.js**
```JavaScript
$(document).on("turbolinks:load", function() {
  $("#new_category").on("submit", function(e) {
    e.preventDefault();
    $.ajax({
      method: "POST",
      url: $(this).attr("action"),
      data: $(this).serialize(),
      success: function(response) {
        selectizeCallback({value: response.id, text: response.name});
        selectizeCallback = null;

        $(".category-modal").modal('toggle');
      }
    });
  });

  $(".selectize").selectize({
    create: function(input, callback) {
      selectizeCallback = callback;

      $(".category-modal").modal();
      $("#category_name").val(input);
    }
  });
});

So we need to then go to our categories controller to make sure that's set up correctly. Now I've taken the typical create function, and instead of rendering html or anything like that or JSON, jbuilder response, I actually just have it set up to render JSON for the category itself. So this will just take the attributes and then convert them into JSON, and that's as simple as that is. You could use ActiveModel serializers or whatever you want to take this object of the category and convert it to JSON. I'm just using the built-in attribute conversion and keeping it simple, but you just need to make sure that you return the id, the name and that's it, because we don't care about this description or any of the extra fields, we just need to have the ones that we want selectize to be able to use in it's template for being added there. And so the default of course is just name and id, and that is all we need to get back.

Once we know that we should be getting that information back, we can then do something simple as console.log(response), and print that out on the screen and make sure that that is showing up correctly.

So we should be able to refresh our page now, we should be able to type Java. Create the Java category, and "this is a programming language" as our description, we should be able to click create, and if everything went correctly, our console should log that object. And it does, and we get the id, and the name, the created at, and all those attributes return back to us, and this is actually a JavaScript object so we know that we are getting a JavaScript object in return that we can access the id and the name values through that nice and easily.

This is working as we expect, and if you go into the Rails logs here, you can see that the POST happened, it submitted the category data over, all that, it inserted it into the database, and then 200 ok with only 1ms abuse which was rendering that JSON of this categories attributes. So that's as simple as that is, and gives us that function that we can now pass back to the callback, so we'll have to be able to take that response data and put it in the format that selectize expects, that callback function expects an object with two options in it, one is value and one is text, so of course value should be the id of the object, so response.id, so that will be the category id, and text will be what gets displayed which will be response.name, which will be the category name, and that is all that we need to do to trigger that callback, and MAKE THAT SELECTIZE FUNCTION GREAT err... FUNCTIONAL AGAIN! but we also want to make sure that we trigger that modal and we hide it. So here we can say modal('toggle'), and that will make sure that it gets hidden, or of course, you could use the height, or whatever the other option is.

I'm going to use toggle here, which should then add the item to our field, and then hide that modal, and we should be good to go. So let's take a look at this, with JavaScript. Add JavaScript: "This is a web programming language", Create JavaScript, and you can see that it was added as the category automatically inside of the selectize field. So that is adding that new item into it. If we were to inspect this, you will see that if we find that select, which is hidden away, we have one option in it. Option with the value of 10, and that is the new id of the JavaScript record that we just created. So this works really well except for a couple different things. If we were to go in here and say: Add a new language, and we wanted to say Python, we could add Python, and then: "Oh! It still has the description in there", but it overrode the name.

Wee need to actually reset this form, and our button has been disabled as well. So the Rails UJS JavaScript is actually disabling the button for us when we submit that function. So we need to go then reset that as well. So we need to do a little bit of clean up in our JavaScript to make this functional. After we toggle modal, we can add

$.rails.enableFormElement($("#new_category"));

If we add that with jQuery, we can pass that into the Rails enable form elements, and that should take care of resetting that form button for us.

The other thing, is that we wanted to clear out that description field at the beginning when we create a new object, or at the end when we close that modal, so I'm going to do this actually when we create the new item, so we can leave the old value in there. But as soon as the form gets loaded again with a new "Create", we can clear it out at the beginning.

So I'm going to add up at the top here of the create function we'll saying

$("#new_category").trigger("reset");

Which should clear that out and give us that ability to have a clean form after we add a language. Let's add this and see what happens. We add Python, create that category, and if we created a new one, such as "Django", we now have a clear form, and we can create Django with a description there as well. And so we have the creep working just fine and all that is doing well, it's cleaning up the form and everything, but what happens when you want to cancel creating a new category?

Let's try something like, maybe you want to do "ArnoldC", if you haven't seen ArnoldC that language is awesome, but what if we wanted to close this? Well we can no longer type in this field, and there's nothing we can do, it is now currently disabled, and probably disabled until you refresh the page. So we need to add an additional event to listen to the modal close, and if that callback exists, then we want to call it and clear anything out. That means that we need to create a variable outside of this, and we can say

var selectizeCallback = null;

and:

var selectizeCallback = null;

  $(".category-modal").on("hide.bs.modal", function(e) {
    if (selectizeCallback != null) {
      selectizeCallback();
      selecitzeCallback = null;
    }
selectizeCallback = callback;

...

selectizeCallback({value: response.id, text: response.name});
selectizeCallback = null;

What this will do, is we will make sure that we have this variable up here, that we have access to, and it's probably best if we put that here as well inside the bootstrap, or inside the Turbolinks load event, then we have the variable scoped properly inside of that. This selectize callback can call this after it's been set up here, and this success never happens, and we may be never submitting that JavaScript for the submit. Then this will call that callback and set it up, but we also need to make sure that selectize callback here is equal to null afterwards as well. So each time that you call the callback, you should clear it out so that this is set to null, and we know that it's no longer in the state of creating a new object.

That also points out something that we can do, we can move this category submit function into another place. So let's grab all of that, so we need this whole thing, and we can put that out here as well, so if we move that in, then we can have our callbacks on the same level, and you can see that the selectize create isn't doing as much crazy work as you though it might have, but that is because our submit function is actually a lot more compartmentalized now, now that we have that callback defined outside of the selectize function. So because we're setting this variable, and the variable exists outside, then we have access to it here, and the scopes will play nicely so that we can use that without having to keep it inside of the selectize create anonymous function here.

We should be able to add a new language, such as "Swift", "This is for iOS development", we can create a category, and we should be able to go and add say "ObjectiveC", and try and do this, but then maybe we want to close it out, and we want to add "C#" instead, so we want to do that, and all of our buttons and everything have been reset accordingly, C# has been set, and when we close that out, we'll be able to type again inside of our form. Which means that our callback and everything is being created, it's being cancelled out whenever we close that form, and whenever we create a new object and close the form automatically, then that callback is being created, and then set as null, so everything is being taken care of cleanly in our JavaScript.

Our JavaScript for this turned out pretty well, we have a selectize function that has a create action in it that triggers our modal and resets the forms, and once that form has been submitted, we then take that data, we submit it with AJAX, and then we tell selectize: Hey, here's our new item, go ahead and add that to the form and close our modal, and everything is good, and we also have the ability for us to, when that modal is closing, actually go and call that callback and reset selectize if we need to, in order to handle the cancellation event. So we have those three things nicely separated in our code here, so you can independently edit any of those that you might like, for example: When you close that form, you might actually want to make sure that in there, we have the form element automatically reenabled, and you might want to issue your reset there as well since that's the one place that will happen every single time that modal gets closed, you can have that code in that single place rather then spreading it out on "Create" or initializing the modal or after submitting it. All of those then could work, but you can have a much more succinct singular place to put that whenever the modal closes to clean up.

Well that's it for this episode, if you want to use something other than a modal, you have full control over that, you just need to make sure that whenever that secondary form gets submitted, you handle calling the selectize callback there, and instead of calling modal's, and displaying and hiding those, you can have another form from somewhere else on the page or whatever you like, you have full control over that, you're just going to need to make sure that you can call that selectize callback with the new data, or call it without any data in order to cancel that depending on however you want to set up your forms. So you don't have to use modals for this, but they do provide a really easy way of going and building that separate functionality to go create that new item and add it into your forms. So I hope you enjoyed this episode, and if you want to see more like this, let me know in the comments below, and I will talk to you in the next episode. Peace v

Transcript written by Miguel

Discussion