-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Serializer: Adding a block state serializer (blocks => postContent) #348
Conversation
917f369
to
1da371f
Compare
blocks/serializer/index.js
Outdated
* @return {String} The post content | ||
*/ | ||
export default function serialize( blocks ) { | ||
return blocks.reduce( ( memo, block ) => { |
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.
This block is a bit hard to follow. It may be good to separate the attribute handling to separate functions, and possibly add some comments.
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, the difficulty here is to figure out which attributes needs to be serialized to the starting comment and which should be ignored (because they're already in the static content).
So what I'm doing is running the attributes
object (or function) defined on each block just to know which "keys" (attributes) are extracted from the content.
blocks/serializer/index.js
Outdated
const blockType = block.blockType; | ||
const blockSettings = wp.blocks.getBlockSettings( blockType ); | ||
const rawContent = wp.element.renderToString( | ||
blockSettings.save( block.attributes ) |
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 save
looks very odd out of context to me, it seems to imply an action instead of returning a value for the generated html.
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 we could rename it serialize
and return the string directly in the function.
To avoid having to call renderToString
and to avoid any extra React attributes. @aduth may disagree 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.
The initial direction was decided primarily from the perspective of how it's used in implementing a block, since the duality between edit
and save
, both with a consistent return type, seemed the most direct way to communicate the intent for the implementer to create an incoming (editor) and outgoing (to save) description of the UI.
Alternative names to consider:
prepareForEdit
,prepareForSave
(not entirely accurate since it's not preparing, it's returning the element describing the UI)renderEdit
,renderSave
Honestly it doesn't really bother me that it looks awkward in the one internal snippet of code where we're actually invoking it. One change I'd like to make which could improve this is to treat save
and edit
as components, not mere functions, like started in #335:
https://github.com/WordPress/gutenberg/pull/335/files#diff-1b2cbb4a0a02cacc1be65d67ece68c21
return the string directly in the function.
If we upgrade to react@next
(16 w/ Fiber), strings are valid return values of a component's (function's) render, so we could support string as an option without making any distinction.
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 trying to be pragmatic here, we have several issues if we return an element (or a component) and I would like to move forward with this.
I think this resolves several issues:
- Imagine using a component that requires lifecycle hooks, the rendering would be async.
- How can we drop the
data-react
attributes easily - We can not return multiple tags, A text block can have multiple paragraphs
- Currently the
content
parsed by the text element returns all the markup<p>test</p>
, if we add a container element, we would change the markup even if we do not perform any change<p><p>test</p></p>
.
The string return value resolves all these without tradeoffs IMO
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 output
make sense?
I added some comments to explain the serializer algorithm. Splitting the attributes serialization to its own method would have required duplicating some logic (computing the blockSettings and the rawContent) or passing those as parameters which creates a function with a weird API. I preferred keeping everything in the same function for now but with more comments. |
Several things on the last commit:
The most controversial change here is renaming I think this resolves several issues:
For a reason I can not find right now, using |
blocks/README.md
Outdated
return RandomImage( { category: attributes.category } ); | ||
serialize: function( attributes ) { | ||
const src = 'http://lorempixel.com/400/200/' + props.category; | ||
return `<img src="${ src }" alt="${ props.category }"/>`; |
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 can't help but this documentation only goes to highlight why this is an idea we should avoid:
- What if
category
has a"
in it? - Why can't we re-use
RandomImage
to generate the same markup? - What if my markup is more than just a single HTML element, like a gallery with potentially hundreds of elements?
- What benefit does this ever have that even if your answer to any of the above is "just call
renderToString
", I as the developer must even come to know thatrenderToString
is a thing?
element/index.js
Outdated
* @param {wp.Element} element Element to render | ||
* @return {String} HTML | ||
*/ | ||
export function renderToString( element ) { |
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 should use react-dom/server
's renderToStaticMarkup
which, unlike renderToString
, includes no checksum or reactid
.
blocks/serializer.js
Outdated
const blockSettings = getBlockSettings( blockType ); | ||
|
||
// static content, to be rendered inside the block comment | ||
const rawContent = blockSettings.serialize( block.attributes ); |
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.
Can we be certain blockSettings
is not undefined
here? What about blocks which had been added previously from a plugin that since has been deactivated?
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.
Good question. I assume the block parser will already fallback to the default block in this case. HTML
block.
blocks/serializer.js
Outdated
// We take all the block attributes and exclude the block attributes computed | ||
// using the `attributes` from the Block Settings. | ||
let contentAttributes = {}; | ||
if ( 'function' === typeof blockSettings.attributes ) { |
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.
Should we abstract this since it's now both here and in parser
's getBlockAttributes
? (Or rather, should we just move getBlockAttributes
somewhere more generic?).
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 need to abstract getBlockContentAttributes
and not the entire getBlockAttributes
blocks/serializer.js
Outdated
} else if ( blockSettings.attributes ) { | ||
contentAttributes = query.parse( rawContent, blockSettings.attributes ); | ||
} | ||
const commentAttributes = Object.keys( block.attributes ).reduce( ( attrs, attribute ) => { |
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.
A Lodash _.difference
on keys would be an easier way to find where there's differences between block state and its defined attributes
😉
blocks/serializer.js
Outdated
}, [] ); | ||
|
||
// serialize the comment attributes | ||
const serializedCommentAttributes = ! commentAttributes.length |
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.
Do you need this ternary at all? Seems like it would work just as well with:
const serializedCommentAttributes = commentAttributes.map( ( { key, value } ) => `${key}:${value}` ).join( ' ' ) + ' ';
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 just wanted to avoid the extra space at the end when there's no comment attribute
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 just wanted to avoid the extra space at the end when there's no comment attribute
There should be no extra space. The result of the map
and join
would still be an empty string if commentAttributes
is empty.
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 was talking about this one + ' '
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.
Oh, I see now. I liked how this was handled transparently in the multi-instance prototype:
Here I suspect that would look like:
const serializedCommentAttributes = commentAttributes.map( ( { key, value } ) => `${ key }:${ value } ` ).join( '' );
Or:
const serializedCommentAttributes = commentAttributes.reduce( ( memo, { key, value } ) => memo + `${ key }:${ value } `, '' );
* @return {String} The post content | ||
*/ | ||
export default function serialize( blocks ) { | ||
return blocks.reduce( ( memo, block ) => { |
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.
Do you think there's a few functions we could split out of this long block perhaps for easier testing?
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.
As I noted above, it's hard to do without duplicating code (like computing rawContent and blockSettings several times) or passing those as parameters (which creates weird API)
@aduth I'm repeating and editing my answer here to ease discussion :) I'm trying to be pragmatic here, we have several issues if we return an element (or a component) and I would like to move forward with this. I think this resolves several issues:
|
In the context of server-rendering, of which this is comparable, there's no concept of asynchronous rendering and we never need to set the expectation that this would be possible to support. |
React 16 will support array return values from functions. We should upgrade to the alpha. |
Then we should spend our effort trying to find ways to avoid |
I'd claim the opposite, that we should expect significant overlap between UI of the editor and what's saved. |
I give up :) you have good points obviously (and I do too :P), but for the moment I can't be convinced. I feel like we're using components to generate markup and components are way more than markup for me. |
Yeah, I think my opinions differ on this point in my distaste for the Component class and lifecycle, and a preference to view React as a simple input/output mechanism, for which the original APIs work quite well. |
cb90ea8
to
40de95e
Compare
blocks/serializer.js
Outdated
}, [] ); | ||
|
||
// serialize the comment attributes | ||
const serializedCommentAttributes = ! commentAttributes.length |
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.
Oh, I see now. I liked how this was handled transparently in the multi-instance prototype:
Here I suspect that would look like:
const serializedCommentAttributes = commentAttributes.map( ( { key, value } ) => `${ key }:${ value } ` ).join( '' );
Or:
const serializedCommentAttributes = commentAttributes.reduce( ( memo, { key, value } ) => memo + `${ key }:${ value } `, '' );
blocks/serializer.js
Outdated
// serialize the comment attributes | ||
const serializedCommentAttributes = ! commentAttributes.length | ||
? '' | ||
: commentAttributes.map( ( { key, value } ) => `${key}:${value}` ).join( ' ' ) + ' '; |
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 we should enable: http://eslint.org/docs/rules/template-curly-spacing (always
)
This seems consistent with our preferred spacing.
editor/editor/layout.js
Outdated
@@ -17,8 +17,7 @@ class Layout extends wp.element.Component { | |||
} | |||
|
|||
switchMode( newMode ) { | |||
// TODO: we need a serializer from blocks here | |||
const html = this.state.html; | |||
const html = this.mode === 'text' ? this.state.html : wp.blocks.serialize( this.state.blocks ); |
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.
Shouldn't this.mode
on this and the following line be this.state.mode
?
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.
Also, can we not store html
and blocks
in state and instead perform this logic in `render?
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 performing this logic here for performance reasons. Only parse/serialize when switching.
And good catch for the this.mode
error
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 since-removed "props in getInitialState as anti-pattern" article spoke to this specific usage of state as a cache to often be a poor choice for the impact on maintainability caused by duplicating the source of truth:
Using props to generate state in getInitialState often leads to duplication of "source of truth", i.e. where the real data is. This is because getInitialState is only invoked when the component is first created.
Whenever possible, compute values on-the-fly to ensure that they don't get out of sync later on and cause maintenance trouble.
In our specific case, it also seems to assume that we don't have a good sense of when render
will be called. As-is, we should expect it only to be called on initialization and on mode changes, which is where we're regenerating html
and blocks
state anyways.
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.
Good point 👍
blocks/test/serializer.js
Outdated
* Internal dependencies | ||
*/ | ||
import { default as serialize } from '../serializer'; | ||
import * as blocks from '../registration'; |
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.
In #355 I tried to avoid treating registration
as some canonical set of blocks properties (since wp.blocks
encompasses more than just registration). I don't know how you feel about this. It's a small point.
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.
It's better 👍
blocks/test/serializer.js
Outdated
|
||
describe( 'block serializer', () => { | ||
// Reset block state before each test. | ||
beforeEach( () => { |
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.
In #335 I changed cleanup to afterEach
. I like beforeEach
in the general case within a single isolated test suite, but our current setup is not very isolated with singleton blocks, to the effect that one test suite can potentially affect the other. Changing to afterEach
was an attempt to ensure that each test will not leave any lingering effects to the next.
9575f6f
to
5502412
Compare
I've rebased and updated this pull request to account for state introduced in #368. A major shift from #368 to better accommodate serialization is that HTML is no longer considered the source of truth, but rather the blocks themselves. In displaying the text editor, we assign the default value as the serialized form of the current blocks state. This simplifies state a fair bit. I also added support for string return values from |
Textarea shown as serialization of current blocks state
Avoids parsing on every keystroke in text editor, rather only when existing field
In this PR, I'm creating a
serializer
to transform a block list to a raw post content string. The serialization is performed while switching between visual and text mode (and will be used for saving the post content)With this PR, I'm hoping I can convince we should switch to just return a string from the
save
function. cc @aduth