Rader on Rails

Dispatches from my web development journey.

My First Open Source Contribution

For a while, I’ve been meaning to blog about my experiences learning web development. I’ve delayed it for so long, probably because I realized that the more I learned, the more I realized just how little I know, which made me feel like I’d have nothing of much value to contribute. But, with a mindset like that, I knew I’d never actually start this blog, so I decided I’d just swallow my pride and begin.

What this blog is meant to be then, for those who stumble upon it, is NOT a professional resource. I’ll post solutions I’ve come up with to certain challenges with the understanding that there might be far better ways to accomplish them. What this blog is meant for is to encourage others thinking about learning this new skill and let them know that it’s accessible to them as well. If I can learn it, so can you. Also, if veteran developers visit the site, I welcome your recommendations for improving my solutions and explaining why some things operate the way they do. That way, we all win.

So while it may be long before I make any really impactful contribution to this vast world, I did have a small victory a couple days ago when I fixed a Ruby gem and submitted a pull request to the original author. And today, the fixes must have been good enough, because he merged them into his master branch. Awesome!

For the past few weeks, I’ve been working on a project challenge through my online web development course with Bloc.io. The project involves creating a site where users can create wikis and share them with other users.

In order to add some easy styling to users’ wikis, the project specs recommend using the Redcarpet Gem, which is very easy to install and set up. But for an extra challenge, they recommended installing a gem that would allow you to preview your markdown stylings while editing your wiki.

They recommended a gem called Markdown Preview, written by Jeff McFadden which has a very simple installation and setup. After following the steps and firing up my Rails server, however, it didn’t appear that anything was actually happening. I noticed that the gem installed a JavaScript file that was presumably doing the heavy lifting, so I used Chrome’s developer tools to inspect.

Right off, I noticed a warning that the file, written in jQuery, was using a deprecated event handler, .live(). From a quick Google search, it looks like this event handler has been removed, and handlers such as .on() or .click() are recommended for use.

After replacing each line with this syntax: $('.some_class').on('click', function(){}), I stopped getting errors and a toolbar that would allow me to edit and preview my text appeared, and things almost seemed to be working… except that they weren’t. The preview, edit and help buttons didn’t seem functional. I decided to take a close look at each line of the code to see if I could piece together how it was supposed to work.

I’ve been mostly focused on learning Ruby and Rails, and while I’ve had some experience with JavaScript and jQuery, I could definitely benefit from studying these a lot more. This old file certainly helped in that endeavor. Here it is:

