Skip to content

Instantly share code, notes, and snippets.

@andyyou
Last active August 9, 2022 07:38
Show Gist options
  • Save andyyou/834e82f5723fec9d2dc021fb7b819517 to your computer and use it in GitHub Desktop.
Save andyyou/834e82f5723fec9d2dc021fb7b819517 to your computer and use it in GitHub Desktop.
Rails 5.2 with webpacker, bootstrap, stimulus starter

Rails 5.2 with webpacker, bootstrap, stimulus starter

This gist will collects all issues we solved with Rails 5.2 and Webpacker

Create Project

# Last few parameters(--skip-* part) is only my habbit not actully required
$ rails new <project_name> --webpack=stimulus --database=postgresql --skip-coffee --skip-test
$ cd <project_name>
$ rails db:create
Support jsonb
$ rails g migration enable_hstore_extension
# db/migrate/<TIMESTAMP>_enable_hstore_extension.rb
class EnableHstoreExtension < ActiveRecord::Migration[5.2]
  def change
    enable_extension 'hstore'
  end
end
Support uuid
$ rails g migration enable_uuid_extension
# db/migrate/<TIMESTAMP>_enable_uuid_extension.rb
class EnableUuidExtension < ActiveRecord::Migration[5.2]
  def change
    enable_extension 'uuid-ossp'
    enable_extension 'pgcrypto'
  end
end
Add model templates
# lib/templates/active_record/model/model.rb
<% module_namespacing do -%>
class <%= class_name %> < <%= parent_class_name.classify %>
  # scope macros

  # Concerns macros

  # Constants

  # Attributes related macros
<% if attributes.any?(&:password_digest?) -%>
  has_secure_password
<% end -%>

  # association macros
<% attributes.select(&:reference?).each do |attribute| -%>
  belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
<% end -%>

  # validation macros

  # callbacks

  # other

  private
    # callback methods
end
<% end -%>

Configure scss architecture

If you are using some front end framework you may like to integrate stylesheet into components with webpack or you just like to integrate stylesheets with webapck like me. This is a way that we integrate that into webpacker.

NOTE: This is only the convention of our team you can avoid this step and keep stylesheet in assets/.

$ mkdir app/javascript/stylesheets
$ touch app/javascript/stylesheets/application.scss
$ touch app/javascript/stylesheets/_variables.scss
$ touch app/javascript/stylesheets/_base.scss

After create files please write down styles as follow:

app/javascript/stylesheets/application.scss

@import 'variables';
@import 'base';

app/javascript/stylesheets/_variables.scss

$colors: (
  major: #00D252,
  minor: #2F3B59
);

app/javascript/stylesheets/_base.scss

h1 {
  color: map-get($colors, major);
}

On the top of app/javascript/packs/application.js

import 'stylesheets/application'

(Optional)Integrate stimulus manually

If you are not use --webpack=stimulus for create project or install stimulus in existed project.

$ yarn add stimulus
$ mkdir app/javascript/controllers
# To provide a example for testing stimulus
$ touch app/javascript/controllers/clipboard_controller.js

(Optional)Configure stimulus

app/javascript/s/packs/application.js

/* eslint no-console:0 */
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.
//
// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
// layout file, like app/views/layouts/application.html.erb
import 'stylesheets/application'

import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"

const application = Application.start()
// The path you may like to change to under `pack` that path will be `./controllers`
// but convention will be in `/app/javascript/controllers`
const context = require.context("controllers", true, /\.js$/)
application.load(definitionsFromContext(context))

Example of testing stimulus:

app/javascript/controllers/clipboard_controller.js

import { Controller } from 'stimulus'

export default class extends Controller {
  static targets = ['source']
  initialize() {
    console.log('clipboard initialize')
  }
  connect() {
    console.log('clipboard connect')
    if (document.queryCommandSupported('copy')) {
      this.element.classList.add('clipboard--supported')
    }
  }
  copy(e) {
    e.preventDefault()
    this.sourceTarget.select()
    document.execCommand('copy')
  }
}

