open-source

Lessons learnt from working with open-source libraries

During my internship at Teamie, I had the chance of working with multiple popular JavaScript open-source libraries for certain features. There were some lessons I learnt during the process.

Testing is important, but might not be enough

One of the first things I do with the libraries is to test the behavior to see if the outcome is what I expected. Sometimes, the library seems to do exactly what I needed, but does not handle the edge cases as well as I wanted it to.

It is usually a good practice to test the library thoroughly as many scenarios as possible and understand its behavior. However, sometimes it is just not practical to do so. For example, we are using a WYSIWYG editor called medium-editor and it has some built-in cleaning features for pasted contents. It is not possible to try all possible types of content that a user might copy on various platforms.

Understanding the behavior through code inspection

Thanks for the open-source nature of the library, we can examine the source code and see the internal working of the feature to see if that is what we needed. The obstacle lies in the fact that different libraries have different coding styles and adopt different structures to organize the code. You might not fully understand the code if you just look at one function in isolation.

For example, in medium-editor, the pasteHTML function looks like this:

There is a   MediumEditor.util.defaults function in the first line, taking in the options argument as well as the default options inherited from the this keyword. The confusion arises here as we do not know which one would take precedence and override the other. Also, it is not clear what this.document refers to, why was it not simply document? Without looking at other parts of the code, we cannot clear these doubts and be sure of the exact behavior of the this function. Hence, it is necessary to look at the code at a higher level of abstraction.

We can either get some idea of the code organization by looking at the folder structure or the final compiled file in dist folder. The compile file might be very huge (7k LOC in this case). We can either use collapse/expand feature of the IDE, or scroll up and down repeatedly until we are able to construct a mental image of how the entire library is structured. In the case of medium-editor, the code is organized into the main MediumEditor object and various extensions loaded as MediumEditor.extensions. The MediumEditor object also have util functions exposed as public APIs.

Tweaking the behavior without breaking something else

Sometimes, a little tweaking is needed to suit the requirements of our product. We can only do this after inspecting through the code and understanding the dependencies between the functions. Otherwise, we risk affecting other components of the library and the features of our product that depends on them.

In the case of medium-editor, we want to customize how the text are cleaned and formatted when the user tries to paste something from outside the app. Hence, we modified the pasteHTML function to suit our need. One of the changes that we made was to disallow pasting of images into the textbox grid.

It seemed reasonable enough at first, as the textbox should only contain text. However, we failed to consider the impact of this change on other components that depend on this function. It turns out that another feature, equator editor, is also using this function to insert equations as images into the textbox, and it stopped working due to the change. Luckily, there is an options overriding logic in the pasteHTML function, and we were able to provide special options for equation editor feature such that the images would not be removed upon insertion.

Resolving compatibility between libraries

As every web developer has experienced, the DOM is a mess. – Mozilla

DOM is the worst nightmare for front-end developers. What is worse than DOM, is different libraries trying to maximize the compatibility with various browsers, but losing compatibility with every other library in the process.

The example that we have is the compatibility between ng-file-uploadangular-gridster and medium-editor. All three libraries add event listeners related to drag and drop:

  • ng-file-upload
    • feature: allow drag and drop of files onto the page
    • event listeners: dragenter, dragleave, drop, etc…
  • angular-gridster
    • feature: allow dragging and repositioning of grids within the layout
    • event listeners: mousedown, mousemove, mouseup, etc…
  • medium-editor
    • feature: allow drag and drop of items into the editor area
    • event listeners: dragover, dragleave, drop, etc…

And the result when you try to drag something on the page:

 

 

 

explosion

Solution to compatibility – Normal Case

There are no easy to solve this. The most common solution is to identify and isolate the scope of each library, and prevent the event listener from firing if it is not the responsibility of that library. In practice, we can do this with two extremely useful JavaScript DOM API functions:  event.stopPropagation(); and  event.preventDefault();

event.stopPropagation(); will prevent the event from propagating down the bubbling/capturing chain, making sure that no other event listeners will be fired once the event reaches the current event listener. If we are not using the capturing phase, then adding this call to the listener at most inner element would make sure no other event listeners would be fired. However, events fired in capture phase would continue to fire.

event.preventDefault(); will prevent the default behavior of the event from triggering. For example, using this alone would prevent the default paste behavior from the browser when user pastes something, as if the paste never happened. Of course the normal use case would be to code your own logic to handle the paste and mimic the browser’s default behavior afterwards.

Solution to compatibility – Trickier Case

Things get more tricky when two libraries use different event listeners but respond to the same user action. How is this possible? It happens when user starts dragging. As expected (or rather unexpected), both mousedown and dragstart events will trigger. However, I only want one of them to trigger, depending on the element being the child or the parent. In this case, the two magical functions do not help as they are two separate events, with their own bubbling/capturing chain.

My first idea to overcome this problem was to introduce state variables, triggering one event would change the state and prevent the other. However, I noticed upon further testing that the triggering order is not guaranteed. Sometimes mousedown happens before dragstart, sometimes the other way round. Hence we cannot ensure one is fired and the other is not. After a lot of trial and error, the final solution (hack) that I came up with, is to artificially delay one of the events, mousedown, to just before the subsequent mousemove event (which would trigger upon mousedown and moving the mouse). This overcomes the problem of firing order not guaranteed, while keeping the rest of event pipeline exactly the same.

Resolving compatibility between libraries and frameworks

Adding on top of the problem, is various front-end frameworks that dictates how you should interact with the DOM. For libraries, they can use any style or utility methods. However, when using the libraries, we cannot just copy paste the example usage into our own project without considering our framework.

For example, using jQuery alone is fine, but trying to mix jQuery and Angular code makes everything exponentially harder. We cannot “anyhow” use jQuery inside Angular controller or link function due to the digest cycle. Instead, we have to use the Angular way of modifying the Angular models (scope variables) and let Angular takes care of the updating the DOM at its own pace.

Contributing back to open-source

Sometimes when using open-source library, you would need to augment the library with new features. Some of the features can be quite useful for others as well. In this case, it is best if we can contribute these features back to the original project to benefit others who are looking for such features. The other way to contribute (or rather cause trouble) is to file bugs that you encountered.

For example, we reported a few bugs for ng-file-upload and angular-gridster and plan to PR a new feature to medium-editor.

Sometimes, the features we added are not generic enough to be in the original repo. In this case, we can maintain our own forked repo, like what we did for calendar-heatmap.

One thing to note is that contributing to the original repository is not the same as modifying the library to suit our own need. We have to follow the standard procedure outlined in the repository. This might including building from source, adhering to coding style, and adding tests as well as documentations.


Cover image from https://blog.fliptrazon.com/my-first-real-open-source-contribution-a21c9c01b652

Car explosion gif from http://bestanimations.com/Military/Explosions/Explosions.html

Leave a Reply