markdown_preview.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
(function( $ ){

      $.fn.markdownPreview = function( type ) {

        return this.each(function() {

          var $this = $(this);

          $this.wrap( '<div class="markdown_wrap editing"></div>' );

          $this.before( '<div class="markdown_wrap_menu"><div class="markdown_wrap_menu_help">Help</div><div class="markdown_wrap_menu_edit">Write</div><div class="markdown_wrap_menu_preview">Preview</div></div>' );

          var help_text = [
'<div class="content cheatsheet">',
  '<h2>Markdown Cheat Sheet</h2>',
  '<div class="cheatsheet-content">',
  '<div class="mod">',
    '<div class="col">',
      '<h3>Format Text</h3>',
      '<p>Headers</p>',
      '<pre># This is an &lt;h1&gt; tag',
'## This is an &lt;h2&gt; tag',
'###### This is an &lt;h6&gt; tag</pre>',
'    <p>Text styles</p>',
'    <pre>*This text will be italic*',
'_This will also be italic_',
'**This text will be bold**',
'__This will also be bold__',
'',
'*You **can** combine them*',
'</pre>',
    '</div>',
    '<div class="col">',
      '<h3>Lists</h3>',
      '<p>Unordered</p>',
      '<pre>* Item 1',
'* Item 2',
'  * Item 2a',
'  * Item 2b</pre>',
'     <p>Ordered</p>',
'     <pre>1. Item 1',
'2. Item 2',
'3. Item 3',
'   * Item 3a',
'   * Item 3b</pre>',
    '</div>',
    '<div class="col">',
      '<h3>Miscellaneous</h3>',
      '<p>Images</p>',
      '<pre>![GitHub Logo](/images/logo.png)',
'Format: ![Alt Text](url)',
'</pre>',
     '<p>Links</p>',
     '<pre>http://github.com - automatic!',
'[GitHub](http://github.com)</pre>',
'<p>Blockquotes</p>',
     '<pre>As Kanye West said:',
'&gt; We\'re living the future so',
'&gt; the present is our past.',
'</pre>',
    '</div>',
  '</div>',
  '<div class="rule"></div>',
  '</div>',
  '</div>' ].join( "\n" );


          $this.before( '<div class="markdown_wrap_help">' + help_text + '</div>' );

          $this.wrap( '<div class="markdown_wrap_content"></div>' );
          $this.after( '<div class="markdown_wrap_preview"></div>' );

          $this.wrap( '<div class="markdown_wrap_editor"></div>' );

          /*
          if ( !type || type == 'width' ) {
            $this.width( $this.width() );
          }

          if ( !type || type == 'height' ) {
            $this.height( $this.height() );
          }*/

        });

      };

      $( '.markdown_wrap_menu_help' ).on( 'click', function(){
        //console.log( 'Clicked Help' );
        $( this ).closest( '.markdown_wrap' ).toggleClass( 'helping' );

        $( this ).closest( '.markdown_wrap' ).find( '.markdown_wrap_help' ).slideToggle( 'fast' );
      });

      $( '.markdown_wrap_menu_edit' ).on( 'click', function(){
        //console.log( 'Clicked Edit' );
        $( this ).closest( '.markdown_wrap' ).removeClass( 'previewing' ).addClass( 'editing' );

        $( this ).closest( '.markdown_wrap' ).find( '.markdown_wrap_preview' ).hide();
        $( this ).closest( '.markdown_wrap' ).find( '.markdown_wrap_editor' ).show();
      });

      $( '.markdown_wrap_menu_preview' ).on( 'click', function(){
        //console.log( 'Clicked Preview' );
        $( this ).closest( '.markdown_wrap' ).removeClass( 'editing' ).addClass( 'previewing' );

        var editor  = $( this ).closest( '.markdown_wrap' ).find( '.markdown_wrap_editor' );
        var preview = $( this ).closest( '.markdown_wrap' ).find( '.markdown_wrap_preview' );

        preview.html( 'Loading...' );

        editor.hide();
        preview.show();

        var editor_content = editor.find( 'textarea' ).val();

        $.ajax({
          type: 'POST',
          url: '/markdown_preview/convert',
          data: { 'format' : 'json', 'markdown_text' : editor_content },
          success: function( data, textStatus, jqXHR ){
            preview.html( data['html'] );
          },
          error: function(jqXHR, textStatus, errorThrown){
            //console.log( "ERROR" );
            //console.log( jqXHR );
            //console.log( textStatus );
            //console.log( errorThrown );
          },
          dataType: 'text json'
        });

      });
    })( jQuery );


    $( document ).ready( function(){
      $( '.markdown_preview' ).markdownPreview();
    });

Yikes, that’s a lot of jQuery. Now I’ll go through how I figured out what it was doing and how to fix it:

First, near the bottom is this block:

1
2
3
$(document).ready(function(){
  $('.markdown_preview').markdownPreview();
});

I knew from my limited study of jQuery that what this says is upon complete loading of the document (you could think of this as upon complete loading of the page you’re viewing), you find the element with the class '.markdown_preview' and then execute the function .markdownPreview();.

The function markdownPreview(); is a jQuery plugin. I knew before coming across this that plugins existed, but I had no idea how you actually create them. If I was going to try to get this working, I figured I ought to learn how you create a plugin.

So I did what anyone should do when trying to figure out how something works: I consulted the documentation.

jQuery’s documentation explains things pretty clearly. To begin making a plugin, all you have to do is write $.fn.someFunction = function() {}. In jQuery, the $ sign actually represents jQuery itself, and .fn literally refers to prototype, which, if you’ve studied some JavaScript, allows you to add properties to an instance of an object. They are similar to classes in languages like Ruby, but a lot more flexible. I’m not an expert on the subject, but I’ve found some pretty good blog posts on the topic here and here.

The important thing to understand here is that in $.fn.someFunction, someFunction will define the additional properties that can be called on jQuery. This is why the code calling markdownPreview(); on $('.markdown_preview') works.

To ensure that this plugin will work and coexist with other JavaScript code, we have to wrap the whole thing inside of an immediately-invoked function expression, written as (function ( $ ) { } (jQuery));. To understand this more, I’d recommend this section in the jQuery documentation and this post on Stackoverflow.

Next, we’ve got return this.each(function(){}). In this context, this refers to the jQuery object, and each() will iterate over all the elements the jQuery object is being called on.

Inside the function being defined inside of each(function(){}), the DOM element becomes the context, which in this case is .markdown_preview. Therefore we refer to it as $(this). Writing $(this) a lot can get annoying, so to make it a bit quicker, McFadden has created a variable var $this and assigned it the value of $(this).

For more explanation on the differences between this and $(this) in jQuery, I recommend checking out Remy Sharp’s blog on the subject, “jQuery’s this: demystified”.

Next, we’ve got $this.wrap( '<div class="markdown_wrap editing"></div>' );. Clearly some html is being inserted here, and as the .wrap() name suggests, it’s wrapping a <div> with the classes 'markdown_wrap editing around the element on which the function is being called. That element is the textarea which we’ve given the class '.markdown_previw', being represented by $this.

Then we’ve got this block:

1
$this.before( '<div class="markdown_wrap_menu"><div class="markdown_wrap_menu_help">Help</div><div class="markdown_wrap_menu_edit">Write</div><div class="markdown_wrap_menu_preview">Preview</div></div>' );

From the docs, the before() command: Insert content, specified by the parameter, before each element in the set of matched elements. So these divs are being inserted before the '.markdown_preview' class.

Finally, we get to the event listeners, such as this one:

1
2
3
4
5
$( '.markdown_wrap_menu_help' ).on( 'click', function(){
  $( this ).closest( '.markdown_wrap' ).toggleClass( 'helping' );

  $( this ).closest( '.markdown_wrap' ).find( '.markdown_wrap_help' ).slideToggle( 'fast' );
});

What this says is upon clicking the element with the specified class, find the closest element with the 'markdown_wrap' class and add the class 'helping'. Then, on the closest 'markdown_wrap' class, find the element with the class '.markdown_wrap_help' and slide it quickly into view.

So that all makes sense, so why doesn’t it work here? I wasn’t seeing any errors in the Chrome console. It’s syntactically correct, but seems to fail.

I came across one answer I didn’t fully understand, ultimately leading me down a path to understanding better how jQuery works.

If I wrote the event listener like this:

$( document ).on( 'click', '.markdown_wrap_menu_help' function(){...

Suddenly, the buttons started working. Great! But what’s different about this syntax?

According to some helpful posts on Stackoverflow, event handlers are bound only to the currently selected elements; they must exist on the page at the time your code makes the call to .on().

Okay, so the class 'markdown_wrap_menu_help' needs to exist before the call is made. If it doesn’t, then you can listen on the document itself and pass in the element that will eventually be generated, like this: $(document).on('click', '.dynamically-generated-class', function(){});.

However, I was still confused by a couple things. First, looking at the hierarchy of McFadden’s code, it certainly looked like '.markdown_wrap_menu_help' did exist at the time the call was being made.

Also, from the jQuery docs, listening on the document isn’t recommended because it can hinder performance:

Attaching many delegated event handlers near the top of the document tree can degrade performance. Each time the event occurs, jQuery must compare all selectors of all attached events of that type to every element in the path from the event target up to the top of the document. For best performance, attach delegated events at a document location as close as possible to the target elements. Avoid excessive use of document or document.body for delegated events on large documents.

And later:

For example, instead of $(“body”).on(“click”, “#commentForm .addNew”, addComment) use $(“#commentForm”).on(“click”, “.addNew”, addComment).

I didn’t want to get in the bad habit of excessively listening on the document, and it appeared the element did exist at the time of the call. So I continued looking at the code a bit more. I noticed that the event handlers were outside of the scope of what was being returned inside of return this.each(function(){});. Interesting. So I moved all the event handlers inside of what was being returned. It worked!

Once I changed that, bam, the buttons now seemed to be working. I could click the help button and the markdown guide appeared, and I could click back over to the write button. Nice. Then I clicked the preview button that tells me the content is loading… and… loading….. and still loading….

This can’t take that long. Chrome tells me that the Ajax request is failing, but now the problem isn’t to do with the jQuery file, it’s to do with the markdown convert action in the gem. The error read, uninitialized constant MarkdownPreviewController::RDiscount.

Hmmm, RDiscount… what the hell is that? Turns out it’s a library containing the code that makes the markdown syntax actually work. The Markdown Preview gem listed it as a dependency so… great. It seems the external library I just spent all this time fixing relies on another library that doesn’t seem to work with it anymore. Not too surprising with two-year old code…

Two-year old code… RDiscount was around two years ago and it has clearly been updated since then. Maybe if I go back in time and tell Rails to use the RDiscount gem version of 2011, that might make a difference? Note, this idea came from my pal Josh Bavari of the Raisemore team. Josh acknowledged that was really reaching, but I may as well try it.

And what do you know… after listing gem 'rdiscount', '1.6.8' in my gemfile, running bundle update, and restarting the server, everything finally seemed to be working together. Awesome.

I’d be curious to know what I could have done even better to modernize Markdown Preview. For one, it’d be a more elegant solution to either specify within the gem itself to use the older version of RDiscount, or to update the whole thing to work with the current version. I may explore that later, but for now, it works, and sometimes that’s all you can ask for.

In the end, I got a lot out of debugging this gem. I learned a lot about how jQuery and jQuery plugins work, some higher JavaScript principles, and that you really ought to specify dependencies, down to the version, in your gems.

Comments