-
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
Element / Block API: Add RawHTML component, drop support for HTML string block save #4786
Conversation
Some additional tasks I'd like to do here:
|
blocks/hooks/deprecated.js
Outdated
return <DangerousHTMLWithWarning children={ element } />; | ||
} | ||
|
||
addFilter( 'blocks.getSaveContent.saveElement', 'core/deprecated/save-props', shimDangerousHTML ); |
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 like how you handled this backward compatibility issue with the hook ❤️
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 like how you handled this backward compatibility issue with the hook ❤️
The backwards-compatibility error also had the side benefit of triggering some failing tests which both hinted toward (a) flawed tests and (b) that maybe we want to support some string returns to continue to work unaffected, but not those including raw HTML. This has been improved in the rebased 687af56.
b310d0e
to
ed02d56
Compare
79061cb
to
e80fb68
Compare
blocks/api/serializer.js
Outdated
} | ||
|
||
saveElement = applyFilters( 'blocks.getSaveContent.saveElement', saveElement, blockType, 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.
Unrelated to this PR. In the future, we should also make sure that functional components are treated as React components, too:
saveElement = save( { 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.
We are about to introduce new hook blocks.getSaveContent.saveElement
. Is it meant to exist only until the saveElement
is being able to return a string? If the answer is yes, then we might want to move all the deprecation logic here instead of using a hook.
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.
Unrelated to this PR. In the future, we should also make sure that functional components are treated as React components, too:
Related: #3745 (comment)
It is potentially related to this pull request, as we've simplified all paths which necessitated treating the function component any different than the component class. I think at one point in putting this branch together I'd tried to convert to createElement
, but ran into challenges. I'll try to revisit and see if it can be done easily.
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 tried the similar in the past but ended with splitting into two methods getSaveContent
and getSaveElement
only :(
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 are about to introduce new hook
blocks.getSaveContent.saveElement
. Is it meant to exist only until thesaveElement
is being able to return a string? If the answer is yes, then we might want to move all the deprecation logic here instead of using a hook.
I don't have a problem with it sticking around. Related to #3800, it can serve as useful in cases like this one where we want to override the entire behavior of save, not just inject some additional props.
Of course, this opens questions of when and why we add filters. Also feel that we should be proactive about documenting them (expecting that, as with PHP, we'd want hook documentation pages.
blocks/hooks/deprecated.js
Outdated
return element; | ||
} | ||
|
||
addFilter( 'blocks.getSaveContent.saveElement', 'core/deprecated/save-props', shimDangerousHTML ); |
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.
'core/deprecated/save-props'
-> 'core/deprecated/save-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.
'core/deprecated/save-props'
->'core/deprecated/save-element'
?
Aside: I still don't like that we name filters 😄 The number of times I've edited this line just to change the name serves as one reason.
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, this namespace thing (2nd param) seems obsolete at the moment.
blocks/hooks/deprecated.js
Outdated
// Still support string return from save, but in the same way any component | ||
// could render a string, it should be escaped. Therefore, only shim usage | ||
// which had included some HTML expected to be unescaped. | ||
if ( typeof element === 'string' && includes( 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.
includes( element, '<' )
- this behavior might lead to some errors. I will comment next to the code which might cause issues.
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.
includes( element, '<' )
- this behavior might lead to some errors. I will comment next to the code which might cause issues.
Which comment expanded on the issue 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.
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.
Talked with @gziolo about this some more. Basically: We want to support string returns from save
, but in the same way that string is a valid React element, it should be unescaped. The shim here specifically targets HTML which previous block implementations may have been returning. The one case where this won't work well is if a block had returned a shortcode string, which will now be escaped. We could similarly add support for shortcode patterns, or alternatively just allow them to break (it's unclear to me how common this would have been).
blocks/api/serializer.js
Outdated
@@ -45,13 +45,10 @@ export function getSaveElement( blockType, attributes ) { | |||
saveElement = createElement( save, { attributes } ); | |||
} else { | |||
saveElement = save( { attributes } ); | |||
|
|||
// Special-case function render implementation to allow raw HTML return | |||
if ( 'string' === typeof saveElement ) { |
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 noted in here https://github.com/WordPress/gutenberg/pull/4786/files#r165907203, we are slightly changing behavior with this PR. When the saveElement
is going to be represented as a plain string without any HTML tags, this function won't return the value immediately. It will be wrapped with Children.map
, but addExtraContainerProps
won't apply filters because it will return immediately after check if an object was passed. In the case where the saveElement
would contain HTML tags it is going to be recognized as an object by the inner function, therefore all filters will be applied. This might introduce some confusion as the same block can have a different markup depending on the 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.
Please also note that there is another function in this file:
export function getSaveContent( blockType, attributes ) {
which performs check against the return value from getSaveElement
. It won't return string
anymore after this refactoring is applied so we can remove the check in lines 78-81. Children.map
makes sure that return value of the function is always object.
We might also want to explore in another PR the possibility of turning getSaveElement
in SaveBlock
component as suggested in #3800 by @andreiglingeanu.
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 the case where the
saveElement
would contain HTML tags it is going to be recognized as an object by the inner function, therefore all filters will be applied.
If I understand correctly: This is different, yes, but arguably more consistent. Example of this being how the Freeform block could get away with avoiding supports.className: false
previously only because it returned a string
from save
and so it was previously skipped in the generated class name hook. While in the case of freeform we don't want the generated class name, it seems to me wise to reduce the number of variations in how save results are handled.
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 looks good and is almost ready to merge. I have one concern where we need to clarify what is the expected behavior because there might be an unexpected change in the output HTML depending on the content of RichText
component. I also left some comment where I would like to discuss further refactorings.
e80fb68
to
402d4a3
Compare
I've pushed a rebased branch which should address most all of the feedback. I've pushed for more refactoring of the I have some stashed changes which pushed even to deprecate cc @youknowriad I know you'd mentioned on at least one occasion removing support for |
* | ||
* @type {Object} | ||
* @typedef {WPBlockType} |
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 just use flow or typescript
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, that would even more useful :)
|
||
// Component classes are unsupported for save since serialization must | ||
// occur synchronously. For improved interoperability with higher-order | ||
// components which often return component class, emulate basic support. |
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 idea 👍
@@ -129,7 +117,7 @@ describe( 'block serializer', () => { | |||
{ fruit: 'Bananas' } | |||
); | |||
|
|||
expect( saved ).toBe( '<div>Bananas</div>' ); |
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 know why the className was missing 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.
Do you know why the className was missing here?
Yep, this was a tricky one. Alluded to in #4786 (comment) with the "fixed test case" mention, the issue was specifically in how createElement
is treated differently from calling save
as a function directly. It's the same reason why we couldn't just refactor to createElement
:
...which is that, when cloning the element produced by createElement
, it's the same as modifying props of what is passed into save
, not what is returned by save
. The filtering expects to be applied on whatever is produced by save
, e.g. the div
here.
We don't really have a way to hook into the render behavior without some awful machinery to render
into a temporary DOM, so instead we drop support for save
as a component and just treat it as a function (i.e. it's still not a "stateless function component", just a function returning an element(s)).
@@ -8,7 +8,7 @@ Array [ | |||
style="display:none" | |||
/>, | |||
<div | |||
class="wp-block-freeform blocks-rich-text__tinymce" |
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.
Wondering why this change, you didn't touch the edit
function right?
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.
Wondering why this change, you didn't touch the
edit
function right?
Because I added the supports.className: false
, it's no longer adding the generated class in BlockEdit
:
gutenberg/blocks/block-edit/index.js
Lines 28 to 32 in 0cbe106
// Generate a class name for the block's editable form | |
const generatedClassName = hasBlockSupport( blockType, 'className', true ) ? | |
getBlockDefaultClassname( name ) : | |
null; | |
const className = classnames( generatedClassName, attributes.className ); |
...though this makes me think that this is a bit hard to follow, and that we might want to consolidate class name handling into blocks/hooks/generated-class-name.js
, particularly since BlockEdit
is already filterable 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.
Oh right I was confused it with customClassName
* @param {Object} attributes Block attributes. | ||
*/ | ||
const props = applyFilters( | ||
'blocks.getSaveContent.extraProps', |
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 rename to blocks.getSaveElement.extraProps
to match with another filter? Can be another PR.
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 rename to
blocks.getSaveElement.extraProps
to match with another filter? Can be another PR.
I think we should, yes, but it is a breaking change, one which we can deprecate with a hook, but probably best left for a separate pull request.
element/index.js
Outdated
let rendered = renderToStaticMarkup( element ); | ||
|
||
// Drop raw HTML wrappers (support dangerous inner HTML without wrapper) | ||
rendered = rendered.replace( /<\/?wp-dangerous-html>/g, '' ); |
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.
Nit: this function can return here instead of updating variable's 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.
Nit: this function can return here instead of updating variable's value.
I'm going to leave this as-is, as it distinguishes / sets a pattern for transformations which would be applied to the default rendered output.
blocks/library/html/index.js
Outdated
@@ -70,6 +71,6 @@ export const settings = { | |||
] ), | |||
|
|||
save( { attributes } ) { | |||
return attributes.content; | |||
return <DangerousHTML>{ attributes.content }</DangerousHTML>; |
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 this component should be called RawHTML
, it doesn't make a lot of sense to call it dangerous in the context of an editor that deals with raw html in valid and regular cases.
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.
Works for me.
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, one potential downside is it's basically a substitute for dangerouslySetInnerHTML
(sans wrapper), not just for serialization, so it could turn into a bit of a foot-gun.
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.
Code looks good, there are still some minor comments which might need some code updates, but everything looks good. I did a quite long testing session and I didn't discover any regressions when using updated blocks: Classic, Custom HTML, Shortcodes. 👍
Explicit support by element static string renderer.
Unused anyways, misleading to imply it is.
402d4a3
to
aca8f72
Compare
Renamed from Also added deprecated compatibility support for shortcodes via a regular expression which is prone to false positives (better to capture and warn than break any plugin which had expected to return a shortcode text from its |
Related: #3745 (cherry-picks 57978eb)
This pull request seeks to migrate away from string return from block
save
implementation toward a newRawHTML
component, which serves an identical purpose while also providing additional utility for nesting (#3745) and Editable value (#2750). Further, it reduces the number of code paths to maintain and improves consistency of hook behaviors (e.g. hooks injecting props viablocks.getSaveContent.extraProps
are now respected). You can see this demonstrated here in the change introducingsupports.className = false
to the Classic block, as otherwise a wrapper with the auto-generated class name is applied (a bug resolved by these changes).Breaking Change: (Deprecation) All existing block
save
implementations returning raw HTML will continue to work, but a warning will be logged to the browser console recommending theRawHTML
component instead. We may want to seek to remove this deprecated code in the future.Testing instructions:
Ensure unit tests pass:
Verify that there are no regressions in the behavior of blocks expecting to save raw HTML, notably the Classic, HTML, and Shortcode blocks. These blocks should also not render any wrapper elements, except in the case that a custom class name is assigned.