-
Notifications
You must be signed in to change notification settings - Fork 28
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
start reduxing activity state #426
Changes from all commits
0e1e192
67b503f
d445cea
2305a7a
98f20d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export const ADD_ACTIVITY = 'ADD_ACTIVITY'; | ||
export const UPDATE_ACTIVITY = 'UPDATE_ACTIVITY'; | ||
|
||
export const addActivity = () => ({ type: ADD_ACTIVITY }); | ||
export const updateActivity = data => ({ type: UPDATE_ACTIVITY, data }); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import PropTypes from 'prop-types'; | ||
import React from 'react'; | ||
import { connect } from 'react-redux'; | ||
|
||
import ActivityDetailAll from './ActivityDetailAll'; | ||
import ActivityListEntry from './ActivityListEntry'; | ||
import { addActivity as addActivityAction } from '../actions/activities'; | ||
import Collapsible from '../components/Collapsible'; | ||
import Container from '../components/Container'; | ||
import Section from '../components/Section'; | ||
import SectionTitle from '../components/SectionTitle'; | ||
|
||
const Activities = ({ activityIds, addActivity }) => ( | ||
<Container> | ||
<Section> | ||
<SectionTitle>Program Activities</SectionTitle> | ||
<Collapsible title="Activity List" open> | ||
<div className="mb2"> | ||
{activityIds.map((aId, idx) => ( | ||
<ActivityListEntry key={aId} aId={aId} num={idx + 1} /> | ||
))} | ||
</div> | ||
<button | ||
type="button" | ||
className="mt2 btn btn-primary" | ||
onClick={addActivity} | ||
> | ||
Add activity | ||
</button> | ||
</Collapsible> | ||
{activityIds.map((aId, idx) => ( | ||
<ActivityDetailAll key={aId} aId={aId} num={idx + 1} /> | ||
))} | ||
</Section> | ||
</Container> | ||
); | ||
|
||
Activities.propTypes = { | ||
activityIds: PropTypes.array.isRequired, | ||
addActivity: PropTypes.func.isRequired | ||
}; | ||
|
||
const mapStateToProps = ({ activities }) => ({ | ||
activityIds: activities.allIds | ||
}); | ||
|
||
const mapDispatchToProps = { | ||
addActivity: addActivityAction | ||
}; | ||
|
||
export default connect(mapStateToProps, mapDispatchToProps)(Activities); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import PropTypes from 'prop-types'; | ||
import React from 'react'; | ||
import { connect } from 'react-redux'; | ||
|
||
import ActivityDetailDescription from './ActivityDetailDescription'; | ||
import ActivityDetailGoals from './ActivityDetailGoals'; | ||
import Collapsible from '../components/Collapsible'; | ||
|
||
const activityTitle = (a, i) => { | ||
let title = `Activity ${i}`; | ||
if (a.name) title += `: ${a.name}`; | ||
if (a.types.length) title += ` (${a.types.join(', ')})`; | ||
return title; | ||
}; | ||
|
||
const ActivityDetailAll = ({ aId, title }) => ( | ||
<Collapsible title={title} bgColor="darken-1" open> | ||
<ActivityDetailDescription aId={aId} /> | ||
<ActivityDetailGoals aId={aId} /> | ||
</Collapsible> | ||
); | ||
|
||
ActivityDetailAll.propTypes = { | ||
aId: PropTypes.number.isRequired, | ||
title: PropTypes.string.isRequired | ||
}; | ||
|
||
const mapStateToProps = ({ activities: { byId } }, { aId, num }) => { | ||
const activity = byId[aId]; | ||
const title = `Program Activities › ${activityTitle(activity, num)}`; | ||
|
||
return { title }; | ||
}; | ||
|
||
export default connect(mapStateToProps)(ActivityDetailAll); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import PropTypes from 'prop-types'; | ||
import { EditorState, ContentState, convertToRaw } from 'draft-js'; | ||
import draftToHtml from 'draftjs-to-html'; | ||
import htmlToDraft from 'html-to-draftjs'; | ||
import React, { Component } from 'react'; | ||
import { Editor } from 'react-draft-wysiwyg'; | ||
import { connect } from 'react-redux'; | ||
|
||
import { updateActivity as updateActivityAction } from '../actions/activities'; | ||
import Collapsible from '../components/Collapsible'; | ||
import Icon, { faHelp } from '../components/Icons'; | ||
import { EDITOR_CONFIG } from '../util'; | ||
|
||
const htmlToEditor = html => { | ||
if (!html) return EditorState.createEmpty(); | ||
|
||
const { contentBlocks, entityMap } = htmlToDraft(html); | ||
const content = ContentState.createFromBlockArray(contentBlocks, entityMap); | ||
return EditorState.createWithContent(content); | ||
}; | ||
|
||
const editorToHtml = editorState => { | ||
const content = convertToRaw(editorState.getCurrentContent()); | ||
return draftToHtml(content); | ||
}; | ||
|
||
class ActivityDetailDescription extends Component { | ||
constructor(props) { | ||
super(props); | ||
|
||
const { descLong, altApproach } = props.activity; | ||
|
||
this.state = { | ||
descLong: htmlToEditor(descLong), | ||
altApproach: htmlToEditor(altApproach) | ||
}; | ||
} | ||
|
||
onEditorChange = name => editorState => { | ||
this.setState({ [name]: editorState }); | ||
}; | ||
|
||
syncEditorState = name => () => { | ||
const html = editorToHtml(this.state[name]); | ||
const { activity, updateActivity } = this.props; | ||
const data = { id: activity.id, name, value: html }; | ||
|
||
updateActivity(data); | ||
}; | ||
|
||
render() { | ||
const { activity, updateActivity } = this.props; | ||
const { descLong, altApproach } = this.state; | ||
|
||
return ( | ||
<Collapsible title="Activity Description" open> | ||
<div className="mb1 bold"> | ||
Summary | ||
<Icon icon={faHelp} className="ml-tiny teal" size="sm" /> | ||
</div> | ||
<div className="mb3"> | ||
<textarea | ||
className="m0 textarea" | ||
rows="5" | ||
maxLength="280" | ||
spellCheck="true" | ||
value={activity.descShort} | ||
onChange={e => | ||
updateActivity({ | ||
id: activity.id, | ||
name: 'descShort', | ||
value: e.target.value | ||
}) | ||
} | ||
/> | ||
</div> | ||
|
||
<div className="mb3"> | ||
<div className="mb1 bold"> | ||
Please describe the activity in detail | ||
<Icon icon={faHelp} className="ml-tiny teal" size="sm" /> | ||
</div> | ||
<Editor | ||
toolbar={EDITOR_CONFIG} | ||
editorState={descLong} | ||
onEditorStateChange={this.onEditorChange('descLong')} | ||
onBlur={this.syncEditorState('descLong')} | ||
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. To make sure I'm clear on this: as the editor changes, update the component state so the editor stays editable; when the editor blurs, grab the HTML and sync that to redux state? 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. exactly! |
||
/> | ||
</div> | ||
|
||
<div className="mb3"> | ||
<div className="mb1 bold"> | ||
Statement of alternative considerations and supporting justification | ||
</div> | ||
<Editor | ||
toolbar={EDITOR_CONFIG} | ||
editorState={altApproach} | ||
onEditorStateChange={this.onEditorChange('altApproach')} | ||
onBlur={this.syncEditorState('altApproach')} | ||
/> | ||
</div> | ||
</Collapsible> | ||
); | ||
} | ||
} | ||
|
||
ActivityDetailDescription.propTypes = { | ||
activity: PropTypes.object.isRequired, | ||
updateActivity: PropTypes.func.isRequired | ||
}; | ||
|
||
const mapStateToProps = ({ activities: { byId } }, { aId }) => ({ | ||
activity: byId[aId] | ||
}); | ||
|
||
const mapDispatchToProps = { | ||
updateActivity: updateActivityAction | ||
}; | ||
|
||
export default connect(mapStateToProps, mapDispatchToProps)( | ||
ActivityDetailDescription | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import React from 'react'; | ||
import { connect } from 'react-redux'; | ||
|
||
import Collapsible from '../components/Collapsible'; | ||
|
||
const ActivityDetailDescription = () => ( | ||
<Collapsible title="Needs and Objectives"> | ||
<div>...</div> | ||
</Collapsible> | ||
); | ||
|
||
const mapStateToProps = ({ activities: { byId } }, { aId }) => ({ | ||
activity: byId[aId] | ||
}); | ||
|
||
export default connect(mapStateToProps)(ActivityDetailDescription); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import PropTypes from 'prop-types'; | ||
import React, { Component } from 'react'; | ||
import { connect } from 'react-redux'; | ||
|
||
import { updateActivity as updateActivityAction } from '../actions/activities'; | ||
|
||
const ACTIVITY_TYPES = ['HIT', 'HIE', 'MMIS']; | ||
|
||
class ActivityListEntry extends Component { | ||
handleTypes = id => e => { | ||
const { value } = e.target; | ||
const { activity, updateActivity } = this.props; | ||
const { types } = activity; | ||
|
||
const newValue = types.includes(value) | ||
? types.filter(t => t !== value) | ||
: [...types, value].sort(); | ||
|
||
const data = { id, name: 'types', value: newValue }; | ||
|
||
updateActivity(data); | ||
}; | ||
|
||
handleName = id => e => { | ||
const { value } = e.target; | ||
const { updateActivity } = this.props; | ||
const name = 'name'; | ||
const data = { id, name, value }; | ||
|
||
updateActivity(data); | ||
}; | ||
|
||
render() { | ||
const { activity, num } = this.props; | ||
const { id } = activity; | ||
|
||
return ( | ||
<div className="flex items-center mb1"> | ||
<div className="mr1 bold mono">{num}.</div> | ||
<div className="mr1 col-4"> | ||
<input | ||
type="text" | ||
className="col-12 input m0" | ||
value={activity.name} | ||
onChange={this.handleName(id)} | ||
/> | ||
</div> | ||
<div> | ||
{ACTIVITY_TYPES.map(option => ( | ||
<label key={option} className="mr1"> | ||
<input | ||
type="checkbox" | ||
value={option} | ||
checked={activity.types.includes(option)} | ||
onChange={this.handleTypes(id)} | ||
/> | ||
{option} | ||
</label> | ||
))} | ||
</div> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
ActivityListEntry.propTypes = { | ||
activity: PropTypes.object.isRequired, | ||
num: PropTypes.number.isRequired, | ||
updateActivity: PropTypes.func.isRequired | ||
}; | ||
|
||
const mapStateToProps = ({ activities: { byId } }, props) => ({ | ||
activity: byId[props.aId] | ||
}); | ||
|
||
const mapDispatchToProps = { | ||
updateActivity: updateActivityAction | ||
}; | ||
|
||
export default connect(mapStateToProps, mapDispatchToProps)(ActivityListEntry); |
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.
I'm curious why you get the activity from redux store by ID rather than taking it as a param?
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.
yeah, i wasn't really sold on this either; i read that it was better performance to pass down the id and look up the data object via connect, let me track down the article
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.
reduxjs/redux#1751
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.
Fascinating. Really gets into the weeds of how React handles re-rendering as state changes.