-
Notifications
You must be signed in to change notification settings - Fork 18
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
[WIP] Move routing here #21
Changes from all commits
94b0104
8a4b554
5698993
e1c70aa
9b6f5e2
3b23de8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
{ "presets": ["react", "es2015"] } | ||
{ | ||
"presets": ["react", "es2015"], | ||
"plugins": ["transform-class-properties"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,12 @@ | ||
import React from 'react'; | ||
|
||
import FormApp from 'us-forms-system/lib/js/containers/FormApp'; | ||
import FormApp from './FormApp'; | ||
import formConfig from '../config/form'; | ||
|
||
export default function Form({ location, children }) { | ||
return ( | ||
<FormApp formConfig={formConfig} currentLocation={location}> | ||
{children} | ||
{children} | ||
</FormApp> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import React from 'react'; | ||
import { connect } from 'react-redux'; | ||
import Scroll from 'react-scroll'; | ||
|
||
import FormNav from 'us-forms-system/lib/js/components/FormNav'; | ||
import FormTitle from 'us-forms-system/lib/js/components/FormTitle'; | ||
import { isInProgress } from 'us-forms-system/lib/js/helpers'; | ||
// import { setGlobalScroll } from 'us-forms-system/lib/js/utilities/ui'; | ||
|
||
const Element = Scroll.Element; | ||
|
||
/* | ||
* Primary component for a schema generated form app. | ||
*/ | ||
class FormApp extends React.Component { | ||
componentWillMount() { | ||
// setGlobalScroll(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still need to figure out where the global scroll should be set. My inclination is within the starter app, but there are many lower level components in USFS that need knowledge of a global scroll. In general I think this is a bad pattern, but it's what we have now. We should come up with a better solution for setting scroll in components that need it. |
||
|
||
// if (window.History) { | ||
// window.History.scrollRestoration = 'manual'; | ||
// } | ||
} | ||
|
||
render() { | ||
const { currentLocation, formConfig, children, formData } = this.props; | ||
console.log('FormApp'); | ||
console.log(formData); | ||
const trimmedPathname = currentLocation.pathname.replace(/\/$/, ''); | ||
const isIntroductionPage = trimmedPathname.endsWith('introduction'); | ||
const Footer = formConfig.footerContent; | ||
|
||
let formTitle; | ||
let formNav; | ||
let renderedChildren = children; | ||
if (!isIntroductionPage) { | ||
// Show title only if we're not on the intro page and if there is a title | ||
// specified in the form config | ||
if (formConfig.title) { | ||
formTitle = <FormTitle title={formConfig.title} subTitle={formConfig.subTitle}/>; | ||
} | ||
} | ||
|
||
// Show nav only if we're not on the intro, form-saved, error, or confirmation page | ||
// Also add form classes only if on an actual form page | ||
if (isInProgress(trimmedPathname)) { | ||
formNav = <FormNav formData={formData} formConfig={formConfig} currentPath={trimmedPathname}/>; | ||
|
||
renderedChildren = ( | ||
<div className="progress-box progress-box-schemaform"> | ||
{children} | ||
</div> | ||
); | ||
} | ||
|
||
let footer; | ||
if (Footer) { | ||
footer = ( | ||
<Footer | ||
formConfig={formConfig} | ||
currentLocation={currentLocation}/> | ||
); | ||
} | ||
|
||
return ( | ||
<div> | ||
<div className="row"> | ||
<div className="usa-width-two-thirds medium-8 columns"> | ||
<Element name="topScrollElement"/> | ||
{formTitle} | ||
{formNav} | ||
{renderedChildren} | ||
</div> | ||
</div> | ||
{footer} | ||
<span className="js-test-location hidden" data-location={trimmedPathname} hidden></span> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
const mapStateToProps = (state) => ({ | ||
formData: state.form.data | ||
}); | ||
|
||
export default connect(mapStateToProps)(FormApp); | ||
|
||
export { FormApp }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import PropTypes from 'prop-types'; | ||
import React from 'react'; | ||
import { connect } from 'react-redux'; | ||
import { withRouter } from 'react-router'; | ||
import Scroll from 'react-scroll'; | ||
import _ from 'lodash/fp'; | ||
import classNames from 'classnames'; | ||
|
||
import FormPage from 'us-forms-system/lib/js/containers/FormPage'; | ||
import ProgressButton from 'us-forms-system/lib/js/components/ProgressButton'; | ||
// import SchemaForm from 'us-forms-system/lib/js/components/SchemaForm'; | ||
// import { setData, uploadFile } from '../actions'; | ||
import { getNextPagePath, getPreviousPagePath } from 'us-forms-system/lib/js/routing'; | ||
import { focusElement } from 'us-forms-system/lib/js/utilities/ui'; | ||
|
||
function focusForm() { | ||
focusElement('.nav-header'); | ||
} | ||
|
||
const scroller = Scroll.scroller; | ||
const scrollToTop = () => { | ||
scroller.scrollTo('topScrollElement', window.Forms.scroll || { | ||
duration: 500, | ||
delay: 0, | ||
smooth: true, | ||
}); | ||
}; | ||
|
||
class PageWithNavigation extends React.Component { | ||
componentDidMount() { | ||
if (!this.props.blockScrollOnMount) { | ||
scrollToTop(); | ||
focusForm(); | ||
} | ||
} | ||
|
||
componentDidUpdate(prevProps) { | ||
if (prevProps.route.pageConfig.pageKey !== this.props.route.pageConfig.pageKey || | ||
_.get('params.index', prevProps) !== _.get('params.index', this.props)) { | ||
scrollToTop(); | ||
focusForm(); | ||
} | ||
} | ||
|
||
getUpdatedFormData = (formData) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the new hook for getting the updated |
||
return formData; | ||
} | ||
|
||
onSubmit = () => { | ||
const { params, route, location } = this.props; | ||
const formData = this.getUpdatedFormData(); | ||
// This makes sure defaulted data on a page with no changes is saved | ||
// Probably safe to do this for regular pages, too, but it hasn’t been necessary | ||
// if (route.pageConfig.showPagePerItem) { | ||
// const newData = _.set([route.pageConfig.arrayPath, params.index], formData, form.data); | ||
// this.props.setData(newData); | ||
// } | ||
|
||
const path = getNextPagePath(route.pageList, formData, location.pathname); | ||
|
||
this.props.router.push(path); | ||
} | ||
|
||
goBack = () => { | ||
const { route: { pageList }, location } = this.props; | ||
const formData = this.getUpdatedFormData(); | ||
|
||
const path = getPreviousPagePath(pageList, formData, location.pathname); | ||
|
||
this.props.router.push(path); | ||
} | ||
|
||
render() { | ||
const { | ||
route, | ||
params, | ||
// form, | ||
contentAfterButtons, | ||
formContext | ||
} = this.props; | ||
|
||
// let { | ||
// schema, | ||
// uiSchema | ||
// } = form.pages[route.pageConfig.pageKey]; | ||
|
||
const pageClasses = classNames('form-panel', route.pageConfig.pageClass); | ||
// let data = form.data; | ||
|
||
// if (route.pageConfig.showPagePerItem) { | ||
// // Instead of passing through the schema/uiSchema to SchemaForm, the | ||
// // current item schema for the array at arrayPath is pulled out of the page state and passed | ||
// schema = schema.properties[route.pageConfig.arrayPath].items[params.index]; | ||
// // Similarly, the items uiSchema and the data for just that particular item are passed | ||
// uiSchema = uiSchema[route.pageConfig.arrayPath].items; | ||
// // And the data should be for just the item in the array | ||
// data = _.get([route.pageConfig.arrayPath, params.index], data); | ||
// } | ||
// It should be "safe" to check that this is the first page because it is | ||
// always eligible and enabled, no need to call getPreviousPagePath. | ||
const isFirstRoutePage = route.pageList[0].path === this.props.location.pathname; | ||
|
||
return ( | ||
<div className={pageClasses}> | ||
<FormPage | ||
route={route} | ||
location={this.props.location} | ||
onSubmit={this.onSubmit} | ||
onPageChange={this.getUpdatedFormData}> | ||
</FormPage> | ||
<div className="row form-progress-buttons schemaform-buttons"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One of the problems we have in regards to navigation buttons comes from a component called |
||
<div className="small-6 medium-5 columns"> | ||
{!isFirstRoutePage && | ||
<ProgressButton | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These buttons aren't actually firing anything when clicked. Need to figure out why. |
||
onButtonClick={this.goBack} | ||
buttonText="Back" | ||
buttonClass="usa-button-secondary" | ||
beforeText="«"/> } | ||
</div> | ||
<div className="small-6 medium-5 end columns"> | ||
<ProgressButton | ||
submitButton | ||
buttonText="Continue" | ||
buttonClass="usa-button-primary" | ||
afterText="»"/> | ||
</div> | ||
{contentAfterButtons} | ||
</div> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
// function mapStateToProps(state) { | ||
// return { | ||
// form: state.form, | ||
// user: state.user | ||
// }; | ||
// } | ||
|
||
// const mapDispatchToProps = { | ||
// setData, | ||
// uploadFile | ||
// }; | ||
|
||
PageWithNavigation.propTypes = { | ||
// form: PropTypes.object.isRequired, | ||
route: PropTypes.shape({ | ||
pageConfig: PropTypes.shape({ | ||
pageKey: PropTypes.string.isRequired, | ||
schema: PropTypes.object.isRequired, | ||
uiSchema: PropTypes.object.isRequired | ||
}), | ||
pageList: PropTypes.arrayOf(PropTypes.shape({ | ||
path: PropTypes.string.isRequired | ||
})) | ||
}), | ||
contentAfterButtons: PropTypes.element, | ||
// setData: PropTypes.func | ||
}; | ||
|
||
export default withRouter(PageWithNavigation); | ||
|
||
export { PageWithNavigation }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,10 +18,16 @@ const formConfig = { | |
firstPage: { | ||
path: 'first-chapter/first-page', | ||
title: 'First Page', | ||
uiSchema: {}, | ||
uiSchema: { | ||
|
||
}, | ||
schema: { | ||
type: 'object', | ||
properties: {} | ||
properties: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This stuff is added for testing purposes, I'll revert these changes once the PR is working. |
||
name: { | ||
type: 'string' | ||
} | ||
} | ||
} | ||
}, | ||
secondPage: { | ||
|
@@ -30,7 +36,11 @@ const formConfig = { | |
uiSchema: {}, | ||
schema: { | ||
type: 'object', | ||
properties: {} | ||
properties: { | ||
favoriteAnimal: { | ||
type: 'string' | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,71 @@ | ||
import { createRoutes } from 'us-forms-system/lib/js/helpers'; | ||
import React from 'react'; | ||
// import { createRoutes } from 'us-forms-system/lib/js/helpers'; | ||
import FormPage from 'us-forms-system/lib/js/containers/FormPage'; | ||
import ReviewPage from 'us-forms-system/lib/js/review/ReviewPage'; | ||
import { createFormPageList, createPageList } from 'us-forms-system/lib/js/helpers'; | ||
|
||
import formConfig from './config/form'; | ||
import Form from './components/Form.jsx'; | ||
import PageWithNavigation from './components/PageWithNavigation'; | ||
|
||
const routes = createRoutes(formConfig); | ||
// const routes = createRoutes(formConfig); | ||
|
||
/* | ||
* Create the routes based on a form config. This goes through each chapter in a form | ||
* config, pulls out the config for each page, then generates a list of Route components with the | ||
* config as props | ||
*/ | ||
function createRoutes(formConfig) { | ||
const formPages = createFormPageList(formConfig); | ||
const pageList = createPageList(formConfig, formPages); | ||
let routes = formPages | ||
.map(page => { | ||
return { | ||
path: page.path, | ||
component: page.component || PageWithNavigation, | ||
pageConfig: page, | ||
pageList, | ||
urlPrefix: formConfig.urlPrefix | ||
}; | ||
}); | ||
if (formConfig.introduction) { | ||
routes = [ | ||
{ | ||
path: 'introduction', | ||
component: formConfig.introduction, | ||
formConfig, | ||
pageList | ||
} | ||
].concat(routes); | ||
} | ||
|
||
return routes.concat([ | ||
{ | ||
path: 'review-and-submit', | ||
formConfig, | ||
component: ReviewPage, | ||
pageList | ||
}, | ||
{ | ||
path: 'confirmation', | ||
component: formConfig.confirmation | ||
}, | ||
{ | ||
path: '*', | ||
onEnter: (nextState, replace) => replace(formConfig.urlPrefix || '/') | ||
} | ||
]); | ||
} | ||
|
||
const formConfigRoutes = createRoutes(formConfig); | ||
|
||
const route = { | ||
path: '/', | ||
component: Form, | ||
indexRoute: { | ||
onEnter: (nextState, replace) => replace(formConfig.urlPrefix+routes[0].path) | ||
onEnter: (nextState, replace) => replace(formConfig.urlPrefix+formConfigRoutes[0].path) | ||
}, | ||
childRoutes: routes, | ||
childRoutes: formConfigRoutes, | ||
}; | ||
|
||
export default route; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The motivation for moving the
<FormApp/>
component from USFS into the starter app is that with pulling out routing we are also going to pull out navigation elements, footer, header, nav, basically anything that wraps around a section of questions and has opinions about what a form page should look like. We think that should be the responsibility of the starter app so that users can decide their own navigation elements or headers and footers if needed.