Showing posts with label jQuery. Show all posts
Showing posts with label jQuery. Show all posts

Friday, January 2, 2015

Using jQuery Plug-ins With Angular (jScrollPane)

I needed to allow for a particular part of a page I was working on to scroll independently of the page.  Typically you would just use overflow-y: auto and be call it a day.  However the default renderings of these scroll bars stick out like sore thumbs in the middle of a page.



So I quickly googled to see if there were any awesome angular.js directives that allowed you to style scrollbars.  Much like the flyout, confirm dialog, and dropdown, I didn't find much online.  I did find a bunch of posts on jQuery controls that have nice scrolling like jScrollPane.  I figured that instead of creating one from scratch this time, I was more curious on the proper way of integrating jQuery plugins with angular.js.

I found this video that shows an integration with the chosen jQuery plugin, which incidentally was something I was looking at when I decided to create my own drop down, and I decided to watch it.  The video is pretty good, but regardless if you watch it or not I'll show you how to integrate a jQuery plugin with angular.js.

So after viewing that example I figured it would be pretty easy.  It should be just adding the JS files and creating a directive that applies the plugin to the element in the link function.  It pretty much boils down to the following directive.
 
.directive('NAME', function(){
    
    function link(scope, element){
        // you can use the angular.element instead
        $(element).PLUGINFUNCTION();

    }    
    
    return {
        link:link,
        restrict : "A"
    };
});

Just replace the 'NAME' with a proper name for the directive, and 'PLUGINFUNCTION' with the actual function to execute the plugin.  I have it restricted as "A" so you can just add the attribute to whatever element you want it to be on.  So to use the jScrollPane plugin you can just use this.
 
.directive('scrollPane', function(){
    
    function link(scope, element){
        // you can use the angular.element instead
        $(element).jScrollPane();

    }    
    
    return {
        link:link,
        restrict : "A"
    };
});

I tried it out in a fiddle real quick and it worked fine.  However when I tried to use it in my application it wasn't working.  After thinking about it for a bit, and reading some additional  documentation for the jScrollPane, I realized the issue was, like most JS heavy applications, were being driven by data from services, or in my case being asynchronously loaded via websockets.

So now I found the function to reinitialize the plugin, but I needed to figure out how to inform the directive to do it.  I ended up deciding to use a built in angular functionality to do so.  The scope.$broadcast and scope.$on  seemed to be exactly what I was looking for.  Whenever what was driving the content changed it would send a message down scope using $broadcast, and $on would listen for the message.  That made me feel good about the separation of concerns.  Essentially the scrollPane directive could just listen for an event from somewhere up the parent scope and whatever was driving the data or re-sizing of the page could drive the sending of the message.  I ended up just using a generic "refresh" message name for it.  Here is the end result for the directive:

 
.directive('scrollPane', function(){
    function link(scope, element, attr){
        
        var $element = $(element),
            api;
        
        // element.jScrollPane();
        //In real world Angular would replace jQuery
        $element.jScrollPane();
        api = $element.data('jsp');
        
        scope.$on('refresh', function onRefresh(event, args){
            api.reinitialise();
        });
        
    }
    
    return {
        restrict: 'A',
        link: link
    };
});

Here is the end result:


Thursday, April 3, 2014

Application-Wide JavaScript Autocomplete for Rails

For my skills application site I wanted to add some more front-end JS code, but wanted to start with something simple.  So the first thing I could think of that was pretty easy to do was adding an autocomplete to a couple of the search boxes.  The search boxes allowed  you to search based on the skill name.  Unfortunately there are over 1500 skills.  So making sure that you query the right skill is hard to do.

To create the autocomplete I decided just utilize jQuery UI's autocomplete control.  For my initial pass I decided to not use the service based approach and go with a static source of values.  To access these values I decided I would put the list of values in a data attribute on the actual autocomplete control itself.  The JavaScript would just then pull those values from that attribute.

Here is what I came up with:

  <%= text_field_tag :skills, params[:skills], :id=> 'skills_autocomplete',
        :data => {:skills =>  SkillTotal.retrieve_skills() } %> 
<% content_for :javascript do %>
  <script type='text/javascript'>
    $(document).ready(function(){
      $("#skills_autocomplete").autocomplete({
        source: $("#skills_autocomplete").data("skills")
      });
    });
  </script>
<% end %>

It ended up working out pretty well, however in my form JS I was querying the control by its ID.  Every time I needed an autocomplete I would essentially have this code on each one of my views.  I decided that this was good, but I could do better.  I wanted to make it more DRY.

I created a global.js file where I would put an on load function and set up the autocomplete so I didn't have to put it on each form.  Instead of going by the ID, I went by a class as well.  In the end it looked like this:

<%= text_field_tag :skills, params[:skills], :class=> 'autocomplete',
        :data => {:autocomplete_data =>  SkillTotal.retrieve_skills() } %>
(function(global){
    global.App = global.App || {};
    global.App.autocomplete = function(){
        $(".autocomplete").each(function(){
            var field = $(this);
            field.autocomplete({
                source: field.data("autocomplete-data")
            });
        });
    };
}(this));

$(document).ready(App.autocomplete);
I soon found out that my code wasn't working.  A little bit of digging and this has to do with the turbolinks gem which is included by default with Rails.  A little more digging and I now my JS looks like this and is running on multiple pages.

(function(global){
    global.App = global.App || {};
    global.App.autocomplete = function(){
        $(".autocomplete").each(function(){
            var field = $(this);
            field.autocomplete({
                source: field.data("autocomplete-data")
            });
        });
    };
}(this));

$(document).ready(App.autocomplete);
$(document).on('page:load', App.autocomplete);