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 ofform.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 benil
. - The
:reject_if
option lambda in youraccepts_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:
- 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...