-
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
Editable: Treat the value prop as a string #2786
Conversation
1927123
to
ca06fc6
Compare
Codecov Report
@@ Coverage Diff @@
## master #2786 +/- ##
==========================================
- Coverage 33.77% 33.16% -0.61%
==========================================
Files 191 191
Lines 5691 5681 -10
Branches 996 983 -13
==========================================
- Hits 1922 1884 -38
- Misses 3189 3225 +36
+ Partials 580 572 -8
Continue to review full report at Codecov.
|
@@ -95,9 +102,11 @@ registerBlockType( 'core/pullquote', { | |||
|
|||
return ( | |||
<blockquote className={ `align${ align }` }> | |||
{ value && value.map( ( paragraph, i ) => <p key={ i }>{ paragraph.props.children }</p> ) } | |||
{ value && value.map( ( paragraph, i ) => | |||
<Editable.Value tagName="p" key={ i }>{ paragraph }</Editable.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.
With Editable.Value
, do you think we can get rid of this map now as well and automatically key?
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 also make more sense as in edit
the Editable
covers the array, but here in save
Editable.Value
only covers an item in that array.
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.
notice the comment above // Need a better matcher joining the values
, we can't do this right now because we can't write a matcher extracting all the p
elements into a single string.
We used to be able to use .join
when declaring the source
but this is not possible since the source is declarative only. Maybe worth adding a div
wrapper, it makes sense to me or come up with a concat
matcher: value: 'concat( 'p', prop( 'outerHTML' ) )
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.
Sorry didn't see the comment
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, it's not that obvious that it's related. Anyway, the comment helps to surface 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.
Do you get the sense that in most of the cases we'd want a joined set of HTML, we'd be better served by supporting block nesting instead (i.e. should we bother with a matcher for this at all?).
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 get the sense that in most of the cases we'd want a joined set of HTML, we'd be better served by supporting block nesting instead (i.e. should we bother with a matcher for this at all?).
I don't think so, I think would have the same issue even with nested blocks, the hard part is that we're not able to target (or filter) nodes where we miss some structure (no wrapper around the similar p
)
function nodeListToString( nodeList ) { | ||
const container = document.createElement( 'div' ); | ||
nodeList.forEach( ( node ) => container.appendChild( node ) ); | ||
return container.innerHTML; |
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.
Wonders if Array.from( beforeFragment.childNodes ).map( node => node.innerHTML ).join( '' )
could work.
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 guess so, with outerHTML
thought. but i think the text nodes don't have the outerHTML
property, maybe better keeping this way for consistency. I don't know.
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.
Yep, you're 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.
Fragments... 🙄
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.
From the perspective of the ideal API to integrate with save
, I'm not thrilled by tagName
. I think eventually we'd want to avoid that, or Editable.Value
altogether, which means creating our own renderToString
implementation.
@@ -652,3 +633,9 @@ export default class Editable extends Component { | |||
Editable.contextTypes = { | |||
onUndo: noop, | |||
}; | |||
|
|||
Editable.Value = function( { tagName: Tag = 'div', children, ...props } ) { |
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 a bit unfortunate we need to surface tagName
as a prop, but it makes sense given that we need to assign dangerouslySetInnerHTML
as a prop to the element, and can't otherwise return just the raw children. Hmmm!
My ideal:
<p><Editable.Value>{ content }</Editable.Value></p>
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't otherwise return just the raw children. [...]
We can't? This worked: https://github.com/WordPress/gutenberg/compare/try/editable-state-tree#diff-c2353ca7ccb0e91040faf47baa55d644R175
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 works if it's an array structure but not when it's an HTML string
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.
Ah, right, you need an element to use the dangerous attribute.
blocks/editable/index.js
Outdated
} | ||
|
||
getContent() { | ||
return nodeListToReact( this.editor.getBody().childNodes || [], createTinyMCEElement ); | ||
// TODO: Improve performance using childNodes | ||
return this.editor.getContent(); |
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 my head, I've been considering a combination of cloneNode
and various DOM querying / manipulation. Something like:
const body = this.editor.getBody().cloneNode( true );
const boguses = body.querySelectorAll( '[data-mce-bogus]' );
for ( var i = 0; i < boguses.length; i++ ) {
const bogus = boguses[ i ];
if ( 'all' !== bogus.getAttribute( 'data-mce-bogus' ) {
while ( bogus.firstChild ) {
bogus.parentNode.insertBefore( bogus.firstChild, bogus );
}
}
bogus.parentNode.removeChild( bogus );
}
const content = body.innerHTML;
Not really sure how the performance compares though.
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, I think we still want to keep getting the raw content and strip what we strip now. I think it might make sense to, just like we do now, loop through all elements and attributes as we need to:
- Unwrap elements with the
data-mce-bogus
attribute. - Remove elements and their children with the
data-mce-bogus="all"
attribute. - Remove attributes that starts with
data-mce-
.
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 not feeling confident with all the TinyMCE's nodes requiring cleanup, I wouldn't mind some help in addressing this
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 have a look.
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.
Added in 0ad5fe1.
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 kind of a lot of code, but it's fast.
blocks/editable/index.js
Outdated
@@ -141,6 +127,9 @@ export default class Editable extends Component { | |||
|
|||
onInit() { | |||
this.updateFocus(); | |||
if ( this.props.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.
Will this cause the content to flicker in only after TinyMCE initializes? An advantage of the original implementation is that the content would be displayed even while TinyMCE initializes. Seems like we could recreate this by dangerously assigning the default value as inner HTML, which... should be safe?
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 don't see any flickering and I don't think it's a good idea. In fact, this allows us to drop the "shouldComponentUpdate/componentWillReceiveProps" entirely which simplifies the TinyMCE component a lot. see 2eb008dd
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 flicker is definitely there, and it's sensible to expect as such since we're not setting the content of the field until TinyMCE finishes initializing.
Master: https://cloudup.com/c3NdeDRaboZ
This branch: https://cloudup.com/cnJth3Fbhjg
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.
Gotcha, thanks for the screenshots. I found a way to fix while avoiding the sCU false
7cd4940
blocks/editable/index.js
Outdated
export default class Editable extends Component { | ||
constructor( props ) { | ||
super( ...arguments ); | ||
|
||
const { value } = props; | ||
if ( 'production' !== process.env.NODE_ENV && undefined !== value && | ||
! Array.isArray( 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.
With these changes, I think we might be able to just remove this warning altogether.
@@ -95,9 +102,11 @@ registerBlockType( 'core/pullquote', { | |||
|
|||
return ( | |||
<blockquote className={ `align${ align }` }> | |||
{ value && value.map( ( paragraph, i ) => <p key={ i }>{ paragraph.props.children }</p> ) } | |||
{ value && value.map( ( paragraph, i ) => | |||
<Editable.Value tagName="p" key={ i }>{ paragraph }</Editable.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.
Do you get the sense that in most of the cases we'd want a joined set of HTML, we'd be better served by supporting block nesting instead (i.e. should we bother with a matcher for this at all?).
blocks/library/table/index.js
Outdated
type: 'string', | ||
source: html( 'table' ), | ||
default: ` | ||
<tbody> | ||
<tr><td><br /></td><td><br /></td></tr> |
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.
Aside: Should these be marked as data-mce-bogus
br
? Seems like they should be ejected from the final markup.
ede9552
to
02c81f9
Compare
that's a good point, I also thought about this, but rewriting |
Can I have a second look here and whether we should merge this in this current shape or not? |
blocks/editable/index.js
Outdated
@@ -220,8 +200,6 @@ export default class Editable extends Component { | |||
|
|||
if ( this.editor.dom.isEmpty( rootNode ) && this.props.onReplace ) { | |||
this.props.onReplace( content ); | |||
} else { | |||
this.splitContent( 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.
Why did this go?
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 don't know, I recall removing it explicitly for a reason but totally forgot why. I'm bringing it back.
|
||
content = renderToString( content ); | ||
this.editor.setContent( content, { format: 'raw' } ); | ||
this.editor.setContent( 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.
Do you think we could drop this method then?
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 strong opinion here, but maybe we should keep it to avoid handling the default value in the different places we call this.
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, actually makes sense if we keep getContent
too.
blocks/editable/patterns.js
Outdated
@@ -189,7 +189,7 @@ export default function( editor ) { | |||
return; | |||
} | |||
|
|||
const firstText = content[ 0 ]; | |||
const firstText = editor.getBody().textContent; |
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.
Maybe worth investigating if this has any side effects. This is no longer firstText
.
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.
Right, not sure why we were only targeting the first text, I tried some patterns, it's working but not sure about all the usecases.
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'll have a look at this too.
// breaking React's ability to reconcile changes. | ||
// | ||
// See: https://github.com/facebook/react/issues/6802 | ||
return false; |
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 don't fully understand why this is being changed. I would also feel more comfortable if @aduth had a look at these parts. :)
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.
basically we were putting the defaultValue as a children of the TinyMce Node, so we couldn't allow preact to rerender this node, because its children can be mangled by TinyMCE.
With this new approach, it's simpler, we don't touch the children so we can allow React to rerender and update the attributes (styles, classnames,...) but it won't touch the children.
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.
Preact eh? 😄
blocks/library/list/index.js
Outdated
} else { | ||
listItems[ listItems.length - 1 ].props.children.push( element ); | ||
|
||
return content.split( /<br\s*?\/?>/ ).reduce( ( memo, item ) => { |
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 will be one of the less nice things about this change. Now be can't look inside the content that easily... I don't feel comfortable with a regex to parse this, it will lead to bugs. What if there's nested <br>
s? Maybe we should convert this to DOM (like we do with paste) and split on them there.
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 can, I was also thinking of removing this transform entirely, it doesn't make sense to me. It made sense a while back because we were adding br
's by default when hitting enter, it's not the case anymore.
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 it still makes sense as we still do allow content like that. I believe it was also a request with the old editor. It's also nice that it stays together when you convert it to text.
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.
@youknowriad I'll adjust it
blocks/library/list/index.js
Outdated
if ( liIndex !== listItems.length - 1 ) { | ||
content.push( createElement( 'br' ) ); | ||
} | ||
const { TEXT_NODE } = window.Node; |
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 we keep browser dependencies at the top?
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.
sure
blocks/library/list/index.js
Outdated
if ( 'UL' === element.nodeName || 'OL' === element.nodeName ) { // lists within lists | ||
element.childNodes.forEach( appendLiToContent ); | ||
} else if ( element.nodeType === TEXT_NODE ) { | ||
content.push( element.nodeValue ); |
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 it make this code simpler if you push the elements instead of the HTML, and get innerHTML
at the end?
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 strong opinions, I guess it doesn't change that much.
43c2fea
to
0ad5fe1
Compare
This is looking good to me, thanks @iseulde for the updates. The question now is whether we want to ship this or wait until we try the array structure approach. |
I think we should just go for it then? I would still prefer to see an array in the state, but we can look into it when we actually need it. |
0ad5fe1
to
70cc732
Compare
Thoughts on merging this @aduth? |
This is a breaking change for block authors, If we go this road, we should make sure to document this in the Release (or somewhere else) |
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.
Evaluating success here as simplifying the experience for a block implementer while not compromising availability of a rich data structure, I see mixed results. Surely, dropping children
and node
is a huge win to reducing the barrier to entry for block implementers, but <Editable.Value>
is fairly awkward in its current usage. Rather than proving that rich data is still easily available, we've opted for RegEx splits on HTML, which is something we'd very much want to discourage. Losing the data structure prevents us from being able to easily assign React children (both in Editable.Value
and Editable
's initial rendering). And we still seem to have need for some distinction to assume the role of node
(outerHTML
?). The simplified implementation of Editable is nice, but relatively speaking not an important consideration.
How do we expect to solve some of the above problems?
- Do we provide tools to convert HTML back into a rich data structure? Is this the wrong direction to be going at it?
- Granting this has been raised before, but it's not clear how we achieve "rich" data as the default without a separate matcher
- How can we resolve some of the issues with assigning HTML as content of React (safely)?
- At least toward a goal of
<p><Editable.Value>{ content }</Editable.Value></p>
- At least toward a goal of
A use case for rich data was described at #2750 (comment).
default: ` | ||
<tbody> | ||
<tr><td></td><td></td></tr> | ||
<tr><td></td><td></td></tr> | ||
</tbody>, |
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 comma is showing verbatim when inserting a new table.
Ella had commits dropping the regex, I accidentally removed them by force pushing. I'm asking Ella to bring them back since she probably still have them locally.
Didn't see any use-case where the
The tools are already Available IMO aka maybe not if we'd want server-side manipulation too, but yes if it's only client side.
I guess this is the tradeoff in this approach. I don't think reimplementing Honestly, I don't know if this is a good solution but don't see in the array structure a far better option. |
I'll bring back the commits and have a good look at this PR again. Honestly I don't mind it as much, as long as we're willing to consider other structures in the future that could benefit logic within editable, global features that touch it, and plugins extending it. I'm still quite curious to see opinions about editable state as a string with meta data. |
After some discussion with @aduth I came to question why we don't use Would have been a cool idea to use this in the HTML string form to simplify the parser to know the difference between text and structure. I'm still having trouble seeing how this is a good idea for the project. The more DOM-parsing which is required to understand a document, the more burden we place on the editors and the more we allow for ambiguity (or at least implementation differences) in the parse. It's nice having a plain text string with regions and attributes indicating modifications to them. |
What's the main reason we want to change the structure right now, before we can figure out the right structure (HTML/Array/String + meta) based on needs and use cases for each (like footnotes, suggestions)? To get the React objects out of the state? In that case we could do it as arrays for now, which is only a small change. a85bfe4 With the introduction of Editable.Value it also becomes easier for us to change later as we don't have to update docs and nothing would break. (Edit: okay, that's not entirely true, for state as HTML, Editable.Value would be different.) |
Can you ping me when this PR gets opened? 😃 I will be happy to test together with changes in coediting PR. |
fixes #2750
This updates the value of the
Editable
component to a string.Pros:
children
andnode
matchersWe may try a tree structure in a separate PR
Testing instructions
It's easy to miss a use case, the more we test this, the better is