-
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
Framework: Use a simple JS object to declare the attribute's source #2854
Conversation
If verbosity is a concern then in JS there could be shorthand “macros” like: const attr = ( key ) => ( {
type: 'attribute',
attribute: key
} ); In that way you could still do: attributes: {
url: {
type: 'string',
source: attr( 'src' ),
}
} If you wanted to. |
Talking with @mtias about this, it was mentioned maybe we don't need to nest source, and rather create the source-relevant properties on the same top-level, i.e. // Before:
attributes: {
url: {
type: 'string',
source: {
type: 'attribute',
attribute: 'src',
}
}
}
// After:
attributes: {
url: {
type: 'string',
source: 'attribute',
attribute: 'src',
}
} |
@aduth How would you declare Nesting is needed IMO, and I personally prefer an explicit |
source: 'attribute',
attribute: 'src', 😵 |
I may be playing devil's advocate here, since I'd originally been a proponent of the nested source. But I'm trying to be sensitive to the impact of nesting, which I think can be harmful to general readability of the block definition and, from the perspective of the implementer, how obvious it is to structure. JSON schema is easy to pick on here, because it's... excessive ([1], [2]). While the requirements forced us to rethink the structure, I long for the simplicity we once had for defining attributes: gutenberg/editor/blocks/image/index.js Lines 11 to 15 in 376d25d
Of course, where we balance is on consistency and accommodating the varied needs of defining the source, where a nested object could have some merit.
If we consider {
type: 'query',
selector: 'div',
source: {
type: 'query',
selector: 'p',
source: {
type: 'attribute',
attribute: 'class',
},
},
} Also worth considering optimizing for the more common scenario:
You are going to have to elaborate here 😃 I assume you're not a fan of the redundancy? What would you see as the alternative here? |
I have no critique, I just thought it was funny. :) |
@aduth so for the first level, the source type is stored in "source", and the nested levels it's in "type". I don't like this because it's not consistent. Overall what you propose is just considering the first level as a superset of "source". |
The intent would be to optimize for the common use-case, which is not |
Actually, the non-nested {
type: 'array',
source: 'query',
selector: 'div',
query: {
source: 'query',
selector: 'p',
query: {
source: 'attribute',
attribute: 'class',
},
},
} |
@aduth Oh! nice proposal, I'll try to update accordingly. |
Just now realizing I had |
40d3d40
to
f9c9356
Compare
Updated per suggestion |
blocks/library/gallery/index.js
Outdated
source: 'query', | ||
selector: 'div.wp-block-gallery figure.blocks-gallery-image img', | ||
query: { | ||
source: 'object', |
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 we need this 'object'
source at all? Is it entirely specific to the query
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.
We need something to distinguish query( 'div', html() )
and query( 'div', { a: html(), b: prop() }
and it's not easy to avoid ambiquity between an object defining thee shape of the sub sources or an object defining a regular source.
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.
Hmm, I see the issue. I don't think we want to natively support the concept of sub-sources, but rather enable specific sources like query
to encapsulate the behavior they need. In other words, I'd really prefer we not have object
be a proper source. Will poke at this a bit, since it's not obvious what a solution would be.
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.
Thinking about this more, I think this matcher can make sense even without query if you want to shape an attribute as an object composed from several sub matchers.
Anyway, I'm open to suggestions 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.
In that case, it still doesn't seem to follow that the source is an object (at least this is how it reads when seeing source: 'object'
), but rather the desired shape is an object of a particular structure. The feature itself is not one I think we need to support at this point either.
(Will take a closer look shortly for more concrete suggestions)
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.
honestly, I still think the current approach is the best approach (consistent and unambiguous)
Personally I find the object
source to be very ambiguous when considered not just within the context of the query
source.
The examples I'm seeing where we'd need to change the structure for a flattened query array are: pullquote, quote, and text columns, correct? And because of how they interact with Editable? Are these not the same types of blocks we'd be porting to use proper block nesting? Wondering if we're optimizing for a problem that won't exist.
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.
Correct, these are the concerned blocks. I didn't consider quotes as container for paragraph blocks but I guess you're right, it's a good fit.
So what's the "temporary" option we should go with right now? removing the non-object query
and transforming the value in edit/save
for these 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.
So what's the "temporary" option we should go with right now? removing the non-object
query
and transforming the value inedit/save
for these blocks?
I'd probably be okay with this, yes. Motivation to get progress on nested 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.
updated in a4c2982
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, quotes would be containers for paragraph blocks, and potentially support a few others, like lists, etc.
blocks/api/parser.js
Outdated
} | ||
|
||
/** | ||
* Given a blocktype, a block's raw content and the commentAttributes returns the attribute value depending on its source definition |
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.
Description here doesn't seem accurate to the arguments accepted (specifically block type).
blocks/api/parser.js
Outdated
* @param {string} rawContent Block's raw content | ||
* @param {Object} commentAttributes Block's comment attributes | ||
* | ||
* @return {mixed} Attribute 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.
From what I can tell, there is no mixed
type for JSDoc, but rather the *
(any) type:
http://usejsdoc.org/tags-param.html#multiple-types-and-repeatable-parameters
blocks/api/serializer.js
Outdated
@@ -102,7 +102,7 @@ export function getCommentAttributes( allAttributes, blockType ) { | |||
} | |||
|
|||
// Ignore values sources from content and post meta | |||
if ( attributeSchema.source || attributeSchema.meta ) { | |||
if ( attributeSchema.source !== '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.
Will this change cause meta attributes to be serialized?
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 this function should only take care of comment
attributes, so better be explicit about it.
blocks/api/parser.js
Outdated
switch ( attributeSchema.source ) { | ||
case 'meta': | ||
break; | ||
case '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.
Hmm, we've tried to avoid being explicit about comment as a source, treating it as a default. Here instead we treat default as though source is one of the hpq matchers. Maybe instead we should include the cases for those sources in matcherFromSource
and let comment be the default case?
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.
Hmm, we've tried to avoid being explicit about comment as a source
Why? I see value in leaving the matchers as the non-explicit ones to avoid listing all the matchers.
It makes sense to me to have a clear matcherFromSource
function responsible only for sourcing attributes from 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.
Why? I see value in leaving the matchers as the non-explicit ones to avoid listing all the matchers.
Implementation aside, it's if we conceptually consider serialized comments to be the default storage for attributes, but support other strategies for sourcing these values.
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.
To this point, we ought to consider consistency with how we refer to the commentAttributes
argument here. In createBlockWithFallback
, this is named attributes
. In parseWithGrammar
, the block node property is named attrs
.
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 I should use attrs
in all these places?
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 a huge fan of abbreviations. If we were to think of comment attributes as the "base" attribute set, then attributes
would be appropriate.
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 confusing to me. The function is called getBlockAttributes
, it shouldn't have attributes
as an argument don't you think?
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 is confusing, I agree. This ties into disjointedness of our attribute sourcing generally (https://github.com/WordPress/gutenberg/pull/2854/files#r144836758). What are these attributes which are magically prepopulated? Are the comments a core part of what a block is, or an incidental implementation detail? In the current context, I could see parsedAttributes
making sense, but our approach of passing content and comment attributes as arguments doesn't really scale well to other source types, and as implemented we are aware of all but implementing behaviors of only some types.
How capable do we want the parsing to be? I don't necessarily think we need to build in concepts of post meta and site options to the parser itself, but maybe as some extensibility pattern: either allowing additional sources to inject their values to be referenced by the parser when encountering that source type, or hooks to allow the post editor a turn when encountering the parsing or serialization behaviors of a specific source type.
Alternatively, if it's the case that our editor selector is responsible for managing the implementation of specific types (post meta), why should this not also extend to sourcing from content as well? Ultimately maybe there is a line where a blocks core knows to handle serialized and markup-sourced attributes, then allow additional handlers to be reflected later. But ideally not with built-in awareness of what those other handlers are; if we dropped case 'meta'
and treated comments as the default, would it work just as well?
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.
if we dropped case 'meta' and treated comments as the default, would it work just as well?
Can you provide a code example, I don't understand what you're proposing here.
: either allowing additional sources to inject their values to be referenced by the parser when encountering that source type,
This won't work for site options and post meta because their value change over time, and using the parser for this is not a good option. The parser is meant for parsing post_content (HTML) and the content contains only two types of attributes: comment attributes and attributes parsed from HTML.
hooks to allow the post editor a turn when encountering the parsing or serialization behaviors of a specific source type.
Maybe, this means hooks to the getBlock
selcetor and updateAttributes
callback.
I think we're struggling here because we're trying to fix the symptoms instead of trying to fix the root issue. And the root issue is the Editor not being generic enough. Things would much clearer if the Editor was a generic content editor (not tied to post or site) with extensibility mechanisms.
I do think this is out of scope of the current 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.
I think we're struggling here because we're trying to fix the symptoms instead of trying to fix the root issue. And the root issue is the Editor not being generic enough. Things would much clearer if the Editor was a generic content editor (not tied to post or site) with extensibility mechanisms.
This is really what I'm trying to get at in my earlier comment, and arguably it is in scope because we're introducing knowledge of a meta type to the parser with these changes that had not existed previously.
blocks/api/registration.js
Outdated
|
||
const attributes = settings.attributes | ||
? settings.attributes | ||
: get( window._wpBlocksAttributes, 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.
Drop trailing comma on arguments.
editor/modes/visual-editor/block.js
Outdated
@@ -145,7 +145,7 @@ class VisualEditorBlock extends Component { | |||
onChange( block.uid, attributes ); | |||
|
|||
const metaAttributes = reduce( attributes, ( result, value, key ) => { | |||
if ( type && has( type, [ 'attributes', key, 'meta' ] ) ) { | |||
if ( type && get( type, [ 'attributes', key, 'source' ] ) === 'meta' ) { |
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.
Minor unrelated note: type &&
is redundant here (get
will handle the falsey type
)
There are failing tests in block registration. |
Yep, I didn't take care of tests and documentation yet, wanted to leave this after we settle on the format. I can do so now. |
Codecov Report
@@ Coverage Diff @@
## master #2854 +/- ##
==========================================
- Coverage 34.61% 34.36% -0.25%
==========================================
Files 261 261
Lines 6769 7007 +238
Branches 1231 1317 +86
==========================================
+ Hits 2343 2408 +65
- Misses 3733 3837 +104
- Partials 693 762 +69
Continue to review full report at Codecov.
|
0a1b070
to
ad04011
Compare
Updated this PR with unit tests and documentation, it's in a better shape now. |
Hit another merge conflict 😄 |
blocks/api/parser.js
Outdated
result[ key ] = coercedValue; | ||
return result; | ||
}, {} ); | ||
const blockAttributes = keys( blockType.attributes ).reduce( ( memo, attributeKey ) => { |
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.
Lodash's reduce
could make this a little more concise:
const blockAttributes = reduce( blockType.attributes, ( memo, attributeSchema, attributeKey ) => {
blocks/api/parser.js
Outdated
const attributeSchema = blockType.attributes[ attributeKey ]; | ||
memo[ attributeKey ] = getBlockAttribute( attributeKey, attributeSchema, rawContent, attributes ); | ||
return memo; | ||
}, {} ) || {}; |
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 think the || {}
would ever be triggered? Even if the reduce
operates on an empty set, the initial value should be returned.
> [].reduce( () => {}, {} );
{}
blocks/api/registration.js
Outdated
...settings, | ||
name, | ||
attributes: keys( attributes ).reduce( ( memo, attributeKey ) => { |
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 your previous comment:
I see value in leaving the matchers as the non-explicit ones to avoid listing all the matchers.
Is the whole existence of this reduce
here to support being able to specify the comment
case?
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?
blocks/api/parser.js
Outdated
export function getBlockAttribute( attributeKey, attributeSchema, rawContent, commentAttributes ) { | ||
let value; | ||
switch ( attributeSchema.source ) { | ||
case 'meta': |
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.
Our approach for defining other attribute sources is a bit awkward, I think, in that we have to explicitly bypass the getter here to allow it to be handled elsewhere later. Would like if these strategies were more consolidated. cc @mcsf
blocks/api/parser.js
Outdated
switch ( attributeSchema.source ) { | ||
case 'meta': | ||
break; | ||
case '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.
To this point, we ought to consider consistency with how we refer to the commentAttributes
argument here. In createBlockWithFallback
, this is named attributes
. In parseWithGrammar
, the block node property is named attrs
.
blocks/library/gallery/index.js
Outdated
source: 'query', | ||
selector: 'div.wp-block-gallery figure.blocks-gallery-image img', | ||
query: { | ||
source: 'object', |
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 few ideas:
- We could support one or the other, but not both, where query can either return an array of objects shaped of a particular structure, or an array of a particular source value
- We could split the two options by property name, e.g.
queryObject
(array of sourced object shape) andquery
- We could test the presence of
query.source
and infer if it's defining a shape or the nested source
ad04011
to
18c5e05
Compare
f0aa05f
to
456f054
Compare
A final review for 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.
Not sure if it's introduced by this pull request or an issue that's been resolved since last rebase, but I'm seeing many invalid blocks when simply saving Demo and refreshing the page.
blocks/api/parser.js
Outdated
|
||
// If the block supports anchor, parse the id | ||
if ( blockType.supportAnchor ) { | ||
blockAttributes.anchor = hpqParse( rawContent, attr( '*', 'id' ) ); | ||
} | ||
|
||
// If the block supports a custom className parse it | ||
if ( blockType.className !== false && attributes && attributes.className ) { | ||
if ( false !== blockType.className && attributes && attributes.className ) { |
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.
Thinking you might have had to make these changes only because you were running an older version of the branch with newer dependencies? (Related: #3187)
Noting that this received no response. It is not a blocker to me for merge.
blocks/api/serializer.js
Outdated
@@ -102,7 +102,7 @@ export function getCommentAttributes( allAttributes, blockType ) { | |||
} | |||
|
|||
// Ignore values sources from content and post meta |
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 update this comment.
104a32a
to
207d794
Compare
Rebased this and addressed the comments. I'm not seeing the "invalid" issue, it might have been fixed by the rebase.
blocks/api/registration.js
Outdated
|
||
return blocks[ name ] = settings; | ||
return blocks[ name ] = 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 assignment seems unnecessary given let block = blocks[ name ] = {
above.
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 catch
blocks/api/registration.js
Outdated
name, | ||
attributes: get( window._wpBlocksAttributes, name ), | ||
|
||
const attributes = settings.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.
Was the change to extract this out necessary? Is it just for providing the default to get
, and if so, could that have been added in-place?
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 reason, just a rebase leftover
207d794
to
c013766
Compare
c013766
to
2aeb51a
Compare
*/ | ||
import * as source from './source'; | ||
|
||
export { source }; | ||
export { createBlock, switchToBlockType, createReusableBlock } from './factory'; | ||
export { default as parse, getSourcedAttributes } from './parser'; |
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.
Bug here: getSourcedAttributes
was removed from parser
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.
See PR with fix here:
#3582
Description
This PR can be seen as the first step towards #2751
Instead of using the hpq matchers, we use a declarative API to define the attributes source
Caveats
A bit verbose compared to the previous approach, noticeable when nesting
Pros
hpq
direct dependency, no need to wrap the matchersChecklist: