Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Sane User Interfaces for Ruby on Rails

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.

Ahmed Omran

November 15, 2017
Tweet

More Decks by Ahmed Omran

Other Decks in Programming

Transcript

  1. Sane User Interfaces
    for Ruby on Rails
    @this_ahmed

    View Slide

  2. Developer Happiness

    View Slide

  3. Rails Way non-Rails
    UI Solutions

    View Slide

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

    View Slide

  5. Growing Pains

    View Slide

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

    View Slide

  7. Action View
    • Global Scope

    • Hard to maintain

    • Hard to reason about

    • Hard to test

    View Slide

  8. Are we missing an
    object?

    View Slide

  9. Action
    View
    Decorator

    View Slide

  10. class CommentDecorator < SimpleDelegator
    def gravatar
    end
    def timestamp
    end
    def author_name
    end
    end
    CommentDecorator.new(comment)

    View Slide

  11. Fat
    Decorator

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  15. def render_component(component, props)
    component
    .new({ context: self }.merge(props))
    .render
    end
    <%= render_component(CommentForm, {}) %>
    Helper

    View Slide

  16. View Components
    • Single Responsibility

    • Easier to reason about

    • Easier to test

    • Not great for interactivity (need JavaScript)

    View Slide

  17. Action
    View
    Decorator
    Components
    Sprinkle JavaScript

    View Slide

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

    View Slide

  19. Duplicate effort
    • Routing

    • Data Layer (models, JSON, etc.)

    • Authentication / Authorization

    • Deploy multiple apps

    • CORS, proxying
    worth it sometimes

    View Slide

  20. JavaScript Fatigue

    View Slide

  21. Emerging Themes …

    View Slide

  22. Web
    Components
    Components

    View Slide

  23. Modern Tooling

    View Slide

  24. Testing & Quality

    View Slide

  25. Server-Side Rendering
    • Static Content

    • Search Engine Crawlers: SEO

    • First render performance

    View Slide

  26. Action
    View
    Decorator
    Components
    JS Components
    Modern JS Tools
    Server Rendering
    Rails Way non-Rails

    View Slide

  27. Pragmatic Middle

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. Example: copy to clipboard

    View Slide

  32. # Install Node Version Manager:
    # https://github.com/creationix/nvm
    $ nvm install --lts
    $ nvm use --lts
    $ brew install yarn --without-node

    View Slide

  33. $ cd my-rails-app
    $ yarn init # creates package.json
    skip for Rails 5.1+

    View Slide

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

    View Slide

  35. $ yarn add primer-tooltips

    View Slide

  36. /*package.json*/
    {
    "name": "my-rails-app",
    "private": true,
    "dependencies": {
    "primer-tooltips": “^1.4.1"
    },
    "engines": {
    "node": “>=8.9.0”,
    "yarn": “^1.3.2"
    }
    }

    View Slide

  37. app/assets/stylesheets/application.css
    *= require primer-tooltips/index
    The file is located here: node_modules/primer-tooltips/index.scss

    View Slide

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

    View Slide

  39. how do we add copy behaviour?

    View Slide

  40. Convert to React Component

    View Slide

  41. # Rails 5.1+
    $ rails new myapp --webpack=react
    # Rails version < 5.1
    gem ‘webpacker'
    $ bin/rails webpacker:install
    $ bin/rails webpacker:install:react

    View Slide

  42. View Slide

  43. Entry
    app/javascript/packs/application.js

    View Slide

  44. require dependencies

    View Slide

  45. <%= javascript_pack_tag 'application' %>

    <%= asset_pack_path 'images/one.png' %>

    View Slide

  46. • Converts latest JavaScript to a target client

    • For example: target browsers which have >1% usage

    View Slide

  47. // app/javascript/packs/application.js
    document.addEventListener("turbolinks:load",
    reactLoader);
    document.addEventListener("turbolinks:before-
    render", reactUnloader);

    View Slide

  48. $ yarn add clipboard

    View Slide

  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

    View Slide

  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!' }) %>

    View Slide

  51. // app/javascript/utils/react_loader.js
    import ClipboardButton from “../clipboard_button/
    index";
    const RegisteredComponentTypes = {
    ClipboardButton: ClipboardButton
    };
    Register Component in JS

    View Slide

  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);
    });

    View Slide

  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 = ;
    ReactDOM.render(element, node);
    };

    View Slide

  54. // app/javascript/utils/react_unloader.js
    const components = document.querySelectorAll("[data-
    component-name]");
    components.forEach(function(node) {
    ReactDOM.unmountComponentAtNode(node);
    });

    View Slide

  55. JS Components
    Modern JS Tools
    Action View
    Options…

    View Slide

  56. Sane User Interfaces
    for Ruby on Rails
    @this_ahmed

    View Slide