Skip to content

Commit

Permalink
Framework: Add makeLayout (#5081)
Browse files Browse the repository at this point in the history
* Framework: Add makeLayout() middleware

* Allow elements as props to the layout component
* Add a ReduxWrappedLayout helper function
* Split the entry points for the isomorphic router controller
(allowing client-specific dependencies)

* Framework: fix layout section proptype

* Themes: Remove obsolete comment

* my-sites/controller: Export makeNavigation middleware

* Themes: Use makeNavigation and makeLayout for rendering logged-in

* client/boot: Remove an obsolete check

* my-sites/controller: Use JSX for createNavigation() return value

* client/controller: Make README.md more verbose

* client/controller: Move shared code to shared.js

* Framework: Unmount multi-tree layout before rendering single-tree

* Themes: Remove siteSelection middleware from multi-site route

* Layout: Conditionally add wp-singletree-layout class

* client/controller: Drop layoutFocus import

* client/controller: Move makeLayoutMiddleware to shared.js
  • Loading branch information
ockham authored Aug 10, 2016
1 parent 9113583 commit 0982dd4
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 99 deletions.
1 change: 0 additions & 1 deletion client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ These are some of the key modules of the application, kept in `client`'s root fo
* `boot` - the booting file that sets up the application and requires the main sections.
* `config` - generated configuration settings.
* `layout` - handles the main React layout, including the masterbar. Notably, it sets #primary and #secondary used to render the different sections.
* `controller.js` - isomorphic routing helper, see comments in that file.
* `sections.js` - defines section groups, paths, and main modules. (Used by webpack to generate separate chunks.)

### Components
Expand Down
38 changes: 17 additions & 21 deletions client/boot/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ require( 'lib/local-storage' )();
* External dependencies
*/
var React = require( 'react' ),
ReactDom = require( 'react-dom' ),
store = require( 'store' ),
ReactInjection = require( 'react/lib/ReactInjection' ),
some = require( 'lodash/some' ),
Expand Down Expand Up @@ -38,24 +39,19 @@ var config = require( 'config' ),
sites = require( 'lib/sites-list' )(),
superProps = require( 'lib/analytics/super-props' ),
translatorJumpstart = require( 'lib/translator-jumpstart' ),
translatorInvitation = require( 'layout/community-translator/invitation-utils' ),
nuxWelcome = require( 'layout/nux-welcome' ),
emailVerification = require( 'components/email-verification' ),
viewport = require( 'lib/viewport' ),
detectHistoryNavigation = require( 'lib/detect-history-navigation' ),
pushNotificationsInit = require( 'state/push-notifications/actions' ).init,
sections = require( 'sections' ),
touchDetect = require( 'lib/touch-detect' ),
setRouteAction = require( 'state/ui/actions' ).setRoute,
accessibleFocus = require( 'lib/accessible-focus' ),
TitleStore = require( 'lib/screen-title/store' ),
syncHandler = require( 'lib/wp/sync-handler' ),
renderWithReduxStore = require( 'lib/react-helpers' ).renderWithReduxStore,
bindWpLocaleState = require( 'lib/wp/localization' ).bindState,
supportUser = require( 'lib/user/support-user-interop' ),
createReduxStoreFromPersistedInitialState = require( 'state/initial-state' ).default,
// The following components require the i18n mixin, so must be required after i18n is initialized
Layout;
createReduxStoreFromPersistedInitialState = require( 'state/initial-state' ).default;

import { getSelectedSiteId, getSectionName, isSectionIsomorphic } from 'state/ui/selectors';
import { setNextLayoutFocus, activateNextLayoutFocus } from 'state/ui/layout-focus/actions';
Expand Down Expand Up @@ -179,17 +175,15 @@ function boot() {
}

function renderLayout( reduxStore ) {
const props = {};
const Layout = require( 'controller' ).ReduxWrappedLayout;

if ( user.get() ) {
Object.assign( props, { user, sites, nuxWelcome, translatorInvitation } );
}
const layoutElement = React.createElement( Layout, {
store: reduxStore
} );

Layout = require( 'layout' );
renderWithReduxStore(
React.createElement( Layout, props ),
document.getElementById( 'wpcom' ),
reduxStore
ReactDom.render(
layoutElement,
document.getElementById( 'wpcom' )
);

debug( 'Main layout rendered.' );
Expand Down Expand Up @@ -225,12 +219,9 @@ function reduxStoreReady( reduxStore ) {
require( 'lib/network-connection' ).init( reduxStore );
}

// Render Layout only for non-isomorphic sections, unless logged-in.
// Isomorphic sections will take care of rendering their Layout last themselves,
// unless in logged-in mode, where we can't do that yet.
// TODO: Remove the ! user.get() check once isomorphic sections render their
// Layout themselves when logged in.
if ( ! isIsomorphic || user.get() ) {
// Render Layout only for non-isomorphic sections.
// Isomorphic sections will take care of rendering their Layout last themselves.
if ( ! isIsomorphic ) {
renderLayout( reduxStore );

if ( config.isEnabled( 'catch-js-errors' ) ) {
Expand Down Expand Up @@ -328,6 +319,7 @@ function reduxStoreReady( reduxStore ) {
}

// Load the application modules for the various sections and features
const sections = require( 'sections' );
sections.load();

// delete any lingering local storage data from signup
Expand Down Expand Up @@ -431,7 +423,11 @@ function reduxStoreReady( reduxStore ) {

if ( isMultiTreeLayout && previousLayoutIsSingleTree ) {
debug( 'Re-rendering multi-tree layout' );

renderLayout( context.store );
} else if ( ! isMultiTreeLayout && ! previousLayoutIsSingleTree ) {
debug( 'Unmounting multi-tree layout' );
ReactDom.unmountComponentAtNode( document.getElementById( 'wpcom' ) );
}
next();
} );
Expand Down
15 changes: 15 additions & 0 deletions client/controller/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Isomorphic Routing Helpers
==========================

Provides middleware to be used agnostically with either `page.js`, or with
`express` (through middleware adapters found in `server/`). Since some of the
middleware needs to function differently on the server than on the client,
both versions are provided transparently in those cases (by `index.web.js` and
`index.node.js`, respectively.)

These middlewares include:
* `makeLayout`: creates a `Layout` (or `LayoutLoggedOut`) component in `context.layout`.
Accepts `primary`, `secondary`, and `tertiary` arguments which it will use to
populate the corresponding `<div>`s.
* `clientRouter`: Essentially an alias for `page`, which invokes a client-side
`render` middleware after all other middlewares.
33 changes: 33 additions & 0 deletions client/controller/index.node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* External Dependencies
*/
import React from 'react';
import { Provider as ReduxProvider } from 'react-redux';

/**
* Internal dependencies
*/
import { makeLayoutMiddleware } from './shared.js';
import LayoutLoggedOut from 'layout/logged-out';

/**
* Re-export
*/
export { setSection } from './shared.js';

const ReduxWrappedLoggedOutLayout = ( { store, primary, secondary, tertiary } ) => (
<ReduxProvider store={ store }>
<LayoutLoggedOut primary={ primary }
secondary={ secondary }
tertiary={ tertiary } />
</ReduxProvider>
);

/**
* @param { object } context -- Middleware context
* @param { function } next -- Call next middleware in chain
*
* Produce a `LayoutLoggedOut` element in `context.layout`, using
* `context.primary`, `context.secondary`, and `context.tertiary` to populate it.
*/
export const makeLayout = makeLayoutMiddleware( ReduxWrappedLoggedOutLayout );
71 changes: 32 additions & 39 deletions client/controller.js → client/controller/index.web.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,49 @@
import React from 'react';
import ReactDom from 'react-dom';
import { Provider as ReduxProvider } from 'react-redux';
import { setSection as setSectionAction } from 'state/ui/actions';
import noop from 'lodash/noop';
import page from 'page';

/**
* Internal dependencies
* Internal Dependencies
*/
import page from 'page';
import Layout from 'layout';
import LayoutLoggedOut from 'layout/logged-out';
import nuxWelcome from 'layout/nux-welcome';
import translatorInvitation from 'layout/community-translator/invitation-utils';
import { makeLayoutMiddleware } from './shared.js';
import { getCurrentUser } from 'state/current-user/selectors';
import userFactory from 'lib/user';
import sitesFactory from 'lib/sites-list';
import debugFactory from 'debug';

const debug = debugFactory( 'calypso:controller' );

/**
* @param { object } context -- Middleware context
* @param { function } next -- Call next middleware in chain
*/
export function makeLayout( context, next ) {
const isLoggedIn = !! getCurrentUser( context.store.getState() );
if ( ! isLoggedIn ) {
context.layout = makeLoggedOutLayout( context );
} // TODO: else { makeLoggedInLayout( context ); }
next();
}
* Re-export
*/
export { setSection } from './shared.js';

/**
* @param { object } context -- Middleware context
* @returns { object } `LoggedOutLayout` element
*
* Return a `LayoutLoggedOut` element, using `context.primary`,
* `context.secondary`, and `context.tertiary` to populate it.
*/
function makeLoggedOutLayout( context ) {
const { store, primary, secondary, tertiary } = context;
return (
<ReduxProvider store={ store }>
<LayoutLoggedOut primary={ primary }
const user = userFactory();
const sites = sitesFactory();
const debug = debugFactory( 'calypso:controller' );

export const ReduxWrappedLayout = ( { store, primary, secondary, tertiary } ) => (
<ReduxProvider store={ store }>
{ getCurrentUser( store.getState() )
? <Layout primary={ primary }
secondary={ secondary }
tertiary={ tertiary }
user={ user }
sites={ sites }
nuxWelcome={ nuxWelcome }
translatorInvitation={ translatorInvitation }
/>
: <LayoutLoggedOut primary={ primary }
secondary={ secondary }
tertiary={ tertiary } />
</ReduxProvider>
);
}
}
</ReduxProvider>
);

export const makeLayout = makeLayoutMiddleware( ReduxWrappedLayout );

/**
* Isomorphic routing helper, client side
Expand All @@ -65,14 +66,6 @@ export function clientRouter( route, ...middlewares ) {
page( route, ...middlewares, render );
}

export function setSection( section ) {
return ( context, next = noop ) => {
context.store.dispatch( setSectionAction( section ) );

next();
};
}

function render( context ) {
context.layout
? renderSingleTree( context )
Expand Down
4 changes: 4 additions & 0 deletions client/controller/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "index.node.js",
"browser": "index.web.js"
}
37 changes: 37 additions & 0 deletions client/controller/shared.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* External dependencies
*/
import React from 'react';
import noop from 'lodash/noop';

/**
* Internal dependencies
*/
import { setSection as setSectionAction } from 'state/ui/actions';
import { getCurrentUser } from 'state/current-user/selectors';

export function makeLayoutMiddleware( LayoutComponent ) {
return ( context, next ) => {
const { store, primary, secondary, tertiary } = context;

// On server, only render LoggedOutLayout when logged-out.
if ( ! context.isServerSide || getCurrentUser( context.store.getState() ) ) {
context.layout = (
<LayoutComponent store={ store }
primary={ primary }
secondary={ secondary }
tertiary={ tertiary }
/>
);
}
next();
};
}

export function setSection( section ) {
return ( context, next = noop ) => {
context.store.dispatch( setSectionAction( section ) );

next();
};
}
34 changes: 30 additions & 4 deletions client/layout/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ Layout = React.createClass( {

_sitesPoller: null,

propTypes: {
primary: React.PropTypes.element,
secondary: React.PropTypes.element,
tertiary: React.PropTypes.element,
sites: React.PropTypes.object,
user: React.PropTypes.object,
nuxWelcome: React.PropTypes.object,
translatorInvitation: React.PropTypes.object,
focus: React.PropTypes.object,
// connected props
isLoading: React.PropTypes.bool,
isSupportUser: React.PropTypes.bool,
section: React.PropTypes.oneOfType( [
React.PropTypes.bool,
React.PropTypes.object,
] ),
isOffline: React.PropTypes.bool,
},

componentWillUpdate: function( nextProps ) {
if ( this.props.section.group !== nextProps.section.group ) {
if ( nextProps.section.group === 'sites' ) {
Expand Down Expand Up @@ -168,7 +187,8 @@ Layout = React.createClass( {
`is-section-${this.props.section.name}`,
`focus-${this.props.currentLayoutFocus}`,
{ 'is-support-user': this.props.isSupportUser },
{ 'has-no-sidebar': ! this.props.hasSidebar }
{ 'has-no-sidebar': ! this.props.hasSidebar },
{ 'wp-singletree-layout': !! this.props.primary }
),
loadingClass = classnames( {
layout__loader: true,
Expand All @@ -188,10 +208,16 @@ Layout = React.createClass( {
{ this.renderWelcome() }
{ this.renderPushNotificationPrompt() }
<GlobalNotices id="notices" notices={ notices.list } forcePinned={ 'post' === this.props.section.name } />
<div id="primary" className="layout__primary" />
<div id="secondary" className="layout__secondary" />
<div id="primary" className="layout__primary">
{ this.props.primary }
</div>
<div id="secondary" className="layout__secondary">
{ this.props.secondary }
</div>
</div>
<div id="tertiary">
{ this.props.tertiary }
</div>
<div id="tertiary" />
<TranslatorLauncher
isEnabled={ translator.isEnabled() }
isActive={ translator.isActivated() }/>
Expand Down
Loading

0 comments on commit 0982dd4

Please sign in to comment.