v0.3.2
-
The DOM
- not meeting accessibility standards
- https://una.im/pa11y-dash/
- unnecessarily nested unlabelled divs dont make any sense structurally and do not meet HTML5 guidelines http://www.quackit.com/html_5/tags/
- many seperate frameworks and libraries for DOM interaction are being used (jquery, knockout, etc) with no communication or cooperation between them
- not meeting accessibility standards
-
CSS
- using ids, tags, and
important
leads to broken styles!important
breaks the cascading nature of CSS http://stackoverflow.com/a/3427813- ids break specificity http://screwlewse.com/2010/07/dont-use-id-selectors-in-css/
- tags are slower than classes and can apply to things you dont intent them to https://jsperf.com/id-vs-class-vs-tag-selectors/466/
- classes added and removed for javascript functions should be functionally seperate from classes added for styling (for instance javascript adding
.js-content
instead of.content
)
- using ids, tags, and
-
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
- DOM nodes being overwritten instead of simply altering the information contained in them (template AJAX vs. a parsable JSON response)
-
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
- serving HTML means we can only render pages; it's harder to move around raw data
-
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
- mobile and desktop apps should be the same responsive app
- 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
- 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
- built in React
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
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.
now take a look at the template itself.
the original template is quite easily expressed in JSX as a react coponent.
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)
so lets take a look at the CSS (dramatically reduced for the purposes of this example)
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):
with our styles added to the component we can start to use our scoped CSS from the styles
object
The scoped CSS classes end up looking like this
I would rather we build this part completely in house, but for now, lets use the JavaScript we have and import it
then we take our implementation js
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
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:
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;