Sane User Interfaces for Ruby on Rails

Ca839bc293e4ca6f9fa327cf95a414a9?s=47 Ahmed Omran
November 15, 2017

Sane User Interfaces for Ruby on Rails

Ruby on Rails is a great experience on the backend. Unfortunately, the user interface is a sad story. Your Rails views are the riskiest part of your application; they are hard to maintain, hard to reason about and hard to test. Some turn to single-page applications, but they can be expensive and time-consuming to build. What if we took full advantage of the best of rails and combined it with small testable UI components using modern tools and techniques.

Ca839bc293e4ca6f9fa327cf95a414a9?s=128

Ahmed Omran

November 15, 2017
Tweet

Transcript

  1. Sane User Interfaces for Ruby on Rails @this_ahmed

  2. Developer Happiness

  3. Rails Way non-Rails UI Solutions

  4. Action View Powerful & Easy to use Rails Way non-Rails

  5. Growing Pains

  6. <%= system('gem install rails') %>

  7. Action View • Global Scope • Hard to maintain •

    Hard to reason about • Hard to test
  8. Are we missing an object?

  9. Action View Decorator

  10. class CommentDecorator < SimpleDelegator def gravatar end def timestamp end

    def author_name end end CommentDecorator.new(comment)
  11. Fat Decorator

  12. Comment Decorator Comment Box Reaction Buttons Comment Form Comment Thread

    Embeds Components
  13. class ViewComponent include ActiveModel::Model attr_accessor :context def render context.render( partial:

    "components/#{template_path}", locals: { component: self } ) end private def template_path self.class.to_s.underscore end end
  14. # object at app/view_components/comment_box.rb # template at app/views/components/_comment_box.html.erb class CommentBox

    < ViewComponent end class CommentThread < ViewComponent end class CommentForm < ViewComponent end
  15. def render_component(component, props) component .new({ context: self }.merge(props)) .render end

    <%= render_component(CommentForm, {}) %> Helper
  16. View Components • Single Responsibility • Easier to reason about

    • Easier to test • Not great for interactivity (need JavaScript)
  17. Action View Decorator Components Sprinkle JavaScript

  18. Action View Decorator Components Front-end MVC Rails Way non-Rails

  19. Duplicate effort • Routing • Data Layer (models, JSON, etc.)

    • Authentication / Authorization • Deploy multiple apps • CORS, proxying worth it sometimes
  20. JavaScript Fatigue

  21. Emerging Themes …

  22. Web Components Components

  23. Modern Tooling

  24. Testing & Quality

  25. Server-Side Rendering • Static Content • Search Engine Crawlers: SEO

    • First render performance
  26. Action View Decorator Components JS Components Modern JS Tools Server

    Rendering Rails Way non-Rails
  27. Pragmatic Middle

  28. Action View Turbolinks, Caching, etc. Server Render by Default

  29. Modern JS Tools Action View Proper Front-End Tools

  30. JS Components Modern JS Tools Action View Components for Complex

    UIs
  31. Example: copy to clipboard

  32. # Install Node Version Manager: # https://github.com/creationix/nvm $ nvm install

    --lts $ nvm use --lts $ brew install yarn --without-node
  33. $ cd my-rails-app $ yarn init # creates package.json skip

    for Rails 5.1+
  34. # config/initializers/assets.rb Rails.application.config.assets.paths << Rails.root.join('node_modules') # gitignore /node_modules/* skip for

    Rails 5.1+
  35. $ yarn add primer-tooltips

  36. /*package.json*/ { "name": "my-rails-app", "private": true, "dependencies": { "primer-tooltips": “^1.4.1"

    }, "engines": { "node": “>=8.9.0”, "yarn": “^1.3.2" } }
  37. app/assets/stylesheets/application.css *= require primer-tooltips/index The file is located here: node_modules/primer-tooltips/index.scss

  38. <button class="tooltipped tooltipped-w" aria-label="Copy to Clipboard!"> Copy </button>

  39. how do we add copy behaviour?

  40. Convert to React Component

  41. # Rails 5.1+ $ rails new myapp --webpack=react # Rails

    version < 5.1 gem ‘webpacker' $ bin/rails webpacker:install $ bin/rails webpacker:install:react
  42. None
  43. Entry app/javascript/packs/application.js

  44. require dependencies

  45. <%= javascript_pack_tag 'application' %>
 <%= asset_pack_path 'images/one.png' %>

  46. • Converts latest JavaScript to a target client • For

    example: target browsers which have >1% usage
  47. // app/javascript/packs/application.js document.addEventListener("turbolinks:load", reactLoader); document.addEventListener("turbolinks:before- render", reactUnloader);

  48. $ yarn add clipboard

  49. // app/javascript/clipboard_button/index.jsx import React from 'react' import ReactDOM from 'react-dom'

    import Clipboard from "clipboard"; class ClipboardButton extends React.Component { ... } export default ClipboardButton
  50. Register Component in ERB def react_component(component_name, props) tag.div data: {

    component_name: component_name, react_props: props.to_json } end <%= react_component('ClipboardButton', { content: 'copy this!' }) %>
  51. // app/javascript/utils/react_loader.js import ClipboardButton from “../clipboard_button/ index"; const RegisteredComponentTypes =

    { ClipboardButton: ClipboardButton }; Register Component in JS
  52. // app/javascript/utils/react_loader.js const containers = document.querySelectorAll("[data-component- name]"); containers.forEach(function(el) { const

    componentName = el.getAttribute("data-component-name"); const type = RegisteredComponentTypes[componentName]; mountComponent(type, el); });
  53. // app/javascript/utils/react_loader.js const extractProps = el => JSON.parse(el.getAttribute("data-react-props")); const mountComponent

    = function(ComponentType, node) { const props = extractProps(node); const element = <ComponentType {...props} />; ReactDOM.render(element, node); };
  54. // app/javascript/utils/react_unloader.js const components = document.querySelectorAll("[data- component-name]"); components.forEach(function(node) { ReactDOM.unmountComponentAtNode(node);

    });
  55. JS Components Modern JS Tools Action View Options…

  56. Sane User Interfaces for Ruby on Rails @this_ahmed