Skip to content

Instantly share code, notes, and snippets.

@mousemke
Last active March 17, 2017 07:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mousemke/2ea4c5756aa31631caf2638e3a5c3009 to your computer and use it in GitHub Desktop.
Save mousemke/2ea4c5756aa31631caf2638e3a5c3009 to your computer and use it in GitHub Desktop.

Modernizing Grails to a Front End App

v0.3.2


Issues

  • The DOM

    • not meeting accessibility standards
    • many seperate frameworks and libraries for DOM interaction are being used (jquery, knockout, etc) with no communication or cooperation between them
  • CSS

  • JavaScript

    • DOM nodes being overwritten instead of simply altering the information contained in them (template AJAX vs. a parsable JSON response)
      • javascript memory leaks caused not removing event listeners before removing the nodes
      • more redraws to add the new DOM nodes
    • our javascript is scattered throughout the app in script tags and has no inherent structure
      • assorted jquery scattered throughout templetes relying on global variables
      • functions are called when they dont apply
      • functions are defined arbitrarily in one template but used in a totally unrelated one, structurally speaking
      • 3rd party plugins
        • carry no assumption of quality
        • easy to add to the site but since they dont interact with out structure (if we had one) they are also easy to forget about
    • jquery version is old and slow
      • v1.7.2 (newest is > 3)
      • uses depreciated methods that are no longer maintained
  • The backend

    • serving HTML means we can only render pages; it's harder to move around raw data
      • JSON output would serve us much better by giving us the flexibilty we need while not needing to overhaul the backend
      • we would not need to maintain a seperate api for mobile
  • the mobile app

    • mobile and desktop apps should be the same responsive app
      • the only difference is CSS
      • we would only need to maintain one app

suggested way forward:

  • using this as a model, we can rebuild the instant access page as a proof of concept.
    • fully contained in css scope, handling it's own event, as well as having no global objects in the javascript namespace
  • remove all the grails variables and grails compiled javascript.
    • javascript can and should be handled by the front end
    • any variables that we now use for compiling the javascript should be made available in the JSON that is sent by the grails app
      • grails already has the info, so no extra work
      • security can be handled exactly the same as well since grails already handles it
  • faster
    • we can build using caching and an offline first mentality. This way the page can work on slow connections as well as offline
    • we can have less requests simply by not requesting resources we are not going to use

By this model we can slowly move, page by page, building in a way that is deployable in an incremental way, as well as test driven to make sure we are not missing any edge cases of page usage

eventual "perfect world" end result

  • Single page app
    • built in React
      • component / scoped-css arcitecture
      • virtual DOM ensures the DOM is only updated when it needs to be, making it much faster
      • no reloads between pages, yet preserves the history (back button)
      • through react / webpack we can reduce the scope of css to it's own modules
        • each component is it's own file
        • each component has it's own css that can't overlap with other components
    • Fully tested
      • unit tests
      • visual regression testing
      • 100% test coverage
    • Access the backend through REST api
      • only grabs the info we need, not the entire html for a new page
    • a11y compliant to meet accessibility standards for screen reader, keyboard only input, etc

example transition

as an example, lets take a very small example. In /grails-app/views/instantAccess/ we find _originalIA.gsp. We will be pulling the heroSlider out. First, we import the component

http://pics.knoblau.ch/LoginHeader.jsx__secret-escapes__val_atob-server_shijiezhichuang_react-frontend_2017-02-28_17-51-25.png

Then replace the template element with the imported component. This will be the mount point. Everything below this is controlled by react. As the conversion progresses, this point will move up the hierarchy until it is the top DOM node and there are no server side templates left.

http://pics.knoblau.ch/_originalIA.gsp__secret-escapes__val_atob-server_shijiezhichuang_2017-02-28_16-49-08.png

http://pics.knoblau.ch/_originalIA.gsp__secret-escapes__val_atob-server_shijiezhichuang_react-frontend_2017-02-28_17-53-14.png