Create a example controller and view

$ rails g controller pages example

Add app/views/pages/example.html.erb

<h1>Hello, World</h1>
<hr>
<div data-controller="clipboard members dashboard">
  PIN
  <input type="text" data-target="clipboard.source" value="1234" readonly>
  <button data-action="clipboard#copy" class="clipboard-button">
    Copy to Clipboard
  </button>
</div>

Add pack to layout

Open app/views/layout/application.html.erb then add pack tags to <head>

<%= stylesheet_pack_tag 'application' %>
<%= javascript_pack_tag 'application' %>

Add route

config/routes.rb

Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  root 'pages#example'
end

Then you can test

$ rails s

Navigate to localhost:3000 should see as follow

Until here you should complete Rails 5.2 using webpacker with stimulus and stylesheets.

For common practical stiuation you may want to use bootstrap v4.x.

Install bootstrap

# https://getbootstrap.com/docs/4.1/getting-started/webpack/
$ yarn add jquery popper.js bootstrap

Import boostrap stylesheets

In app/javascript/stylesheets/application.scss add bootstrap

@import '~bootstrap/scss/bootstrap';
@import 'variables';
@import 'base';

Import bootstrap JavaScript

app/javascript/packs/application.js

import 'bootstrap'

Configure webpacker

Add configuration to config/webpack/environment.js. If you do not setup this step, the abilities related to Popper.js such as tooltip will not working.

const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
/**
 * Automatically load modules instead of having to import or require them everywhere.
 * Support by webpack. To get more information:
 *
 * https://webpack.js.org/plugins/provide-plugin/
 * http://j.mp/2JzG1Dm
 */
environment.plugins.prepend(
  'Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    jquery: 'jquery',
    'window.jQuery': 'jquery',
    Popper: ['popper.js', 'default']
  })
)
module.exports = environment

Sometimes you may like to use jQuery in views you should expose jQuery to global

expose jQuery to global for views

# https://webpack.js.org/loaders/expose-loader/
$ yarn add expose-loader -D
# NOTICE: For some production environment you may ignore `-D`

Add configuration to config/webpack/environment.js

/**
 * To use jQuery in views
 */
environment.loaders.append('expose', {
  test: require.resolve('jquery'),
  use: [{
    loader: 'expose-loader',
    options: '$'
  }]
})

Other convention of our team

$ mkdir -p lib/templates/active_record/model
$ touch lib/templates/active_record/model/model.rb

lib/templates/active_record/model/model.rb

<% module_namespacing do -%>
class <%= class_name %> < <%= parent_class_name.classify %>
  # scope macros

  # Concerns macros

  # Constants

  # Attributes related macros
<% if attributes.any?(&:password_digest?) -%>
  has_secure_password
<% end -%>

  # association macros
<% attributes.select(&:reference?).each do |attribute| -%>
  belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
<% end -%>

  # validation macros

  # callbacks

  # other

  private
    # callback methods
end
<% end -%>
@andyyou
Copy link
Author

andyyou commented May 7, 2018

To import images from assets/* since webpacker 3.5+

Warning: Read next comment first, it's not recommand way and if you want to reduce complex configuration you can just use relative path such as url('./../../assets/images/logo.png').

Install resolve-url-loader

$ yarn add resolve-url-loader
# Note: If you want to deploy to Heroku you should not add `-D`

Then modify config/webpack/environment.js

Resolve URL Loader 3+

1. Add config/webpack/loaders/resolve.js
const fs = require('fs');
const path = require('path');

module.exports = {
  loader: 'resolve-url-loader',
  options: {
    join: (uri, options) => {
      return (filename, base) => {
        // Add lookup folder path you want
        const sourceFolders = [
          'app/assets/images',
          'app/assets/fonts'
        ];
        const paths = sourceFolders.map((folder) => {
          if (fs.existsSync(`${folder}/${filename}`)) {
            return path.normalize(`${folder}/${filename}`);
          } else {
            return;
          }
        });
        return paths.filter(s => s)[0] || uri;
      }
    }
  }
}
2. Modify config/webpack/environment.js
const resolve = require('./loaders/resolve');

//...
environment.loaders.get('sass').use.splice(-1, 0, resolve);

Resolve URL Loader 2.3.0

// config/webpack/environment.js
// NOTE: Unsafe way
environment.loaders.get('sass').use.splice(-1, 0, {
  loader: 'resolve-url-loader',
  options: {
     attempts: 0
  }
})

@pjchender
Copy link

pjchender commented May 7, 2018

Some Reference for resolve-url-loader

In rails, our convention is add a helper in javascript/stylesheets/ for generate relative path quickly

@function asset-image($path) {
  @return url('./../../assets/images/' + $path)
};

@andyyou
Copy link
Author

andyyou commented May 17, 2018

Deploy to Heroku errors

Uglifier::Error: Unexpected token: punc ((). To use ES6 syntax, harmony mode must be enabled with Uglifier.new(:harmony => true) 
# confog/environments/production.rb
# Change setup to Uglifier.new(harmony: true)
config.assets.js_compressor = Uglifier.new(harmony: true)

Another problems if devDependencies needs install something

# Add settings
$ heroku config:set NPM_CONFIG_PRODUCTION=false YARN_PRODUCTION=false

@andyyou
Copy link
Author

andyyou commented Jun 6, 2018

Rails ajax with token

var token = $( 'meta[name="csrf-token"]' ).attr( 'content' );

$.ajaxSetup( {
  beforeSend: function ( xhr ) {
    xhr.setRequestHeader( 'X-CSRF-Token', token );
  }
});
let token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch('<url.json>', {
  headers: {
    'User-Agent': navigator.userAgent,
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest',
    'X-CSRF-Token': token
  },
  body: JSON.stringify(data),
  method: 'POST',
  credentials: 'same-origin'
})
.then(res => res.json())
.thrn(data => {});

@andyyou
Copy link
Author

andyyou commented Jun 22, 2018

$ bundle exec rails webpacker:install:erb

Then change filename.js to filename.js.erb

(Manually explain NOTE: It's NOTE best way)

$ yarn add rails-erb-loader

config/webpack/environment.js

environment.loaders.append('js.erb', {
  test: /\.js.erb$/,
  enforce: 'pre',
  use: ['babel-loader', 'rails-erb-loader']
})

config/webpacker.yml

extensions:
    - .js.erb

stimulus load

import { Application } from 'stimulus';
import { definitionsFromContext } from 'stimulus/webpack-helpers';

const application = Application.start();
const context = require.context('controllers', true, /\.js(\.erb)?$/);
application.load(definitionsFromContext(context));

@andyyou
Copy link
Author

andyyou commented Jun 22, 2018

Deploy heroku

$ heroku apps:create <project_name>

# 1. Add `RAILS_MASTER_KEY` to Heroku > Settings > Config Vars
# 2. Add `SECRET_KEY_BASE` to Heroku > Settings > Config Vars (`rails secret` to generate secret base)
# NOTE: In Rails 5.2 if you use ES6 please check `config/environments/production.rb`
# config.assets.js_compressor = Uglifier.new(harmony: true)

$ git push heroku master
$ heroku run rails db:migrate

# Compile assets
$ heroku run rails assets:precompile

# Reset Database
$ heroku pg:reset DATABASE

# Check logs
$ heroku logs --tail

@ahbou
Copy link

ahbou commented Oct 17, 2018

FYI font-awesome 5 has been renamed on 5.10

yarn add @fortawesome/fontawesome-free    
yarn add @fortawesome/free-solid-svg-icons

Add on application.scss

$fa-font-path: '~@fortawesome/fontawesome-free/webfonts'; 
@import '~@fortawesome/fontawesome-free/scss/fontawesome';
@import '~@fortawesome/fontawesome-free/scss/solid';

@pjchender
Copy link

When using react-rails with SSR, remember to add this snippet in app/javascript/packs/application.js, which can solve component reload issue:

// Order is particular. First start Turbolinks:
Turbolinks.start();
window.Turbolinks = Turbolinks;    // Add Turbolinks to the global namespace:
ReactRailsUJS.detectEvents();      // Remove previous event handlers and add new ones:
delete window.Turbolinks;              // (Optional) Clean up global namespace:

React-Rails Event Handling

@ameft
Copy link

ameft commented Nov 30, 2018

Hi @andyyou, to deploy the app in production I had to yarn add expose-loader whithout -D otherwise Webpacker compiling complains about the module not being available in production. (Actually it usually says nothing and just hangs indefinitely, I had to bang my head against the wall for hours to undestand the problem)

@andyyou
Copy link
Author

andyyou commented Dec 19, 2018

@ameft Sorry for that I didn't mention, you are right some environment such as Heroku has the problem you mention so I put NOTE. Thanks

@andyyou
Copy link
Author

andyyou commented Dec 19, 2018

@leastbad
Copy link

leastbad commented Dec 19, 2018

@andyyou, this is seriously one of the most incredible things I've found in my journey to wrap my head around Webpack.

Thank you so much. Please keep up the amazing work.

FWIW, you might consider adding a note to the Font Awesome section that paid users need a slightly different path structure:

$fa-font-path: '~@fortawesome/fontawesome-pro/webfonts'; 
@import '~@fortawesome/fontawesome-pro/scss/fontawesome';
@import '~@fortawesome/fontawesome-pro/scss/solid';

Two minor differences in my application.js which were ultimately necessary to get everything working:

import 'bootstrap/dist/js/bootstrap.bundle.js'
import {} from 'jquery-ujs'

The bootstrap.bundle.js contains Popper.js, which otherwise was not getting included despite my labours to get it working. It didn't seem to matter how or where I included it, it would report as undefined on the console. Also, I switched back to jquery-ujs because I prefer working with my trusted tool of choice.

I haven't yet attempted to get ActionCable working, but this seems to have the high-level details summarized in one place.

The only other thing I wanted to add is that I renamed my app/javascript to app/webpack because it seems silly to call it that. So far as I can tell, all I needed to change was the default->source_path in webpacker.yml and it appears to work without issue. Can you foresee any potential concerns?

@andyyou
Copy link
Author

andyyou commented Dec 20, 2018

@pjforde1978 Thanks for sharing. About .js part I think your way is a good idea. We usually only need to override .scss varables of bootstrap.

@andyyou
Copy link
Author

andyyou commented Dec 25, 2018

Outlook 365 Mailer settings:

config.action_mailer.smtp_settings = {
    address: Rails.application.credentials.dig(:smtp, :host),
    port: '587',
    user_name: Rails.application.credentials.dig(:smtp, :username),
    password: Rails.application.credentials.dig(:smtp, :password),
    from: Rails.application.credentials.dig(:smtp, :username),
    authentication: :login
}

@kirantpatil
Copy link

Greetings @andyyou!

Please add new gist for "Rails 6 with webpacker, bootstrap, stimulus starter"

I installed Rails 6 as below.

$ git clone --depth 1 https://github.com/rails/rails
$ cd rails
$ bundle
$ cd ..
$ rails/railties/exe/rails new fooapp --dev

Thanks.

@leastbad
Copy link

@kirantpatil do you have anything to contribute to such a guide?

The reason that Andy's guide is so useful is that it is the product of real-world development experience. Not only is he under no obligation to keep this document updated, but it would also be difficult to know what merits inclusion in such a guide for a framework release that hasn't come out yet. The framework itself will hopefully make much of this guide somewhat obsolete by establishing sane conventional defaults, and in a best-case scenario Rails' core maintainers will be able to address many of these concerns out of the box.

Andy's missing manual is a godsend and only comes to exist through persistence and suffering. I do everything I can to give back, and so should you.

@andyyou
Copy link
Author

andyyou commented Mar 16, 2019

First of all, thanks and @pjforde1978 is right. I haven't use Rails 6 in my products yet so I don't exactly know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment