Tagged: stimulus Toggle Comment Threads | Keyboard Shortcuts

  • kmitov 10:38 pm on January 13, 2022 Permalink |
    Tags: bootstrap, cssbundling-rails, jsbundling-rails, , rails 7, stimulus   

    Rails 7 with bootstrap 5 by cssbundling-rails with dart-sass and jsbundling-rails with esbuild. No webpack and webpacker and a salt of Stimulus 

    Today I put on my “new to Rails hat” and I decided to start a new Rails 7 project using the latest two new sharp knives that we have with Rails 7 to get a job done.

    The Job To Be Done is: Demonstrate Bootstrap 5 examples of a Toast in a Rails 7 project.

    This is a simple example of a popular Bootstrap 5 component that has both css and js involved. My goal is to build a website that will show a button and when the button is clicked a Toast will be visualised as it is in https://getbootstrap.com/docs/5.0/components/toasts/#live

    Provide sharp knives and the menu is omakase

    These are two points of Rails doctrine. The menu is omakase, which means that we have a good default for the tools that we are using. We are also given sharp knives to use – cssbundling-rails, jsbundling-rails, Stimulus, dart-sass, esbuild.

    The process

    I would like to bundle bootstrap 5 in the application. One of the options is to take bootstrap from npm and I would like to demonstrate how we bundle bootstrap from npm. Note that this might not be the right choice for you.

    NPM version

    Make sure you have npm 7.1+. I was trying for more than an hour to identify an error because my npm version was 6.X

    $ npm --version

    Rails 7 project

    # We pass in the option --css bootstrap that will configure most things for us
    $ rails _7.0.1_ new bProject --css bootstrap
    # enter the project folder
    $ cd bProject

    Everything is installed. You have all the dependencies to bootstrap, there is a node_modules folder containing node modules like bootstrap.

    Bundling the CSS

    cssbundling-rails gem is installed by default in the gem file.

    There is an app/assets/stylesheet/application.bootstrap.scss that imports the bootstrap css.

    It’s content is:

    @import 'bootstrap/scss/bootstrap';

    We would be using sass (again from npm) to build this .scss file. There is a script in the package.json

    {
      "name": "app",
      "private": "true",
      "dependencies": {
        "@hotwired/stimulus": "^3.0.1",
        "@hotwired/turbo-rails": "^7.1.0",
        "@popperjs/core": "^2.11.2",
        "bootstrap": "^5.1.3",
        "esbuild": "^0.14.11",
        "sass": "^1.48.0"
      },
      "scripts": {
        "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",
        "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules"
      }
    }

    The script build:css will take the app/assets/stylesheets/application.bootstrap.scss and will produce an app/assets/builds/application.css

    The file application.css is the one that our application will be using. It is referred in application.html.erb as
    <%= stylesheet_link_tag “application”, “data-turbo-track”: “reload” %>

    <!DOCTYPE html>
    <html>
      <head>
        <title>BProject</title>
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <%= csrf_meta_tags %>
        <%= csp_meta_tag %>
    
        <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
        <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
      </head>
    
      <body>
        <%= yield %>
      </body>
    </html>

    The live reload of CSS

    In order to change the css and live reload it we must start sass with –watch option as

    $ sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules --watch

    but don’t do this as there is a helper file that we should execute at the end of the article that is  – calling ./bin/dev

    Bundling the JavaScript

    jsbundling-rails is installed in the Gemfile.

    There is an app/javascript/application.js that imports bootstrap

    // Entry point for the build script in your package.json
    import "@hotwired/turbo-rails"
    import "./controllers"
    import * as bootstrap from "bootstrap"

    The application.js is bundled with esbuild as a tool  and the command is in the package json

    # part of package.json
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",

    The result is produced in app/assets/builds

    The result is referred by the Rails Assets Pipeline and included in the html by calling javascript_inlcude_tag ‘application’ as in

    <!DOCTYPE html>
    <html>
      <head>
        <title>BProject</title>
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <%= csrf_meta_tags %>
        <%= csp_meta_tag %>
    
        <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
        <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
      </head>
    
      <body>
        <%= yield %>
      </body>
    </html>

    Create a new controller

    Initial controller that will be the home screen containing the button.

    # app/controllers/home_controller.rb
    $ echo '
    class HomeController < ApplicationController
    
      def index
      end
    end
    ' > app/controllers/home_controller.rb

    Create a views folder

    # views folder
    $ mkdir app/views/home

    Create the view

    It contains a button and a toast.

    $ echo '
    <!-- app/views/home/index.html.erb -->
    <h1>Title</h1>
    <div data-controller="toast">
      <button type="button" class="btn btn-primary" data-action="click->toast#show">Show live toast</button>
    </div>
    
    <div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
      <div id="liveToast" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true">
        <div class="toast-header">
          <strong class="me-auto">Bootstrap</strong>
          <small>11 mins ago</small>
          <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
        </div>
        <div class="toast-body">
          Hello, world! This is a toast message.
        </div>
      </div>
    </div>
    ' > app/views/home/index.html.erb

    Note the code:

    <div data-controller="toast">
      <button type="button" class="btn btn-primary" data-action="click->toast#show">Show live toast</button>
    </div>

    Here we have a ‘data-controller’ attribute in the div element and a ‘data-action’ attribute in the button element.
    This is how we will connect Stimulus JS as a javascript framework to handle the logic for clicking on this button.

    Configure the routes

    When we open “/” we want Rails to call the home#index method that will return the index.html.erb page.

    $ echo '
    # config/routes.rb
    Rails.application.routes.draw do
      # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
    
      # Defines the root path route ("/")
     root "home#index"
    end' > config/routes.rb

    Create a new Stimulus controller

    We will use Stimulus as a small prefered framework in Rails for doing JavaScript. When the button in the above HTML fragment is selected the method show() in the controller will be called.

    # Use the helper method to generate a stimulus controller
    $ bin/rails generate stimulus toastController
    
    # create the content of the controller
    $ echo '
    // app/javascript/controllers/toast_controller.js
    import { Controller } from "@hotwired/stimulus"
    import { Toast } from "bootstrap"
    
    // Connects to data-controller="toast"
    export default class extends Controller {
    
      connect() {
      }
    
      show() {
        const toastElement = document.getElementById("liveToast")
        const toast = new Toast(toastElement)
        toast.show();
      }
    }
    ' > app/javascript/controllers/toast_controller.js

    Start the dev servers for rails, jsbundling-rails and cssbundling-rails

    $ ./bin/dev

    Visit localhost:3000

    There is a button that could be selected and a toast will appear.

     
  • kmitov 6:28 pm on April 7, 2021 Permalink |
    Tags: , , , rails-ujs, , stimulus   

    From a ticket to deploy in an hour. jQuery runs the scripts, DOM does not. 

    (Everyday Code – instead of keeping our knowledge in a README.md let’s share it with the internet)

    This is an article of a production incidents with Rails/Stimulus/Rails-ujs/DOM and jQuery.

    We made mistake that made us lose 31 registrations on the FLLCasts and a few more on the BuildIn3D platform. The stack involved includes Ruby on Rails with Rails-ujs, Stimulus, DOM and jQuery. I will enter into some details about how it occurred, why and how we resolved it in under an hour. I am writing this article mainly to share with my our team, but I am sure it could be useful for other teams.

    FLLCasts registration form as of April 2021

    The incident – people could not Sign Up on the platforms

    On the Sign up page there is a captach. Years ago we found our that the captcha helps us reduce invalid registartion.
    There is also one more thing on this platform – usernames. As the FLLCasts platforms is used by students and academies, one of the very useful features on the platform is the automatically generated username. Users type their names in the “Full name” field and a username is automatically generated for them. In this way there is a unique username for users because email is not unique – there could be many users with the same email.

    To generate a new username we send a request to the server and refresh form. We use Stimulus and rails-ujs for this.

    The is an “change->registrations#refreshForm” for the input field for the name. When the full name is changed the form is refreshed and a unique username is returned from the server.

    <%= user_form.fields_for :data, {parent_builder: user_form} do |data_form| %>
      <%= data_form.text_field :name_attribute,
        {
          icon_prepend: "icon-finance-067 u-line-icon-pro",
          autofocus: true,
          data: {
            registrations_target: "name",
            action: "change->registrations#refreshForm"
          },
          autocomplete: "off"
        }
      %>
    
      <%= data_form.email_field :email %>
    <% end %>

    Here is how it should work:

    How the form is supposed to work

    The incident was that when the form was refresh the captcha got lost and we were not showing it after that. This means that on the server we were checking the captcha, but there was not captcha on the client.

    The ticket

    A wild ticket appears from a user.

    Hi,
    
    We wanted to enroll our Kid for Personal B programme and we were trying to Sign Up to pay online , but we are unable to sign up using our details.
    We get an error message though we tried a few times.
    
    Please help to sort this out.
    
    Thanks
    
    Regards

    This is a really wild ticket. The user wants to buy and they can not sign up. 1,2,3 go.

    The problem

    We’ve made the following commit in the registration form

    --- a/app/views/devise/registrations/new.js.erb
    +++ b/app/views/devise/registrations/new.js.erb
    @@ -1 +1 @@
    -$("#<%= @form_id %>").replaceWith("<%= escape_javascript(render 'form') %>")
    \ No newline at end of file
    +document.getElementById("<%=form_id%>").outerHTML= '<%= escape_javascript(render "#{partial_name}") %>';
    \ No newline at end of file

    We are in the process of removing jQuery from the code. I saw this jQueyr call and decided to change it to a simple DOM call. It should be the same, right? No…

    jQuery executes scrips while document.outerHTML does not

    I knew this. But I did not consider it in this commit. What happens is that we receive the new form from the server and replace the form on the page with the new form received from the server that contains the generate username.

    But in this form (and this form only on the whole platform) there is a recaptach. This recaptcha is a JavaScript and when dynamically changing the DOM the JavaScript must be executed. Well, jQuery automatically executed this for us. document.outerHTML does not.

    Developing and RSpec system spec.

    First I deployed the fix to production. It took about 5 minutes.

    After that I developed the spec. This is how the spec looks like:

    scenario "shows recaptach when dynamically reloading the form", js: true  do
        with_recaptcha_enabled do
          visit_sign_up
    
          expect(page).to have_recaptcha
    
          name_field.set unique_name
          email_field.set "#{SecureRandom.hex(16)}@example.com"
    
          # This means we have reloaded with js because we've set the email field 
          # and this has change the focus and there was a fire event for the name_field.
          #
          # We are now waiting for the username to appear on the screen
          expect(page).to have_username_filled
    
          expect(page).to have_recaptcha
        end
      end

    We expect that there is a recaptcha before and after refreshing the form.

    What surprised us

    Looking at the data in the time frame this commit was on production – we’ve received 9 registrations on the platform. In the same previous period we’ve receive 40 registrations.

    This means that we’ve lost 31 registrations on the platform.

    What surprised us is that 9 people managed to register. The only way you could register on the platform during this period is to first enter your username. If you enter the username on your own we do not refresh the form as we don’t have to generate a username for you. This means that about 25% of the people start with their username and about 75% of the users at FLLCasts start the registration with their name or email.

    Good to know. Good to know.

    Conclusion

    Looking at the issue we could have prevented it in a number of ways. In a test environment it is difficult to have a recaptcha because there is no way to test that the recaptcha works. After all that is the whole purpose of a recaptcha – to prevent bots and a Selenium driver is exactly a bot. But it turned out we’ve missed it as a scenario in the specs. It is always a missing spec.

     
  • kmitov 1:34 pm on March 25, 2021 Permalink |
    Tags: , , , stimulus   

    Stimulus 1.1.1 to Stimulus 2.0.0 – practical cost 

    We just migrated Stimulus 1.1.1. to Stimulus 2.0.0 so I decided to share this with our whole team, but I thought this could be useful for the whole community.

    The practical cost is that this migration could be done in a few minutes per Stimulus controller. 10-20 controllers – you should be done in less than a day.

    Overview of the changes

    Here are a few examples of the changes that were committed for a single _form.html.erb

    html.erb

    # app/views/public_users_searches/_form.html.erb
    -            target: "public-users-searches.term",
    +            public_users_searches_target: "term",
    ...
    -    <%= f.submit t('public_users_searches.form.search'), data: {target: "public-users-searches.commit", value: t('public_users_searches.form.search')} do %>
    +    <%= f.submit t('public_users_searches.form.search'), data: {public_users_searches_target: "commit", value: t('public_users_searches.form.search')} do %>
    

    Conclusion

    If you are wondering whether you should migrate or not and how much it will cost probably you should migrate. It is not that expensive.

     
  • kmitov 5:41 am on October 6, 2020 Permalink |
    Tags: , progressive web application, pwa, , , stimulus   

    The path to a progressive web app – or how we skipped the whole JS Frameworks on the client thing. 

    I recently responded to a question in the Stimulus JS forum that prompted me to write this blog.

    About 6 months ago we decided to skip the whole “JSON response from server and JS framework on the client” stuff and we’ve never felt better. We significantly reduce the code base while delivering more features. We manage to do it with much less resources. Here is an outline of our path

    Progressive Web Application

    We had a few problems.

    1. Specs that were way to fragile and user experience that was way to fragile. Fragile specs that are failing from time to time are not a problem on their own. They are an indicator that the features also do not work in the client browsers from time to time.
    2. Too much and two difficult JS on the client. Although it might see that making a JSON request from the client to the server and generating the HTML from the response might seem as a good idea, it is not always a good idea. If we are to generate the HTML from a JSON, why don’t we ask the server for the HTML and be done with it? Yes, some would say it is faster, but we found out it is basically the same for the server to render ‘{“video_src”: “https://…&#8221;}’ or to render “<video src=’https://…&#8217;></video>’ . The drawback is that in the first scenario you must generate the video tag on the client and this means more work. Like twice the amount of work.

    So we said:

    Let’s deliver the platform to a browser that has NO JS at all, and if it has, we would enhance it here and there.

    How it worked out?

    In retrospective… best decision even. Just know that there is not JS in the browser and try to deliver your features. Specs got a lot faster and better. 1h 40 m compared to 31 minutes. They are not fragile. We have very little JS. The whole platform is much faster. We user one framework less. So, I see no drawbacks.

    First we made the decision not to have a JS framework on the client and to drop this idea as a whole. For our case it was just adding complexity and one more framework. This does not happen overnight, but it could happen. So we decide that there is no JS and the whole platform should work in the case of JS disabled on the browser (this bootstrap navigation menus are a pain in the a…). It should be a progressive web application (PWA).

    After this decisions we did not replace JSON with Ajax calls. We skipped most of them entirely. Some JSON requests could not be skipped, but we changed them as AJAX – for example “generating a username”. When users register they could choose a username, but to make it easier for them we generate one by default. When generating we must make sure it is a username that does not exists in the DB. For this we need to make a request to the server and this is one place we are using Stimulus to submit the username.

    A place that we still use JSON is with Datatables- it is just so convenient. There are also a few progress bars that are making some legacy JSON requests.

    Overall we have Ajax here and there, an a few JSON requests, but that’s it. Like 90-95% of the workflow is working with JS disabled.

    We even took this to the extreme. We are testing it with browsers with JS and browsers without JS. So a delete button on a browser without JS is not opening a confirmation. But with JS enabled the delete opens a confirmation. I was afraid this will introduce a lot of logic in the specs, but I am still surprised it did not. We have one method “js_agnostic_delete” with an if statement that check if JS is enabled and decides what to do.

    My point is that moving JSON to Ajax 1:1 was not for us. It would not pay off as we would basically be doing the same, but in another format. What really payed off and allowed us to reduce the code base with like 30-40%, increase the speed and make the specs not so fragile was to say – “let’s deliver our platform to a JS disabled browser, and if it has JS, than great.”

    To give you even more context this was a set of decisions we made in April 2020 after years of getting tired with JS on client. We are also quite experience with JS as we’ve build a pretty large framework for 3D that is running entirely in browser so it was not like a lack of knowledge and experience with JS on our side that brought us to these decisions. I think whole team grew up enough to finally do without JS.

     
c
compose new post
j
next post/next comment
k
previous post/previous comment
r
reply
e
edit
o
show/hide comments
t
go to top
l
go to login
h
show/hide help
shift + esc
cancel