now take a look at the template itself.

http://pics.knoblau.ch/_heroSlider.gsp__secret-escapes__val_atob-server_shijiezhichuang_2017-02-28_16-51-04.png

the original template is quite easily expressed in JSX as a react coponent.

http://pics.knoblau.ch/untitled__secret-escapes__val_atob-server_shijiezhichuang_react-frontend_2017-02-28_17-14-22.png

you'll notice that the if is now handled by the props that are coming into the constructor. These are variables coming in from the JSON. Now that we have that we can make the slides highly changeable and expandable. (note: this list could also come from the JSON making it changeable from a theoretical admin panel that doesnt exist that i'm aware of)

http://pics.knoblau.ch/HeroSlider.jsx__secret-escapes__val_atob-server_shijiezhichuang_react-frontend_2017-02-28_17-15-39.png

so lets take a look at the CSS (dramatically reduced for the purposes of this example)

http://pics.knoblau.ch/untitled__secret-escapes__val_atob-server_shijiezhichuang_2017-02-28_17-08-16.png

it does the job, but CSS can be quite annoying debug. suppose there's something else in the app that uses the class container or slide. In our new setup, we can scope this CSS to only be available to this component. First we include the CSS for the javascript component (it will be in the same directory):

http://pics.knoblau.ch/_heroSlider.gsp__secret-escapes__val_atob-server_shijiezhichuang_2017-02-28_17-10-14.png

http://pics.knoblau.ch/HeroSlider.jsx__secret-escapes__val_atob-server_shijiezhichuang_react-frontend_2017-02-28_17-16-10.png

with our styles added to the component we can start to use our scoped CSS from the styles object

http://pics.knoblau.ch/HeroSlider.jsx__secret-escapes__val_atob-server_shijiezhichuang_react-frontend_2017-02-28_17-23-56.png

The scoped CSS classes end up looking like this

http://pics.knoblau.ch/Join_now_for_Free__Save_up_to_70_on_luxury_travel__Secret_Escapes_2017-02-28_17-30-50.png

I would rather we build this part completely in house, but for now, lets use the JavaScript we have and import it

http://pics.knoblau.ch/HeroSlider.jsx__secret-escapes__val_atob-server_shijiezhichuang_react-frontend_2017-02-28_17-35-39.png

then we take our implementation js

http://pics.knoblau.ch/hero-slider.js__secret-escapes__val_atob-server_shijiezhichuang_react-frontend_2017-02-28_17-33-10.png

clean it up, update it to ES6, drop it in our component, and make it happen once the component is mounted. Notice, now we've added the wrapper as a ref. This enables us to use it without searching the DOM for it. Another page-load saver

http://pics.knoblau.ch/HeroSlider.jsx__secret-escapes__val_atob-server_shijiezhichuang_react-frontend_2017-02-28_17-45-10.png

That's it. Rinse. Repeat until done.

Finished Component: (slide urls coming in from props. it will render as many as you throw at it)

in the finished component, the commponent takes 2 props:

http://pics.knoblau.ch/_originalIA.gsp__secret-escapes__val_atob-server_shijiezhichuang_react-frontend_2017-02-28_18-38-54.png

import React, { Component } from 'react';
import Swiper               from './idangerous.swiper.js';

import styles               from './HeroSlider.css';


/**
 * ## HeroSlider
 *
 * renders the Hero Slider component
 */
class HeroSlider extends Component
{
    /**
     * ## buildSlider
     *
     * builds the swiper into the rendered DOM
     *
     * @param {DOMElement} sliderEl wrapper elementto build the slider on
     *
     * @return {Object} Swiper instance
     */
    buildSlider( sliderEl )
    {
        return new Swiper( sliderEl,
            {
                autoplay    : 4000,
                direction   : 'vertical',
                loop        : true
            } );
    }


