How to make changes to a Ruby gem (as a Rails developer)

At makandra, we've built a few gems over the years. Some of these are quite popular: spreewald (> 1M downloads), active_type (> 1M downloads), and geordi (> 200k downloads)

Developing a Ruby gem is different from developing Rails applications, with the biggest difference: there is no Rails. This means:

  • no defined structure (neither for code nor directories)
  • no autoloading of classes, i.e. you need to require all files yourself
  • no active_support niceties

Also, their scope...

HTML5: disabled vs. readonly form fields

Form fields can be rendered as noneditable by setting the disabled or the readonly attribute. Be aware of the differences:

disabled fields

  • don’t post to the server
  • don’t get focus
  • are skipped while tab navigation
  • available for button, fieldset, input, select, textarea, command, keygen, optgroup, option

Browser specific behavior:

  • IE 11: text inputs that are descendants of a disabled fieldset appear disabled but the user can still interact with them
  • Firefox: selecting text in a disabled text field is no...

Things you probably didn’t know you could do with Chrome’s Developer Console

Collection of useful tools in the Chrome JavaScript console.

Make the whole page editable

This is not special to Chrome, but still a clever thing:

document.body.contentEditable=true 

Taking time

You can easily measure the time on the console with named timers:

console.time('myTime'); // Start timer
console.timeEnd('myTime'); // End timer and print the time

Reference previously inspected elements (from the Elements panel)

Variables $0, $1, ... $n reference the nth-last inspected Element. $0 ...

HTTP 302 redirects for PATCH or DELETE will not redirect with GET

A HTTP 302 Found redirect to PATCH and DELETE requests will be followed with PATCH or DELETE. Redirect responses to GET and POST will be followed with a GET. The Rails form_for helper will use a workaround to send POST requests with a _method param to avoid this issue for PATCH/DELETE.

If you make requests yourself, watch out for the following behavior.

When you make an AJAX request PATCH /foo and the /foo action redirects to /bar, browsers will request PATCH /bar. You probably expected the second requ...

sessionStorage: Per-window browser storage

All major browsers (IE8+, FF3.5+, Safari 4+, any Chrome) support sessionStorage, a JavaScript storage object that

  • survives page reloads and browser restores,
  • but is different per new tab/window (in contrast to localStorage which is shared across all tabs).

MDN says:

The sessionStorage object is most useful for hanging on to temporary data that should be saved and restored if the browser is accidentally refreshed

Demo

Example usage:

The developer console can do more than you think!

You can do so much more than console.log(...)! See the attached link for a great breakdown of what the developer console can give you.

Some of my favorites:

console.log takes many arguments

E.g. console.log("Current string:", string, "Current number:", 12)

Your output can have hyperlinks to Javascript objects

E.g. console.log("Check out the current %o, it's great", location)

[Di...

Capybara will not find links without an href attribute

Capybara will fail to find <a> tags that are missing an href attribute. This will probably happen to you every now and then on JavaScript-heavy applications.

An example would be an AngularJS application where the following HTML actually works. [1]

<a ng-click="hello()">Hello</a>

Capybara will fail to find that link, even though looking it up via the DOM shows it:

>> find_link("Hello")
Capybara::ElementNotFound: Unable to find link "Hello"

>> find("a").text
=> "Hello"

To make find_link and click_link work, ...

Google Analytics: Change the tracked URL path

By default, Google Analytics tracks the current URL for every request. Sometimes you will want to track another URL instead, for example:

  • When an application URL contains a secret (e.g. an access token)
  • When you want to track multiple URLs under the same bucket
  • When you want to track interactions that don't have a corresponding URL + request (e.g. a Javascript button or a PDF download)

Luckily the Analytics code snippet allows you to freely choose what path is being tracked. Simple change this:

ga('send', 'pageview');

......

Popular mistakes when using nested forms

Here are some popular mistakes when using nested forms:

  • You are using fields_for instead of form.fields_for.
  • You forgot to use accepts_nested_attributes in the containing model. Rails won't complain, but nothing will work. In particular, nested_form.object will be nil.
  • The :reject_if option lambda in your accepts_nested_attributes call is defined incorrectly. Raise the attributes hash given to your :reject_if lambda to see if it looks like you expect.
  • If you are nesting forms into nested forms, each model involved ne...

Efficiently add an event listener to many elements

When you need to add a event listener to hundreds of elements, this might slow down the browser.

An alternative is to register an event listener at the root of the DOM tree (document). Then wait for events to bubble up and check whether the triggering element (event.target) matches the selector before you run your callback.

This technique is called event delegation.

Performance considerations

Because you only register a single listener, registering is ...

include_tags with the asset pipeline

You can include files from app/assets or from the public folder with javascript_include_tag. The subtle difference that tells rails how to build the path correctly is a single slash at the beginning of the path:

<%= javascript_include_tag('ckeditor/config') %> # for assets/ckeditor/config.js
<%= javascript_include_tag('/ckeditor/ckeditor') %> # for public/ckeditor/ckeditor.js

This also applies to stylesheet_link_tag.

Note that when you refer to a Javascript or stylesheet in /assets you need to add it to [the list of asse...

Webpacker: Configuring browser compatibility

Webpacker uses Babel and Webpack to transpile modern JavaScript down to EcmaScript 5. Depending on what browser a project needs to support, the final Webpack output needs to be different. E.g. when we need to support IE11 we can rely on fewer JavaScript features. Hence our output will be more verbose than when we only need support modern browsers.

Rails 5.1+ projects often use Webpacker to preconfigure the Webpack pipeline for us. The default configuration works something like this:

  1. Webpack checks w...

Cucumber: Wait until CKEditor is loaded

I had to deal with JavaScript Undefined Error while accessing a specific CKEditor instance to fill in text.

Ensure everything is loaded with

patiently do
  page.execute_script("return isCkeditorLoaded('#{selector}');").should be_true
end

Example

The fill in text snippet for Cucumber:

When /^I fill in the "([^\"]+)" WYSIWYG editor with:$/ do |selector, html|
  patiently do
    page.execute_script("return isCkeditorLoaded('#{selector}');").should be_true
  end
  html.gsub!(/\n+/, "") # otherwise: unterminated string lit...

Bug in Chrome 56+ prevents filling in fields with slashes using selenium-webdriver/Capybara

There seems to be a nasty bug in Chrome 56 when testing with Selenium and Capybara: Slashes are not written to input fields with fill_in. A workaround is to use javascript / jquery to change the contents of an input field.

Use the following code or add the attached file to your features/support/-directory to overwrite fill_in.

module ChromedriverWorkarounds

  def fill_in(locator, options = {})
    text = options[:with].to_s
    if Capybara.current_driver == :selenium && text.include?('/')
      # There is a nasty Bug in Chrome ...

Jasmine: Use `throwUnless` for testing-library's `waitFor`

testing-library are widely used testing utilities libraries for javascript dependent frontend testing. The main utilities provided are query methods, user interactions, dom expectations and interacting with components of several frontend frameworks, which allows us to worry less about the details happening in the browser and focus more on user centric tests instead!


Some of the time you will find a necessity to use methods like [waitFor](https://testing-library.com/docs/dom-testing-library/api-async/...

A gotcha of Ruby variable scoping

I recently stumbled over a quirk in the way Ruby handles local variables that I find somewhat dangerous.

Consider:

def salutation(first_name, last_name = nil)
  if last_name
    full_name = "#{first_name} #{last_name}"
  end 
  "Hi #{full_name}"
end 

This is obviously wrong, full_name is unset when last_name is nil.

However, Ruby will not raise an exception. Instead, full_name will simply be nil, and salutation('Bob') returns 'Hi '.

The same would happen in an else branch:

def salutation(fi...

Bookmarklet to generate a commit message for an issue in Linear.app

Your commit messages should include the ID of the issue your code belongs to.
Our preferred syntax prefixes the issue title with its ID in brackets, e.g. [FOO-123] Avatars for users.
Here is how to generate that from an issue in Linear.

Add a new link to your browser's bookmarks bar with the following URL.

javascript:(() => {
  if (document.querySelector('[data-view-id="issue-view"]')) {
    const [id, ...words] = document.title.split(' ') ;
    prompt('Commit message:', `[${id}] ${words.join(' ')}`)
  } else {
    alert('Open issue...

Open UI: Future development in web components and controls

tl;dr When browsers start to adapt proposals from Open UI, it might not be necessary to use any 3rd party libraries to have nice components and controls in web applications e.g. selects. It would require only a minimum of CSS and Javascript to get them working and looking good.

The purpose of the Open UI, a W3C Community Group, is to allow web developers to style and extend built-in web UI components and controls, such as dropdowns, checkboxes, radio buttons, and date/color pickers.

To do that, we’ll need to fully specify...

Virtual scrolling: A solution for scrolling wide content on desktops

I recently built a screen with a very high and wide table in the center. This posed some challenges:

  • Giving the table a horizontal scroll bar is very unergonomic, since the scrollbar might be far off screen.
  • Making the whole page scrollable looks bad, since I don't want the rest of the UI to scroll.
  • Giving the table its own vertical scrollbar and a limited height would have solved it, but felt weird, since the table was 90% of the page.

What I ended up doing is reusing the horizontal page scrollbar (which is naturally fixed at t...

How to display an unsaved changes alert

All browsers implement an event named beforeunload. It is fired when the active window is closed and can be used to display an alert to warn the user about unsaved changes.

To trigger the alert, you have to call preventDefault() on the event.

Note

The beforeunload event is only dispatched when the user navigation makes a full page load, or if it closes the tab entirely. It will not be dispatched when navigating via JavaScript. In this case you need to ...

In Chrome 121+ the now supported spec-compliant scrollbar properties override the non-standard `-webkit-scrollbar-*` styles

Up until Chrome 120, scrollbars could only be styled using the various -webkit-scrollbar-* pseudo elements, e.g. to make the scrollbars have no arrows, be rounded, or with additional margin towards their container.

Starting with version 121, Chrome now also supports the spec-compliant properties scrollbar-width and scrollbar-color.
These allow less styling. You may only specify the track and thumb colors, and a non-specific width like auto, thin, or none.

How to work around selenium chrome missing clicks to elements which are just barely visible

Chromedriver (or selenium-webdriver?) will not reliably scroll elements into view before clicking them, and actually not click the element because of that.

We've seen this happen for elements which are just barely in the viewport (e.g. the upper 2px of a 40px button). Our assumption is that the element is considered visible (i.e. Capybara::Selenium::ChromeNode#visible? returns true for such elements) but the Selenium driver wants to actually click the center of the element which is outside of the viewport.

We don't know who exactly i...

How to transition the height of elements with unknown/auto height

If you want to collapse/expand elements with dynamic content (and thus unknown height), you can not transition between height: 0 and height: auto.

Doing it properly, with modern CSS features

In the past, you might have resorted to bulky JavaScript solutions or CSS hacks like transitioning between max-height: 0 and max-height: 9999px. All of them were awkward and/or have several edge cases.

With modern CSS, there is actually a way to do it properly:
Just use a display: grid container which transitions its grid row height betwe...

Bookmarklet: cards Markup Link Bookmarklet

The cards editor has a feature "Cite other card" to create links to other cards in the same deck as mardown links.
If you want to reference a card from a different deck, this bookmarklet might be useful:

javascript:(function () {
  const doAlert = () => { alert("Maybe not a makandra card?") };
  let cardsPathPattern = /(\/[\w-]+\/\d+)-.+/;
  if (window.location.pathname.match(cardsPathPattern)) {
    let currentPath = window.location.pathname.match(cardsPathPattern)[1];
    let title = document.querySelector('h1.note--title')?.textCon...