• Stars
    star
    181
  • Rank 204,329 (Top 5 %)
  • Language
  • Created almost 9 years ago
  • Updated over 6 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Reasonable System for JavaScript Structure

rsjs

Reasonable System for JavaScript Structure

This document is a collection of guidelines on how to structure your JavaScript in a standard non-SPA web application. Also see RSCSS, a document on CSS conventions that follows a similar line of thinking.

Problem

For a typical non-SPA website, it will eventually be apparent that there needs to be conventions enforced for consistency. While styleguides cover coding styles, conventions on namespacing and file structures are usually absent.

The jQuery soup anti-pattern

You will typically see Rails projects with behaviors randomly attached to classes, such as the problematic example below.

<script src='blogpost.js'></script>
<div class='author footnote'>
  This article was written by
  <a class='profile-link' href="/user/rstacruz">Rico Sta. Cruz</a>.
</div>
$(function () {
  $('.author a').on('hover', function () {
    var username = $(this).attr('href')

    showTooltipProfile(username, { below: $(this) })
  })
})

What's wrong?

This anti-pattern leads to many issues, which rsjs attempts to address.

  • Ambiguious sources: It's not obvious where to look for the JS behavior. (Is the handler attached to .author, .footnote, or .profile-link?)

  • Non-reusable: It's not obvious how to reuse the behavior in another page. (Should blogpost.js be included in the other pages that need it? What if that file contains other behaviors you don't need for that page?)

  • Lack of organization: Making new behaviors gets confusing. (Do you make a new .js file for each page? Do you add them to the global application.js? How do you load them?)

In a nutshell

RSJS makes JavaScript easy to maintain in a typical web application. All recommendations here have these goals in mind:

  • Keep your HTML declarative (get rid of inline scripts).
  • Put all your imperative code in JS files.
  • Reduce ambiguity by having straight-forward conventions.
  • HTML elements can be components that have behaviors.
  • Behaviors apply JavaScript to a [data-js-behavior-name] selector.

Structure

Think in component behaviors

Think that a piece of JavaScript code to will only affect 1 "component", that is, a section in the DOM.

There files are "behaviors": code to describe dynamic JS behavior to affect a block of static HTML. In this example, the JS behavior collapsible-nav only affects a certain DOM subtree, and is placed on its own file.

<!-- Component -->
<div class='main-navbar' data-js-collapsible-nav>
  <button class='expand' data-js-expand>Expand</button>

  <a href='/'>Home</a>
  <ul>...</ul>
</div>
/* Behavior - behaviors/collapsible-nav.js */

$(function () {
  var $nav = $('[data-js-collapsible-nav]')
  if (!$nav.length) return

  $nav
    .on('click', '[data-js-expand]', function () {
      $nav.addClass('-expanded')
    })
    .on('mouseout', function () {
      $nav.removeClass('-expanded')
    })
})

One component per file

Each file should a self-contained piece of code that only affects a single element type.

Keep them in your project's behaviors/ path. Name these files according to the data-js-___ names (see below) or class names they affect.

└── javascripts/
    └── behaviors/
        β”œβ”€β”€ collapsible-nav.js
        β”œβ”€β”€ avatar-hover.js
        β”œβ”€β”€ popup-dialog.js
        └── notification.js

Load components in all pages

Your main .js file should be a concatenation of all your behaviors.

It should be safe to load all behaviors for all pages. Since your behaviors are localized to their respective components, they will not have any effect unless the element it applies to is on the page.

See loading component files for guides on how to do this for your project.

Use a data attribute

It's preferred to mark your component with a data-js-___ attribute.

You can use ID's and classes, but this can be confusing since it isn't obvious which class names are for styles and which have JS behaviors bound to them. This applies to elements inside the component too, such as buttons (see collapsible-nav example).

<!-- βœ— Avoid using class names -->
<div class='user-info'>...</div>
$('.user-info').on('hover', function () { ... })
<!-- βœ“ Better to use data-js attributes -->
<div class='user-info' data-js-avatar-popup>...</div>
$('[data-js-avatar-popup]').on('hover', function () { ... })

You can also provide values for these attributes.

<!-- βœ“ Attributes with values -->
<button data-js-tooltip='Close'>...</button>
$('[data-js-tooltip]').on('hover', function () { ... })

Don't overload class names

If you don't like data-js-* attributes and prefer classes, don't add styles to the classes that your JS uses. For instance, if you're styling a .user-info class, don't attach an event to it; instead, add another class name (eg, .js-user-info) to use in your JS.

This will also make it easier to restyle components as needed.

<!--
βœ— Bad: is the JavaScript behavior attached to .user-info? or to
.centered? This can be confusing developers unfamiliar with your code.
-->
  <div class='user-info centered'>...</div>
  $('.user-info').on('hover', function () { ... })

<!--
βœ“ Better: this makes it more obvious to find the source of
the behavior.
-->
  <div class='user-info js-avatar-popup'>...</div>
  $('.js-avatar-popup').on('hover', function () { ... })

No inline scripts

Avoid adding JavaScript thats inlined inside your HTML markup. This includes <script>...</script> blocks and onclick='...' event handlers.

By putting imperative logic outside your .js files (eg, JavaScript in your .html), it makes your application harder to test and they pose a significant maintenance burden. Keep all your imperative code in your JS files, and your declarative code in your HTML.

<!-- βœ— Avoid -->
<script>
  $('button').on('click', function () { ... })
</script>
<!-- βœ— Avoid -->
<body onload="loadup()">

Prefer to use JS behaviors instead of inline scripts.

Bootstrap data with meta tags

A common pattern is to use inline <script> tags to leave data that scripts will pick up later on. In the spirit of avoiding inline scripts, these patterns should also be avoided.

<!-- βœ— Avoid -->
<script>
window.UserData = { email: '[email protected]', id: 9283 }
</script>

If they're going to be used in a component, put that data inside the HTML element and let the behavior pick it up.

<!-- βœ“ Used by the user-info behavior -->
<div class='user-info' data-js-user-info='{"email":"[email protected]","id":9283}'>

If multiple components are going to use this data, put it in a meta tag in the <head> section.

<head>
  ...
  <!-- option 1 -->
  <meta property="app:user_data" content='{"email":"[email protected]","id":9283}'>

  <!-- option 2 -->
  <meta property="app:user_data:email" content="[email protected]">
  <meta property="app:user_data:id" content="9283">

This keeps your HTML files declarative, and your JS files imperative. (Also see: Imperative vs. Declarative from latentflip.com)

function getMeta (name) {
  return $('meta[property=' + JSON.stringify(name) + ']').attr('content')
}

getMeta('app:user_data:email')  // => '[email protected]'

Writing code

These are conventions that can be handled by other libraries. For straight jQuery however, here are some guidelines on how to write behaviors.

Consider using onmount

onmount is a library that allows you to write safe, idempotent, reusable and testable behaviors. It makes your JavaScript code compatible with Turbolinks, and it'd allow you to write better unit tests.

$.onmount('[data-js-push-button]', function () {
  $(this).on('click', function () {
    alert('working...')
  })
})

Writing code without onmount

The onmount library solves many pitfalls when writing component behaviors. If you choose not to use it, however, be prepared to deal with those caveats on your own. This next section describes these cases in detail.

Use document.ready

Add your events and initialization code under the document.ready handler. This assures you that the element you're binding behaviors to already exists. (There is an exception to this--see the next section).

$(function() {
  $('[data-js-expanding-menu]').on('hover', function () {
    // ...
  })
})

If you're using onmount, just run onmount() on document.ready as its documentation describes it.

Use event delegation

The only time document.ready is not necessary is when attaching event delegations to document. These are typically best done before DOM ready, which would allow them to work even when the entire document hasn't loaded completely.

This allows you to listen for those events whether the element is on the page or not, unlike doing $('[data-js-menu]').on(...) which needs to be done when the element is on the page.

$(document).on('click', '[data-js-menu]', function () {
  // ...
})

However, this technique comes at a runtime performance cost. If you bind events for click for 50 components, every click on the page will check for 50 possible event handlers. Instead, consider using onmount and bind your events directly to elements as needed.

Also see extras for more info on event delegation.

Use each() when needed

When your behavior needs to either initialize first and/or keep a state, consider using jQuery.each. See extras for a more detailed example.

$(function() {
  $('[data-js-expanding-menu]').each(function() {
    var $menu = $(this)
    var state = {}

    // - do some initialization code on $menu
    // - bind events to $menu
    // - use `state` to manage state
  })
})

If you are using onmount, this is not necessary.

Avoid side effects

Make sure that each of your JavaScript files will not throw errors or have side effects when the element is not present on the page.

$(function () {
  var $nav = $("[data-js-hiding-nav]")

  // Don't bind the `scroll` event handler if the element isn't present.
  // This will avoid making the page sluggish unnecessarily. This also
  // avoids the error that $nav[0] will be undefined.
  if (!$nav.length) return

  $('html, body').on('scroll', function () {
    if ($nav[0].disabled) return
    var isScrolled = $(window).scrollTop() > $nav.height()
    $nav.toggleClass('-hidden', !isScrolled)
  })
})

If you're using onmount, things like the $nav.length check is not necessary.

Dynamic content

There are times when you may wish to apply behaviors to content that may appear on the page later on. This is the case for AJAX-loaded content such as modal dialogs.

You can do three approaches for this:

  • Use onmount (recommended). This is the easiest and most scalable solution.
  • Use event delegation on document. This only works if you're only binding events.
  • Wrap your initialization code in a function and run in when the modal appears (described below).

If you're not using onmount or event delegation, you can wrap your initialization code in a function. Run that function on document.ready and when the DOM changes (eg, show.bs.modal in the case of Bootstrap modal dialogs).

Since your initializer may be called multiple times in a page, you will need to make them idempotent by bypassing elements that it has already been applied to. This can be done with an include guard pattern.

void (function() {
function init() {
  $('[data-js-key-value-pair]').each(function () {
    var $parent = $(this)

    // an include guard to keep it idempotent
    if ($parent.data('key-value-pair-loaded')) return
    $parent.data('key-value-pair-loaded', true)

    // init code here
  })
}

$(init)
$(document).on('show.bs.modal', init)
})()

If you're using onmount, simply bind onmount() to these events (such as show.bs.modal). Idempotency will be taken care of for you.

Namespacing

Keep the global namespace clean

Place your publically-accessible classes and functions in an object like App.

if (!window.App) window.App = {}

App.Editor = function() {
  // ...
}

Organize your helpers

If there are functions that will be reusable across multiple behaviors, put them in a namespace. Place these files in helpers/.

/* helpers/format_error.js */
if (!window.Helpers) window.Helpers = {}

Helpers.formatError = function (err) {
  return "" + err.project_id + " error: " + err.message
}

Third party libraries

If you want to integrate 3rd-party scripts into your app, consider them as component behaviors as well. For instance, you can integrate select2.js by affecting only .js-select2 classes.

...
└── javascripts/
    └── behaviors/
        β”œβ”€β”€ colorpicker.js
        β”œβ”€β”€ select2.js
        └── wow.js
// select2.js -- affects `[data-js-select2]`
$(function () {
  $("[data-js-select2]").select2()
})
// wow.js -- affects `.wow`
$(function () {
  new WOW().init()
})

Separate your vendor libs

Keep your 3rd-party libraries in something like vendor.js.

This will have browsers cache the 3rd-party libs separately so users won't need to re-fetch them on your next deployment.

It also makes it easier to create new app packages should you need more than one (eg, one for your public pages and another for private dashboards).

/* vendor.js */
//= require jquery
//= require jquery_ujs
//= require bootstrap
/* application.js */
//= require_tree ./helpers
//= require_tree ./behaviors

Load 3rd-party resources asynchronously

For third-party resources that are loaded externally, consider loading them with an asynchronous loading mechanism such as script.js.

Some 3rd party libraries are loaded via <script> tags, such as Google Maps's API. These vendors typically expect you to embed them like so:

<script src='//maps.google.com/maps/api/js?v=3.1.3&amp;libraries=geometry'></script>
$.onmount('.map-box', function () {
  Gmaps.buildMap({ ... })
})

If your website doesn't use them everywhere, it'd be wasteful to include them on every page. You can selectively include them only on pages that need them, but that would hamper reusability: if you want to reuse the .map-box component in another page, you'll need to re-include the script in that other page as well.

Instead, consider using script.js wrapped in a helper function to load them on an as-needed basis.

Helpers.useGoogleMaps = function useGoogleMaps (fn) {
  var url = 'https://maps.google.com/maps/api/js?v=3.1.3&libraries=geometry'

  $script(url, function () {
    // pass the global `Gmaps` variable to the callback function.
    fn(window.Gmaps)
  })
}

This useGoogleMaps helper can be used like so:

var useGoogleMaps = Helpers.useGoogleMaps

$.onmount('.map-box', function () {
  useGoogleMaps(function (Gmaps) {
    Gmaps.buildMap({ ... }) // use Gmaps here
  })
})

Appendix

Loading component files

Rails: this can be accomplished with the asset pipeline's built-in require_tree.

// js/application.js
/*= require_tree ./behaviors
/*= require_tree ./initializers

Browserify: you can use require-globify.

require('./behaviors/**/*.js', { mode: 'expand' })
require('./initializers/**/*.js', { mode: 'expand' })

Webpack: you can use require.context to load multiple CSS files. See this StackOverflow answer for details.

// http://stackoverflow.com/a/30652110/873870
function requireAll (r) { r.keys().forEach(r) }

requireAll(require.context('./behaviors/', true, /\.js$/))
requireAll(require.context('./initializers/', true, /\.js$/))

Brunch: you can use glob-brunch.

glob('./behaviors/**/*.js', (e, files) => files.forEach(require))
glob('./initializers/**/*.js', (e, files) => files.forEach(require))

/* brunch-config.js */
  plugins: {
    glob: {
      appDir: '...'
    }
  }

Conclusion

This document is a result of my own trial-and-error across many projects, finding out what patterns are worth adhering to on the next project.

This document prioritizes developer sanity first. The approaches here may not have the most performant, especially given its preference for event delegations and running document.ready code for pages that don't need it. Regardless, the pros and cons were weighed and ultimately favored approaches that would be maintainable in the long run.

The guidelines outlined here are not a one-size-fits-all approach. For one, it's not suited for single page applications, or any other website relying on very heavy client-side behavior. It's written for the typical conventional web app in mind: a collection of HTML pages that occasionally need a bit of JavaScript to make things work.

As with every other guideline document out there, try out and find out what works for you and what doesn't, and adapt accordingly.

Further reading

  • rscss.io - reasonable system for CSS stylesheet structure
  • onmount - for safe, idempotent JavaScript behaviors and easy Turbolinks support
  • script.js - for loading external scripts

More Repositories

1

nprogress

For slim progress bars like on YouTube, Medium, etc
JavaScript
25,552
star
2

cheatsheets

My cheatsheets
SCSS
13,391
star
3

jquery.transit

Super-smooth CSS3 transformations and transitions for jQuery
JavaScript
7,328
star
4

rscss

Reasonable System for CSS Stylesheet Structure
3,894
star
5

flatdoc

Build sites fast from Markdown
CSS
2,679
star
6

webpack-tricks

Tips and tricks in using Webpack
2,359
star
7

sparkup

A parser for a condensed HTML format
Python
1,562
star
8

backbone-patterns

Common Backbone.js usage patterns.
760
star
9

remount

Mount React components to the DOM using custom elements
JavaScript
675
star
10

sinatra-assetpack

Package your assets transparently in Sinatra.
CSS
542
star
11

jsdom-global

Enable DOM in Node.js
JavaScript
472
star
12

hicat

Command-line syntax highlighter
JavaScript
405
star
13

kingraph

Plots family trees using JavaScript and Graphviz
JavaScript
390
star
14

vim-closer

Closes brackets
Vim Script
329
star
15

scour

Traverse objects and arrays with ease
JavaScript
308
star
16

mocha-jsdom

Simple jsdom integration with mocha
JavaScript
254
star
17

css-condense

[unsupported] A CSS compressor that shows no mercy
JavaScript
206
star
18

swipeshow

The unassuming touch-enabled JavaScript slideshow
JavaScript
191
star
19

vim-coc-settings

My Vim settings for setting it up like an IDE
Vim Script
164
star
20

startup-name-generator

Let's name your silly startup
JavaScript
164
star
21

onmount

Safe, reliable, idempotent and testable behaviors for DOM nodes
JavaScript
156
star
22

psdinfo

Inspect PSD files from the command line
JavaScript
147
star
23

vim-hyperstyle

Write CSS faster
Python
144
star
24

vim-from-scratch

Rico's guide for setting up Vim
144
star
25

js2coffee.py

JS to CoffeeScript compiler. [DEPRECATED]
Python
127
star
26

firefox-stealthfox

Firefox customization for stealth toolbars
CSS
122
star
27

mocha-clean

Clean up mocha stack traces
JavaScript
107
star
28

jquery-stuff

A collection of small jQuery trinkets
JavaScript
96
star
29

npm-pipeline-rails

Use npm as part of your Rails asset pipeline
Ruby
95
star
30

navstack

Manages multiple screens with mobile-friendly transitions
JavaScript
94
star
31

bootstrap-practices

List of practices when using Bootstrap in projects
90
star
32

details-polyfill

Polyfill for the HTML5 <details> element, no dependencies
JavaScript
89
star
33

dom101

DOM manipulation utilities
JavaScript
82
star
34

unorphan

Removes text orphans
JavaScript
81
star
35

expug

Pug templates for Elixir
Elixir
80
star
36

stylelint-rscss

Validate CSS with RSCSS conventions
JavaScript
74
star
37

feh-blur-wallpaper

Blur your desktop wallpaper when windows are open
Shell
73
star
38

pomo.js

Command-line timer, great for Pomodoros
JavaScript
73
star
39

sinatra-backbone

Neat Backbone.js integration with Sinatra.
Ruby
72
star
40

flowloop

A Pomodoro-like timer for hyper-productivity
JavaScript
71
star
41

vimfiles

My VIM config files.
Lua
63
star
42

bookmarks

My bookmarks
63
star
43

my_qmk_keymaps

Keymaps for keyboards
C
59
star
44

typish

Typewriter simulator
JavaScript
58
star
45

iconfonts

Fine-tuned icon fonts integration for Sass, Less and Stylus
CSS
54
star
46

pre.js

Efficient, resilient resource preloader for JS/CSS
JavaScript
54
star
47

tape-watch

Rerun tape tests when files change
JavaScript
53
star
48

tinkerbin

Tinkerbin.com
JavaScript
52
star
49

vim-fastunite

Search for files fast
Vim Script
48
star
50

collaborative-etiquette

A manifesto for happy Open Source projects
46
star
51

decca

Render interfaces using pure functions and virtual DOM
JavaScript
45
star
52

vim-opinion

My opinionated vim defaults
Vim Script
44
star
53

modern-development

Using agile methods to build quality web applications
CSS
43
star
54

til-2013

Old version of http://ricostacruz.com/til (2015-2018)
CSS
42
star
55

ion

[deprecated] Ruby/Redis search engine.
Ruby
41
star
56

timetip

Deliciously-minimal time tracker for the command-line
JavaScript
40
star
57

vim-xtract

Extract the selection into a new file
Vim Script
39
star
58

bump-cli

Command-line version incrementer
JavaScript
39
star
59

halla

Native Slack wrapper app without the bloat
JavaScript
38
star
60

newsreader-sample-layout

CSS
36
star
61

ento

Simple, stateful, observable objects in JavaScript
JavaScript
36
star
62

cron-scheduler

Runs jobs in periodic intervals
JavaScript
35
star
63

promise-conditional

Use if-then-else in promise chains
JavaScript
34
star
64

ractive-touch

Touch events for Ractive
JavaScript
33
star
65

git-update-ghpages

Simple tool to update GitHub pages
Shell
33
star
66

jquery.unorphan

[deprecated] Obliterate text orphans.
JavaScript
33
star
67

greader

[UNSUPPORTED] Google Reader API client for Ruby
Ruby
32
star
68

penpad

Design and document web UI components
TypeScript
32
star
69

typecat

TypeScript
31
star
70

react-meta-elements

Sets document title and meta tags using React elements or hooks
TypeScript
31
star
71

vim-ultisnips-css

[deprecated] Write CSS in VIM faster.
Ruby
31
star
72

tape-plus

Nested tape tests with before/after, async, and promise support
JavaScript
30
star
73

taskpaper.js

Taskpaper parser in JavaScript
JavaScript
28
star
74

frontend-starter-kit

Rico's opinionated Metalsmith frontend kit
JavaScript
28
star
75

homebrew-backup

Back up your Homebrew profile
Shell
28
star
76

fishfiles

my fish-shell config files
Shell
28
star
77

passwordgen.js

Password generator for the command line
JavaScript
28
star
78

reacco

Generate documentation from README files.
Ruby
26
star
79

ghub

Open this project in github
Shell
26
star
80

sass_icon_fonts

Sass 3.2 integration with modern icon fonts.
CSS
26
star
81

lidoc

[Deprecated] Literate-programming style documentation tool.
CoffeeScript
24
star
82

rspec-repeat

Repeats an RSpec example until it succeeds.
Ruby
24
star
83

lofi

VHS music machine from the 80's
JavaScript
24
star
84

sinatra-template

Ruby
24
star
85

fish-asdf

Fish shell integrations for asdf version manager
Shell
24
star
86

wiki

Stuff
23
star
87

arch-installer

Install UI for Arch Linux
Shell
22
star
88

slack-emoji-magic

Magic: the Gathering emojis
Makefile
21
star
89

node-hledger

Node.js API for hledger
JavaScript
21
star
90

vimbower

Use bower, git and pathogen to manage your vim setup
21
star
91

ractive-promise-alt

Adaptor for Ractive.js to support promises
JavaScript
21
star
92

til-2020

Today I learned blog of @rstacruz
TypeScript
21
star
93

webpack-starter-kit

Baseline configuration for Webpack
JavaScript
21
star
94

phoenix_expug

Expug integration for Phoenix
JavaScript
20
star
95

cssutils

Collection of Sass utility mixins and other goodies.
CSS
20
star
96

responsive-modular-scale.css

Responsive typography using CSS variables
20
star
97

cdnjs-command

Command line helper for cdnjs.com
Ruby
20
star
98

curlformat

CLI utility to clean up your "Copy as cURL" strings
JavaScript
19
star
99

ractive-loader

ractive template loader for webpack
JavaScript
19
star
100

ajaxapi

Minimal AJAX library for APIs. Supports promises
JavaScript
18
star