    /**
     * ## componentDidMount
     *
     * after rendering the DOM, this activates the swiper
     *
     * @return {Void} void
     */
    componentDidMount()
    {
        this.slider = this.buildSlider( this.refs.heroSlider );
    }


    /**
     * ## constructor
     */
    constructor()
    {
        super();
    }


    /**
     * ## render
     *
     * @return {JSX} rendered instant access page
     */
    render()
    {
        const {
            iaSpecificPage,
            slides
        } = this.props;

        return (
            <div className={ `${styles.container} hero-slider` }
                    ref="heroSlider">
                <div className={ styles.wrapper }>
                    {
                        iaSpecificPage && iaSpecificPage.backgroundImage ?
                            <div className={ styles.slide }>
                                <img src={ iaSpecificPage.backgroundImage } />
                            </div> : null
                    }
                    {
                        slides.map( slide =>
                        {
                            return (
                                <div className={ styles.slide }>
                                    <img src={ slide } />
                                </div>
                            );
                        } )
                    }
                </div>
            </div>
        );
    }
}


HeroSlider.propTypes = {
    iaSpecificPage  : React.PropTypes.obj,
    slides          : React.PropTypes.array
};


export default HeroSlider;

the final es5 transpiled code:

'use strict';var _createClass=function(){function a(b,c){for(var e,d=0;d<c.length;d++)e=c[d],e.enumerable=e.enumerable||!1,e.configurable=!0,'value'in e&&(e.writable=!0),Object.defineProperty(b,e.key,e)}return function(b,c,d){return c&&a(b.prototype,c),d&&a(b,d),b}}(),_react=require('react'),_react2=_interopRequireDefault(_react),_idangerousSwiper=require('./idangerous.swiper.js'),_idangerousSwiper2=_interopRequireDefault(_idangerousSwiper),_HeroSlider=require('./HeroSlider.css'),_HeroSlider2=_interopRequireDefault(_HeroSlider);Object.defineProperty(exports,'__esModule',{value:!0});function _interopRequireDefault(a){return a&&a.__esModule?a:{default:a}}function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError('Cannot call a class as a function')}function _possibleConstructorReturn(a,b){if(!a)throw new ReferenceError('this hasn\'t been initialised - super() hasn\'t been called');return b&&('object'==typeof b||'function'==typeof b)?b:a}function _inherits(a,b){if('function'!=typeof b&&null!==b)throw new TypeError('Super expression must either be null or a function, not '+typeof b);a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,enumerable:!1,writable:!0,configurable:!0}}),b&&(Object.setPrototypeOf?Object.setPrototypeOf(a,b):a.__proto__=b)}var HeroSlider=function(a){function b(){return _classCallCheck(this,b),_possibleConstructorReturn(this,(b.__proto__||Object.getPrototypeOf(b)).call(this))}return _inherits(b,a),_createClass(b,[{key:'buildSlider',value:function buildSlider(c){return new _idangerousSwiper2.default(c,{autoplay:4e3,direction:'vertical',loop:!0})}},{key:'componentDidMount',value:function componentDidMount(){this.slider=this.buildSlider(this.refs.heroSlider)}}]),_createClass(b,[{key:'render',value:function render(){var _props=this.props,c=_props.iaSpecificPage,d=_props.slides;return _react2.default.createElement('div',{className:_HeroSlider2.default.container+' hero-slider',ref:'heroSlider'},_react2.default.createElement('div',{className:_HeroSlider2.default.wrapper},c&&c.backgroundImage?_react2.default.createElement('div',{className:_HeroSlider2.default.slide},_react2.default.createElement('img',{src:c.backgroundImage})):null,d.map(function(e){return _react2.default.createElement('div',{className:_HeroSlider2.default.slide},_react2.default.createElement('img',{src:e}))})))}}]),b}(_react.Component);HeroSlider.propTypes={iaSpecificPage:_react2.default.PropTypes.obj,slides:_react2.default.PropTypes.array},exports.default=HeroSlider;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment