Skip to content
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

Code review for upcoming Wagtail integration #1

Open
wants to merge 81 commits into
base: review-1-target
Choose a base branch
from

Conversation

thibaudcolas
Copy link
Owner

@thibaudcolas thibaudcolas commented Jan 18, 2019

This PR was created to help review the code of https://github.com/noripyt/react-streamfield before integration into Wagtail (wagtail/wagtail#4942). As of now, it covers v0.6.10 of react-streamfield.

For the high-level summary, see wagtail-deprecated#4.

@@ -0,0 +1,2 @@
/node_modules/
/.idea/
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest ignoring dist in git, and only having it in the npm package (via the package.json's files attribute, as of now).

</head>
<body>
<h1>React StreamField demo</h1>
<h2>1 block type</h2>
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those examples would greatly benefit from using Storybook. It's meant for exactly this type of "UI development / playground / examples / demos" scenario, is really easy to set up (by comparison to a custom Webpack + dev server setup), and comes with very nice features built-in:

  • Hot reloading of React components and styles
  • Navigation tree to easily move between examples
  • Plugin ecosystem of development helper panels, say for testing across multiple screen sizes, or i18n testing, or accessibility checks. And many more.

I switched the Draftail examples to it a few weeks ago and it was well worth it. See http://demo.draftail.org/storybook for a demo.

}
],
"value": [{"type": "title", "value": "Wagtail is awesome!"}]
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you take a moment to document this schema somewhere? Up until now it’s been Wagtail’s internals, but now that it's an important part of this package’s API I would find it important to have a high-level outline of the different attributes, their type and purpose, whether they need to be unique or not, which are optional and which aren't.

This doesn't necessarily need to be written docs, it could be as simple as adding comments to BlockDefinitionType and linking to it from the README / copying its code there.

"blockDefinitions": [
{
"key": "title",
"icon": "<i class='fas fa-heading fa-fw'></i>",
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to use SVGs (symbols or inline path definitions) for the icons in the examples, instead of FontAwesome, as we are actively trying to remove icon fonts from Wagtail.

"html": "<input type='checkbox' name='field-__ID__' />"
}
],
"html": "As you can see by this text, it’s possible <strong>to insert some HTML</strong> before or after the contained blocks. <BlocksContainer /> You can even have multiple times the same blocks container. <BlocksContainer /> Can’t think of a case where that would be useful, but still, it’s possible if you really want it."
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure I'll have other occasions to understand this better elsewhere in the code, but <BlocksContainer /> doesn't look like valid HTML and even if it's harmless I'd rather encourage people to only do things that validate as HTML5.

border-color: $error-border-color;
}
}
&:hover, &:focus, &:focus-within {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:focus-within still doesn't have that good browser support. Is this functionality important enough to justify adding a --focus class in JS, and add styles based on the class instead?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s not worth doing, this is just a cosmetic functionality to make ancestors blocks with an light red background & border. And the errored blocks are all force opened, so there’s no way we can miss an error in a block, even without this focus-within.

&:hover, &:focus, &:focus-within {
> .block-container {
border-color: $error-border-color-focus;
> header {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use a class name instead of a bare element name.

border-radius: $border-radius;
background: white;
transition: border-color $hover-transition-duration ease-in-out;
transition-property: border-color, box-shadow;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be clearer as:

transition: border-color $hover-transition-duration ease-in-out, box-shadow $hover-transition-duration ease-in-out;

&.SIMPLE {
display: flex;
flex-flow: row nowrap;
> header {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments as above for bare elements and BEM state syntax, and also for all following styles.

flex: 0 0 auto;
padding: $header-padding;
h3 {
position: sticky;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to only be a minor effect (but nice nonetheless), but I think it would be worth stating in the project's README that a position: sticky polyfill might be desirable, as IE11 doesn't support it.

This should also be added in the Wagtail docs, over at https://docs.wagtail.io/en/v2.4/contributing/developing.html?highlight=Sticky.

@connect((state, props) => {
const {fieldId, parentId, blockId} = props;
let blockDefinitions;
if (parentId === null) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For strings that don't have a reason to be empty, it would be more idiomatic to write the conditional as if (parentId).

event.preventDefault();
event.stopPropagation();
if (this.hasChoice) {
this.setState({open: !this.state.open});
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally it's considered a bad habit to update state based on other state like this, because setState can batch updates and thus the UI can end up in an inconsistent state.

The alternative is to use an updater function, which guarantees the state to be up to date when it runs:

this.setState((state, props) => ({open: !state.open});

See https://reactjs.org/docs/react-component.html#setstate

event.stopPropagation();
this.props.addBlock(this.props.index,
event.target.closest('button').value);
this.toggle(event);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can see toggle already does preventDefault and stopPropagation, so those calls shouldn't be necessary above.

event.preventDefault();
event.stopPropagation();
this.props.addBlock(this.props.index,
event.target.closest('button').value);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IE11 doesn't implement closest. The README should mention that an element-closest polyfill is needed for it. Luckily, Wagtail already has it.

if (isNA(icon)) {
return null;
}
return <span className="icon" dangerouslySetInnerHTML={
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is way too generic of a class name, and is guaranteed to clash with existing or future code. Please use something more specific, or consider namespacing classes (e.g. streamfield-icon).

} = this.props;
return (
<aside>
<div className="actions">
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too generic of a class name.

duplicateBlock: () => duplicateBlock(fieldId, blockId),
}, dispatch);
})
class BlockActions extends (React.Component) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra parens?

{sortableBlock ?
<React.Fragment>
<button onClick={this.moveUpHandler}
title="Move up"
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be localized / provided by Wagtail.

}

triggerCustomEvent(name, data=null) {
triggerCustomEvent(ReactDOM.findDOMNode(this), name, data);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I can see below, the data parameter is never used.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this to keep consistency with the other components defining a triggerCustomEvent method, like BlockHeader.

<Draggable draggableId={id} index={index}
type={`${fieldId}-${parentId}`}>
{(provided, snapshot) => (
<article className={className}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem like the appropriate tag for this scenario.

"bugs": {
"url": "https://github.com/noripyt/react-streamfield/issues"
},
"license": "BSD",
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"BSD" isn't a valid license identifier according to npm (SPDX identifiers only). Looks like it should be BSD-3-Clause to be the same as Wagtail.

<React.Fragment>
{button}
<AnimateHeight height={this.panelHeight} easing="ease-in-out"
contentClassName="add-panel">
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing this, I see that AnimateHeight renders all of its content in order to calculate its height. Do you know if there is a way around this? If there are many AddButton instances on a page, this is a lot of DOM nodes for not much value.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, there is absolutely no way around this. If we want collapse transitions, we have to do this. This is an extremely annoying limitation of CSS3, you can transition between height: 0 and height: auto.

You should not worry about the amount of DOM nodes introduced by AnimateHeight. It consists only of 2 nested divs, and every time I use AnimateHeight I use the child div as a container that I would have done otherwise, by specifying contentClassName.

{Object.entries(this.groupedBlockDefinitions).map(
([group, blockDefinitions]) => (
<div key={group}>
<h4 className="group-name">{group}</h4>
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This h4 should not be rendered if group is an empty string.

<AnimateHeight className="draggable-container"
height={this.draggableHeight}
onAnimationEnd={this.onDraggableContainerAnimationEnd}>
{this.wrapSortable(blockContent)}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as with AddButton – it seems like a big waste on the performance side to render all of these, and have them hidden with styles. For pages with lots of StreamField, this means the browser will come to a crawl even if only some of them are open.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As stated above, this is not a problem. Besides, I tested with and without AnimateHeight on the whole package, and it doesn’t affect performance in a noticable way.

Maybe you tested in the development mode of React, where things are indeed a little slow.
Test with this build, you shouldn’t see performance issues even though it contains 198 nested blocks.

</button>
<button onClick={this.moveDownHandler}
title="Move down"
className={this.isLast ? 'disabled' : null}>
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buttons should have the disabled attribute, rather than this being achieved with CSS.

<AnimateHeight height={this.height} easing="ease-in-out"
className="content-container"
contentClassName={className}>
{content}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern as other AnimateHeight comments above.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concern answered above.

}
}
this.runInnerScripts();
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a potential source of memory leaks, due to the event listener (and the mutation observer)? in bindChange. There should be a corresponding componentWillUnmount that removes the event listeners, and disconnects the mutation observers.

<DragDropContext onDragEnd={this.onDragEnd}>
<BlocksContainer fieldId={id} />
<input type="hidden" name={id}
value={JSON.stringify(generatedValue)} />
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this strange that StreamField takes the responsibility for storing its data like this, and patches over Wagtail's storage in its componentWillMount. I think it would be better if the component exposed an onChange callback (or similar) in its props, so Wagtail can take charge of storing the data.

@thibaudcolas thibaudcolas changed the title react-streamfield code review for upcoming Wagtail integration Code review for upcoming Wagtail integration Jan 21, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants