Skip to content

Commit

Permalink
Guided Tours: Kill component state, use state.ui.guidesTour
Browse files Browse the repository at this point in the history
- Note use of createSelector in order to benefit from strict equality
  checks in GuidesTours#shouldComponentUpdate.

- This architecture should make a number of things easier in the future,
  including adding a preview of the frontend during the tour (#4461).
  • Loading branch information
mcsf committed Apr 12, 2016
1 parent dc597b5 commit d77518b
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 22 deletions.
5 changes: 4 additions & 1 deletion client/boot/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,10 @@ function reduxStoreReady( reduxStore ) {

// If `?tour` is present, show the guides tour
if ( config.isEnabled( 'guidestours' ) && context.query.tour ) {
context.store.dispatch( showGuidesTour( { shouldShow: true, tour: context.query.tour } ) );
context.store.dispatch( showGuidesTour( {
shouldShow: true,
tour: context.query.tour,
} ) );
}

// Bump general stat tracking overall Newdash usage
Expand Down
2 changes: 2 additions & 0 deletions client/guidestours/config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/** @ssr-ready **/

/**
* External dependencies
*/
Expand Down
48 changes: 28 additions & 20 deletions client/guidestours/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
* External dependencies
*/
import React, { Component } from 'react'
import { connect } from 'react-redux';

/**
* Internal dependencies
*/
import guideConfig from './config';
import { getSelectedSite, getGuidesTourState } from 'state/ui/selectors';
import { nextGuidesTourStep } from 'state/ui/actions';
import { query } from './positioning';
import {
GuidesBasicStep,
Expand All @@ -16,32 +18,29 @@ import {
GuidesActionStep,
} from './steps';

export default class GuidesTours extends Component {
class GuidesTours extends Component {
constructor() {
super();
this.bind( 'next', 'quit' );
this.state = { currentStep: guideConfig.init };
}

bind( ...methods ) {
methods.forEach( m => this[ m ] = this[ m ].bind( this ) );
}

componentDidMount() {
const { stepConfig } = this.props.tourState;
this.tipTargets = this.getTipTargets();
this.updateTarget( this.state.currentStep );
this.updateTarget( stepConfig );
}

shouldComponentUpdate( nextProps, nextState ) {
if ( this.state.currentStep === nextState.currentStep &&
this.state.target === nextState.target ) {
return false;
}
return true;
shouldComponentUpdate( nextProps ) {
return this.props.tourState !== nextProps.tourState;
}

componentWillUpdate( nextProps, nextState ) {
this.updateTarget( nextState.currentStep );
componentWillUpdate( nextProps ) {
const { stepConfig } = nextProps.tourState;
this.updateTarget( stepConfig );
}

updateTarget( step ) {
Expand All @@ -58,17 +57,19 @@ export default class GuidesTours extends Component {
}

next() {
this.setState( { currentStep: guideConfig[ this.state.currentStep.next ] } );
const nextStepName = this.props.tourState.stepConfig.next;
this.props.nextGuidesTourStep( nextStepName );
}

quit() {
//TODO: should we dispatch a showGuidesTour action here instead?
this.currentTarget && this.currentTarget.classList.remove( 'guidestours__overlay' );
this.setState( { currentStep: null } );
this.props.nextGuidesTourStep( null );
}

render() {
if ( ! this.state.currentStep ) {
const { stepConfig } = this.props.tourState;

if ( ! stepConfig ) {
return null;
}

Expand All @@ -77,17 +78,24 @@ export default class GuidesTours extends Component {
GuidesActionStep,
GuidesLinkStep,
GuidesFinishStep,
}[ this.state.currentStep.type ] || GuidesBasicStep;
}[ stepConfig.type ] || GuidesBasicStep;

return (
<div>
<div className="guidestours">
<StepComponent
{ ...this.state.currentStep }
key={ this.state.target }
{ ...stepConfig }
key={ stepConfig.target }
target={ this.currentTarget }
onNext={ this.next }
onQuit={ this.quit } />
</div>
);
}
}

export default connect( ( state ) => ( {
selectedSite: getSelectedSite( state ),
tourState: getGuidesTourState( state ),
} ), {
nextGuidesTourStep,
} )( GuidesTours );
2 changes: 2 additions & 0 deletions client/state/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ export const SERVER_DESERIALIZE = 'SERVER_DESERIALIZE';
export const SET_EXPORT_POST_TYPE = 'SET_EXPORT_POST_TYPE';
export const SET_ROUTE = 'SET_ROUTE';
export const SET_SECTION = 'SET_SECTION';
// TODO(mcsf): CHANGE WORD ORDER OF THESE TWO:
export const SHOW_GUIDESTOUR = 'SHOW_GUIDESTOUR';
export const UPDATE_GUIDESTOUR = 'UPDATE_GUIDESTOUR';
export const SITE_MEDIA_STORAGE_RECEIVE = 'SITE_MEDIA_STORAGE_RECEIVE';
export const SITE_MEDIA_STORAGE_REQUEST = 'SITE_MEDIA_STORAGE_REQUEST';
export const SITE_MEDIA_STORAGE_REQUEST_SUCCESS = 'SITE_MEDIA_STORAGE_REQUEST_SUCCESS';
Expand Down
8 changes: 8 additions & 0 deletions client/state/ui/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
SELECTED_SITE_SET,
SET_SECTION,
SHOW_GUIDESTOUR,
UPDATE_GUIDESTOUR,
} from 'state/action-types';

/**
Expand Down Expand Up @@ -57,3 +58,10 @@ export function showGuidesTour( { shouldShow = false, tour = 'main', siteId = nu
siteId,
}
}

export function nextGuidesTourStep( stepName ) {
return {
type: UPDATE_GUIDESTOUR,
stepName,
};
}
7 changes: 6 additions & 1 deletion client/state/ui/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import { combineReducers } from 'redux';
import omit from 'lodash/omit';

/**
* Internal dependencies
Expand All @@ -12,6 +13,7 @@ import {
SERIALIZE,
DESERIALIZE,
SHOW_GUIDESTOUR,
UPDATE_GUIDESTOUR,
} from 'state/action-types';
import editor from './editor/reducer';
import reader from './reader/reducer';
Expand Down Expand Up @@ -80,12 +82,15 @@ export function isLoading( state = false, action ) {
export function guidesTour( state = {}, action ) {
switch ( action.type ) {
case SHOW_GUIDESTOUR:
const { stepName = 'init' } = action;
return {
stepName,
shouldShow: action.shouldShow,
tour: action.tour,
stepName: action.stepName,
siteId: action.siteId,
};
case UPDATE_GUIDESTOUR:
return Object.assign( {}, state, omit( action, 'type' ) );
}
return state;
}
Expand Down
23 changes: 23 additions & 0 deletions client/state/ui/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import get from 'lodash/get';
/**
* Internal dependencies
*/
import createSelector from 'lib/create-selector';
import { getSite } from 'state/sites/selectors';
import guidesToursConfig from 'guidestours/config';

/**
* Returns the site object for the currently selected site.
Expand Down Expand Up @@ -44,3 +46,24 @@ export function getSelectedSiteId( state ) {
export function getSectionName( state ) {
return get( state.ui.section, 'name', null );
}

/**
* Returns the current state for Guided Tours.
*
* This includes the raw state from state/ui/guidesTour, but also the available
* configuration (`stepConfig`) for the currently active tour step, if one is
* active.
*
* @param {Object} state Global state tree
* @return {Object} Current Guided Tours state
*/
const getRawGuidesTourState = state => get( state, 'ui.guidesTour', false );
export const getGuidesTourState = createSelector(
state => {
const tourState = getRawGuidesTourState( state );
const { stepName = '' } = tourState;
const stepConfig = guidesToursConfig[ stepName ] || false;
return Object.assign( {}, tourState, { stepConfig } );
},
getRawGuidesTourState
);

0 comments on commit d77518b

Please sign in to comment.