-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Editor: Reduxify The EditorTitle component #8814
Conversation
isNew, | ||
post, | ||
site, | ||
ownProps |
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.
why are we adding ownProps
here? it will come in as this.props.ownProps
- is that the intention? also, did you know that ownProps
automatically gets merged into the props through connect
even if we never use them?
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.
Yes I know, the idea is that ownProps needs to be passed down the textarea. The previous way of doing this was to rely on ownProps automatically merged to the props and using omit to exclude the current component props.
And when using localize and connect etc... we get extra props that we don't want to pass down to the textarea (we'll get warnings). And it's hard to keep those ignored props up to date with this method. So, for now, localize adds three props (translate, moment and another one). Imagine someone adding another prop to localize, he won't think of fixing these kind of issues (adding the new Prop to omit). That's why I used this technique.
I could also replace the ...ownProps with explicit props passing (we use only tabIndex for now I think), but I see the component as an upgraded textarea, so It's "logic" to just pass all parent props down.
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 sorry @youknowriad but I don't think I understand what you are saying here. what it appears like you are saying doesn't fit with what I think the code is saying.
what is the problem with localize()
and why does it break things? also, one of my chief questions here is that we are forcing the component/children to change their logic here because if they formerly depended on this.props.value
they will now have to call out to this.props.ownProps.value
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.
Yes, you are right! my answer was not so clear. Let me explain more. the PostEditor
component renders the EditorTitle
Component like this : <EditorTitle tabIndex="1" />
and the EditorTitle
Component renders a textarea close to this : <Textarea {...props />
.
The issue here, is to find the best way to compute the props that needs to be provided to the Textarea
. In the example here, these props are { tabIndex: '1' }
. We need to find the best way to find these exact props without passing any extra prop provided by the HoC components used on the EditorTitle
.
The options are :
1- const props = omit( this.props, Object.keys( this.constructor.propTypes ) )
and make sure we define all the propTypes provided by HoC like localize even If we don't use them like the moment
prop. Also If the localize HoC get updated and provides an extra prop, the EditorTitle
component will have to be updated to "omit" the new prop.
2- using ownProps
since we know these are the only props provided to the EditorTitle
container and not computed by any HoC.
3- avoid using { ...props }
and use only something like tabIndex={this.props.tabIndex}
. This approach is fine but we change the behavior of the EditorTitle
component since we can not provide extra props (for example style={someStyle} or whatever) to the textarea
I hope this is clearer.
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 think I understand what you are saying now @youknowriad. it seems like an indication that bigger issues exist here.
we have plenty of places where we pass ...props
down through several levels of nesting and that is almost always really confusing. I'm a huge advocate for explicit prop passing. props passed by localize shouldn't be passed along the chain either because if a child needs translate
then it should be directly wrapped by localize
concerning passing other props I wonder if it's not more of a concern than it seems. why should we want to pass some new style to an underlying textarea
inside a component? that actually breaks our React idioms, that parents should need to know nothing of the inner workings of their children. perhaps being explicit about a component's public API and keeping the nested children isolated from the grandparents can provide more robustness and reliability here.
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 ok with using explicit props. I think it's a good solution. My concern about it was that It changes the way this component works (and it was not the purpose of this PR, just a refactor).
From my understanding, this component was considered like a "decorator" of the textarea and this won't be the case anymore with explicit props but I'm ok with it.
I'll update the PR to reflect that :)
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.
we shouldn't be changing the behavior, but wouldn't this.props.ownProps
also change it?
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.
No, it does not change it. If we compare the props passed down to the textarea before the refacto and after (with ownProps), they'll remain exactly the same.
@dmsnell I update the RP with explicit props. It's probably better. We'll just pass down new props when needed and avoid the hassle of figuring out the right props to pass down. |
Any other thoughts about this @dmsnell tks :) |
Sorry for missing this @youknowriad - I think it's probably ready to go. I haven't tested it so that's the catch. Please merge at your leisure or what on someone else to review if you prefer. Again, sorry - I'm a little swamped at the moment. |
Don't worry @dmsnell I totally understand, we can all be overwhelmed :) I'll test it deeply again and merge this up. Tks |
@youknowriad Does this account for empty content checking that exists in the Flux implementation? Will explain shortly in follow-up, just wanted to catch before merge. |
Specifically, see #5445 which is a similar refactoring that accounts for post having content. A post cannot be saved unless it has one of these fields not being empty: title, content, excerpt. The editor Publish and Save buttons should be disabled until one of these conditions is met. That is currently managed via |
Steps to reproduce:
Expected: I can publish |
Oh! I missed that, thanks @aduth I updated the PR by using the same behavior we had for the post dirty checking. Basically, to check if the post has content, we use the Thoughts? |
* @param {Number} postId Post ID | ||
* @return {Boolean} Whether the edited post has content or not | ||
*/ | ||
export const editedPostHasContent = createSelector( |
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.
Could this selector be simplified to:
const editedPost = getEditedPost( siteId, postId );
return (
some( [ 'title', 'excerpt' ], ( field ) => !! editedPost[ field ].trim() ) ||
! isPostContentEmpty( editedPost.content )
);
Notably leveraging getEditedPost
instead of recreating that behavior.
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.
Tks @aduth I was not aware getEditedPost
was handling the merge thing 👍
If this is a memorized selector, as via Post content changes but code is requesting the same site/post ids, memorized selector thusly returns previous value of |
If I understand correctly the behavior of |
At least, if it's not the case, then we should update the example on the README there |
is it the case then that we are "flushing" the memorized cache any time any post changes or is added/removed? is the memorized selector even helping us that much? did we have performance issues with this title that led us to forego the simplicity of a straightforward selector? |
@dmsnell to be honest, I have not experienced specific performance any issue but I used this approach to mimic what has been done for So yes, I'm ok to drop the memoization unless we experience performance issues. |
Yes, this is true. The members of the second argument of
The most benefit comes from those selectors used often and perform heavy logic. Since Arguments from the "de facto" solution reselect: https://github.com/reactjs/reselect#motivation-for-memoized-selectors The hope was for |
from the code of the memorized selector @aduth it looked like it might not save us any function calls. it looked like this one was simply checking if a string is empty while the memorized selector also makes several function calls |
There's more going on in the selector than just that, but sure, there's a balance to be had between the logic of the memoize function and the selector itself. |
* @return {Boolean} true if the site's permalinks are editable | ||
*/ | ||
export function areSitePermalinksEditable( state, siteId ) { | ||
const site = getRawSite( state, siteId ); |
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.
Would be good to leverage getSiteOption
here I think.
ff8b073
to
b90fe74
Compare
Rebased and fixed the last notes :) |
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.
Observed strange behavior that may not be specifically related to these changes but more apparent.
- Start new post
- Enter title
- Before post saves, open drafts drawer and select another post
- Accept warning that changes will be lost
- Observe draft loads correctly
- Start new post
Expected: Fresh post
Actual: Title from step 2 is included
* @param {Number} postId Post ID | ||
* @return {Boolean} Whether the edited post has content or not | ||
*/ | ||
export function editedPostHasContent( state, siteId, postId ) { |
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.
Ideally the has
(or is
, or get
) would come first in the selector name as an indicator of the type of value it returns... but I'm struggling to come up with a better name 😄
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 tend to agree, but here it's hard to follow this rule and have something meaningful. Naming is hard 😅
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 support putting has
in the middle. reads much easier for A has B
functions
* Drop sitesList usage * Drop PostEditStore calls
…ost check * This allows us to have the same behavior as dirty checking while still moving to redux. Basically we check for content presence on Redux and on EditPostStore.
b90fe74
to
dd97399
Compare
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.
Looked over code and took it for another spin, all appears to work well 👍
I'd much rather the new selectors (and maybe even updated selectors) be moved to state/selectors
, but not opposed to merging as-is.
return ( | ||
!! editedPost && | ||
( | ||
some( [ 'title', 'excerpt' ], ( field ) => editedPost[ field ] && !! editedPost[ field ].trim() ) || |
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.
No need to change, but I like how Lodash methods handle the falsey case for you gracefully. In this case it could be simplified to:
some( [ 'title', 'expect' ], ( field ) => trim( editedPost[ field ] ) )
!! editedPost && | ||
( | ||
some( [ 'title', 'excerpt' ], ( field ) => editedPost[ field ] && !! editedPost[ field ].trim() ) || | ||
/* |
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.
Nitpick: Per guidelines, multi-line comments which are not docblocks should use //
(reference)
* @param {string} content Raw post content | ||
* @return {Boolean} Whether it's considered empty | ||
*/ | ||
export function isEmptyContent( content ) { |
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.
Curious how we might handle these types of utility functions once selectors are all moved to the state/selectors
directory. I personally don't really like them all that much, but seem an unfortunate necessity in some cases. Here, since isEmptyContent
is probably only needed by the editedPostHasContent
selector, I might imagine it could live alongside the selector in its state/selectors/edited-post-has-content.js
file (exported only for purposes of testing, but not to be used elsewhere).
return false; | ||
} | ||
|
||
return /\/\%postname\%\/?/.test( permalinkStructure ); |
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.
Guessing this is partly legacy, but I don't think we need to escape %
in the RegExp.
@aduth Valuable notes above, I'm moving |
expect( require( '../' + kebabCase( key ) ) ).to.equal( selector ); | ||
const module = require( '../' + kebabCase( key ) ); | ||
const defaultExport = module.default ? module.default : module; | ||
expect( defaultExport ).to.equal( selector ); |
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.
@aduth I had to update this test to allow global selectors to export "utils" functions for test purpose.
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.
@aduth I had to update this test to allow global selectors to export "utils" functions for test purpose.
Makes sense 👍 If/when we drop babel-plugin-add-module-exports
(npm) this can be simplified to:
expect( require( '../' + kebabCase( key ) ).default ).to.equal( selector );
Still tests well for me after latest changes 👍 |
Testing instructions
cc @aduth