Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Framework: Add makeLayout #5081

Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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' ) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the motivation for adding this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem to fix the memory leak going /design -> /start and back.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the motivation for adding this?

This: #5081 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(which was causing #5081 (comment))

}
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.
51 changes: 51 additions & 0 deletions client/controller/index.node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* External Dependencies
*/
import React from 'react';
import { Provider as ReduxProvider } from 'react-redux';

/**
* Internal dependencies
*/
import LayoutLoggedOut from 'layout/logged-out';
import { getCurrentUser } from 'state/current-user/selectors';

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

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

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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for having controllers under a folder? I prefer to have the files directly in the root.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because we need separate client/server versions of the controller, in combination with the package.json browser field trick.

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 './index.node.js';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go in shared.js right? :)

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"
}
10 changes: 10 additions & 0 deletions client/controller/shared.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { setSection as setSectionAction } from 'state/ui/actions';
import noop from 'lodash/noop';

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