From 7364882a03a245902145c6a51c89fea30b3eb01b Mon Sep 17 00:00:00 2001 From: James Calfee <james@jcalfee.info> Date: Fri, 19 Aug 2016 10:32:44 -0500 Subject: [PATCH] Remove redux from from Medium Editor. --- app/components/cards/CategorySelector.jsx | 2 +- app/components/elements/MediumEditor.jsx | 149 +++++++++++----------- 2 files changed, 76 insertions(+), 75 deletions(-) diff --git a/app/components/cards/CategorySelector.jsx b/app/components/cards/CategorySelector.jsx index bbfc0f5a3..967acd079 100644 --- a/app/components/cards/CategorySelector.jsx +++ b/app/components/cards/CategorySelector.jsx @@ -10,7 +10,7 @@ class CategorySelector extends React.Component { autoComplete: React.PropTypes.string, placeholder: React.PropTypes.string, onChange: React.PropTypes.func.isRequired, - onBlur: React.PropTypes.func.isRequired, + onBlur: React.PropTypes.func, isEdit: React.PropTypes.bool, disabled: React.PropTypes.bool, value: React.PropTypes.string, diff --git a/app/components/elements/MediumEditor.jsx b/app/components/elements/MediumEditor.jsx index 88eae1a29..5a4539a44 100644 --- a/app/components/elements/MediumEditor.jsx +++ b/app/components/elements/MediumEditor.jsx @@ -1,5 +1,6 @@ +/* eslint react/prop-types: 0 */ import React from 'react'; -import {reduxForm} from 'redux-form' +import reactForm from 'app/utils/ReactForm' import transaction from 'app/redux/Transaction'; import MarkdownViewer from 'app/components/cards/MarkdownViewer' import CategorySelector from 'app/components/cards/CategorySelector' @@ -12,7 +13,6 @@ import sanitize from 'sanitize-html' import HtmlReady from 'shared/HtmlReady' import g from 'app/redux/GlobalReducer' import {Map, Set} from 'immutable' -import {cleanReduxInput} from 'app/utils/ReduxForms' let RichTextEditor if(process.env.BROWSER) { @@ -48,23 +48,6 @@ class MediumEditor extends React.Component { title: React.PropTypes.string, // initial value body: React.PropTypes.string, // initial value - //redux connect - reply: React.PropTypes.func.isRequired, - setMetaLink: React.PropTypes.func.isRequired, - clearMetaData: React.PropTypes.func.isRequired, - setMetaData: React.PropTypes.func.isRequired, - metaLinkData: React.PropTypes.object, - state: React.PropTypes.object.isRequired, - hasCategory: React.PropTypes.bool.isRequired, - isStory: React.PropTypes.bool.isRequired, - username: React.PropTypes.string, - - // redux-form - fields: React.PropTypes.object.isRequired, - handleSubmit: React.PropTypes.func.isRequired, - resetForm: React.PropTypes.func.isRequired, - submitting: React.PropTypes.bool.isRequired, - invalid: React.PropTypes.bool.isRequired, } static defaultProps = { @@ -76,21 +59,23 @@ class MediumEditor extends React.Component { metaLinkData: Map(), } - constructor() { + constructor(props) { super() this.state = { rteRef: Date.now() } + this.initForm(props) this.shouldComponentUpdate = shouldComponentUpdate(this, 'MediumEditor') this.onTitleChange = e => { const value = e.target.value // TODO block links in title (the do not make good permlinks) const hasMarkdown = /(?:\*[\w\s]*\*|\#[\w\s]*\#|_[\w\s]*_|~[\w\s]*~|\]\s*\(|\]\s*\[)/.test(value) this.setState({ titleWarn: hasMarkdown ? 'Markdown is not supported here' : '' }) - this.props.fields.title.onChange(e) + this.state.title.props.onChange(e) } this.onCancel = e => { if(e) e.preventDefault() - const {onCancel, resetForm} = this.props - resetForm() + const {onCancel} = this.props + const {mediumForm} = this.state + mediumForm.clearForm() this.setAutoVote() this.setState({rte_value: RichTextEditor ? '' : null, rteRef: Date.now()}) if(onCancel) onCancel(e) @@ -110,10 +95,10 @@ class MediumEditor extends React.Component { } } this.autoVoteOnChange = () => { - const {autoVote} = this.props.fields + const {autoVote} = this.state const key = 'replyEditorData-autoVote-story' localStorage.setItem(key, !autoVote.value) - autoVote.onChange(!autoVote.value) + autoVote.props.onChange(!autoVote.value) } } componentWillMount() { @@ -125,20 +110,20 @@ class MediumEditor extends React.Component { if(editorData) { editorData = JSON.parse(editorData) if(editorData.formId === formId) { - const {fields: {category, title, body}} = this.props - if(category) category.onChange(editorData.category) - if(title) title.onChange(editorData.title) + const {category, title, body} = this.state + if(category) category.props.onChange(editorData.category) + if(title) title.props.onChange(editorData.title) if (editorData.body) { const html = getHtml(editorData.body) if(html) { rte_value = editorData.body rte = true } else - body.onChange(editorData.body) + body.props.onChange(editorData.body) } } } else { - const {body} = this.props.fields + const {body} = this.state // const {isStory} = this.props // if(isStory) rte = true //JSON.parse(localStorage.getItem('replyEditorData-rte') || RTE_DEFAULT); @@ -165,21 +150,21 @@ class MediumEditor extends React.Component { }, 300) } componentWillUpdate(nextProps, nextState) { - const {fields: {body}} = nextProps - const tp = this.props.fields - const np = nextProps.fields + const ts = this.state + const ns = nextState if( - tp.body.value !== np.body.value || - this.state.rte_value !== nextState.rte_value || - (np.category && tp.category.value !== np.category.value) || - (np.title && tp.title.value !== np.title.value) + ts.body.value !== ns.body.value || + ts.rte_value !== ns.rte_value || + (ns.category && ts.category.value !== ns.category.value) || + (ns.title && ts.title.value !== ns.title.value) ) { // also prevents saving after parent deletes this information - const {fields: {category, title}, formId} = nextProps + const {formId} = nextProps + const {category, title} = ns + const {rte, rte_value} = ns const data = {formId} - const {rte, rte_value} = nextState data.title = title ? title.value : undefined data.category = category ? category.value : undefined - data.body = rte ? rte_value : body.value + data.body = rte ? rte_value : ns.body.value clearTimeout(saveEditorTimeout) saveEditorTimeout = setTimeout(() => { // console.log('save formId', formId) @@ -191,33 +176,57 @@ class MediumEditor extends React.Component { const {clearMetaData, formId} = this.props clearMetaData(formId) } + + initForm(props) { + const {isStory, type, hasCategory, fields} = props + const isEdit = type === 'edit' + const maxKb = isStory ? MAX_STORY_KB : MAX_COMMENT_KB + reactForm({ + fields, + instance: this, + name: 'mediumForm', + initialValues: props.initialValues, + validation: values => ({ + title: isStory && ( + !values.title || values.title.trim() === '' ? 'Required' : + values.title.length > 255 ? 'Shorten title' : + null + ), + category: hasCategory && validateCategory(values.category, !isEdit), + body: isBodyEmpty(this.state, values.body) ? 'Required' : + values.body.replace(/data:image\/.+?("|quot)/g, '"').length > maxKb * 1024 ? 'Exceeds maximum length ('+maxKb+'KB)' : + null + }) + }) + } + onChange(value, rte_serialize) { // Serilize can be expensive.. Only use it for a small body or when submitting the post... let rte_value if(value === '') { rte_value = EMPTY_MEDIUM_HTML - this.props.fields.body.onChange('') + this.state.body.props.onChange('') } else if(value.length < 1000) { // Allow valid tags which have no body but can show something. Sanitize will strip out all other html but leave text which indicates the user is looking at something... const ser = sanitize(value, {allowedTags: ['img', 'iframe']}).trim() if(ser === '' || ser === '+'/*insert plugin*/) { rte_value = EMPTY_MEDIUM_HTML - this.props.fields.body.onChange('') + this.state.body.props.onChange('') } } if(!rte_value) { rte_value = `<html>${value}</html>` - this.props.fields.body.onChange(rte_value) + this.state.body.props.onChange(rte_value) } this.setState({rte_value, rte_serialize}) } setAutoVote() { const {isStory} = this.props if(isStory) { - const {autoVote} = this.props.fields + const {autoVote} = this.state const key = 'replyEditorData-autoVote-story' const autoVoteDefault = JSON.parse(localStorage.getItem(key) || true) - autoVote.onChange(autoVoteDefault) + autoVote.props.onChange(autoVoteDefault) } } toggleRte(e) { @@ -234,13 +243,15 @@ class MediumEditor extends React.Component { body: this.props.body, } const {onCancel, autoVoteOnChange} = this - const {title, category, body, autoVote} = this.props.fields + const {title, category, body, autoVote} = this.state const { reply, username, hasCategory, isStory, formId, noImage, author, permlink, parent_author, parent_permlink, type, jsonMetadata, metaLinkData, - state, successCallback, handleSubmit, submitting, invalid, //lastComment, + state, successCallback, } = this.props - const {postError, loading, titleWarn, rte, rte_serialize} = this.state + const {submitting, valid, handleSubmit} = this.state.mediumForm + const {postError, titleWarn, rte, rte_serialize} = this.state + const loading = submitting // tmp const {onTitleChange} = this const errorCallback = estr => { this.setState({ postError: estr, loading: false }) } const successCallbackWrapper = (...args) => { @@ -282,7 +293,7 @@ class MediumEditor extends React.Component { > <div className={vframe_section_shrink_class}> {isStory && <span> - <input type="text" {...cleanReduxInput(title)} onChange={onTitleChange} disabled={loading} + <input type="text" {...title.props} onChange={onTitleChange} disabled={loading} placeholder="Title" autoComplete="off" ref="titleRef" tabIndex={1} /> {titleError} </span>} @@ -301,7 +312,7 @@ class MediumEditor extends React.Component { options={EditorOptions} readOnly={loading} /> : - <textarea {...cleanReduxInput(body)} disabled={loading} rows={isStory ? 10 : 3} placeholder={isStory ? 'Write your story...' : 'Reply'} autoComplete="off" ref="postRef" tabIndex={2} /> + <textarea {...body.props} disabled={loading} rows={isStory ? 10 : 3} placeholder={isStory ? 'Write your story...' : 'Reply'} autoComplete="off" ref="postRef" tabIndex={2} /> } </div> <div className={vframe_section_shrink_class}> @@ -310,7 +321,7 @@ class MediumEditor extends React.Component { <div className={vframe_section_shrink_class} style={{marginTop: '0.5rem'}}> {hasCategory && <span> - <CategorySelector {...category} disabled={loading} isEdit={isEdit} tabIndex={3} /> + <CategorySelector {...category.props} disabled={loading} isEdit={isEdit} tabIndex={3} /> <div className="error">{category.touched && category.error && category.error} </div> </span>} </div> @@ -318,16 +329,20 @@ class MediumEditor extends React.Component { {postError && <div className="error">{postError}</div>} </div> <div className={vframe_section_shrink_class}> - {!loading && <button type="submit" className="button" disabled={submitting || invalid} tabIndex={4}>{isEdit ? 'Update Post' : postLabel}</button>} + {!loading && + <button type="submit" className="button" disabled={submitting} + tabIndex={4}>{isEdit ? 'Update Post' : postLabel}</button> + } {loading && <span><br /><LoadingIndicator type="circle" /></span>} {!loading && this.props.onCancel && - <button type="button" className="secondary hollow button no-border" tabIndex={5} onClick={(e) => {e.preventDefault(); onCancel()}}>Cancel</button> + <button type="button" className="secondary hollow button no-border" tabIndex={5} + onClick={(e) => {e.preventDefault(); onCancel()}}>Cancel</button> } {!loading && !this.props.onCancel && <button className="button hollow no-border" tabIndex={5} disabled={submitting} onClick={onCancel}>Clear</button>} {isStory && !isEdit && <div className="float-right"> <small onClick={autoVoteOnChange}>Upvote post</small> - <input type="checkbox" {...cleanReduxInput(autoVote)} onChange={autoVoteOnChange} /> + <input type="checkbox" {...autoVote.props} onChange={autoVoteOnChange} /> </div>} </div> {!loading && !rte && <div className={'Preview ' + vframe_section_shrink_class}> @@ -355,17 +370,14 @@ function isBodyEmpty(state, body) { return false } -export default formId => reduxForm( - // config - {form: formId}, - // https://github.com/erikras/redux-form/issues/949 - // Warning: Failed propType: Required prop `form` was not specified in `ReduxFormConnector(ReplyEditor)`. Check the render method of `ConnectedForm`. +import {connect} from 'react-redux' +export default formId => connect( // mapStateToProps (state, ownProps) => { // const current = state.user.get('current')||Map() const username = state.user.getIn(['current', 'username']) - const fields = ['body', 'autoVote'] + const fields = ['body', 'autoVote:bool'] const {type, parent_author, jsonMetadata} = ownProps const isStory = /submit_story/.test(type) || ( /edit/.test(type) && parent_author === '' @@ -375,19 +387,6 @@ export default formId => reduxForm( fields.push('title') } if (hasCategory) fields.push('category') - const isEdit = type === 'edit' - const maxKb = isStory ? MAX_STORY_KB : MAX_COMMENT_KB - const validate = values => ({ - title: isStory && ( - !values.title || values.title.trim() === '' ? 'Required' : - values.title.length > 255 ? 'Shorten title' : - null - ), - category: hasCategory && validateCategory(values.category, !isEdit), - body: isBodyEmpty(state, values.body) ? 'Required' : - values.body.replace(/data:image\/.+?("|quot)/g, '"').length > maxKb * 1024 ? 'Exceeds maximum length ('+maxKb+'KB)' : - null - }) let {category, title, body} = ownProps if (/submit_/.test(type)) title = body = '' @@ -398,7 +397,7 @@ export default formId => reduxForm( const metaLinkData = state.global.getIn(['metaLinkData', formId]) const ret = { ...ownProps, - fields, validate, isStory, hasCategory, username, + fields, isStory, hasCategory, username, initialValues: {title, body, category}, state, // lastComment: current.get('lastComment'), formId, @@ -444,6 +443,8 @@ export default formId => reduxForm( originalPost.category : formCategories.first() const rootTag = /^[-a-z\d]+$/.test(rootCategory) ? rootCategory : null + // if(rte_serialize) console.log('body', body) + // if(rte_serialize) console.log('body ser', rte_serialize()) if(rte_serialize) body = rte_serialize() const rtags = HtmlReady(body, {mutate: false}) -- GitLab