Framework: Use a simple JS object to declare the attribute's source#2854
Framework: Use a simple JS object to declare the attribute's source#2854youknowriad merged 13 commits intomasterfrom
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.
Do we need this 'object' source at all? Is it entirely specific to the query behavior?
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
So what's the "temporary" option we should go with right now? removing the non-object
queryand transforming the value inedit/savefor these blocks?
I'd probably be okay with this, yes. Motivation to get progress on nested blocks 😄
There was a problem hiding this comment.
Yes, quotes would be containers for paragraph blocks, and potentially support a few others, like lists, etc.
| } | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
Description here doesn't seem accurate to the arguments accepted (specifically block type).
| * @param {Object} attributeSchema Attribute's schema | ||
| * @param {string} rawContent Block's raw content | ||
| * @param {Object} commentAttributes Block's comment attributes | ||
| * |
There was a problem hiding this comment.
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
|
|
||
| // Ignore values sources from content and post meta | ||
| if ( attributeSchema.source || attributeSchema.meta ) { | ||
| if ( attributeSchema.source !== 'comment' ) { |
There was a problem hiding this comment.
Will this change cause meta attributes to be serialized?
There was a problem hiding this comment.
no this function should only take care of comment attributes, so better be explicit about it.
blocks/api/parser.js
Outdated
| let value; | ||
| switch ( attributeSchema.source ) { | ||
| case 'meta': | ||
| break; |
There was a problem hiding this comment.
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.
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.
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.
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.
Do you think I should use attrs in all these places?
There was a problem hiding this comment.
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.
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.
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.
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.
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.
Drop trailing comma on arguments.
editor/modes/visual-editor/block.js
Outdated
|
|
||
| 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.
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.
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.
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
| attributes: get( window._wpBlocksAttributes, name ), | ||
| ...settings, | ||
| name, | ||
| attributes: keys( attributes ).reduce( ( memo, attributeKey ) => { |
There was a problem hiding this comment.
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?
| */ | ||
| export function getBlockAttribute( attributeKey, attributeSchema, rawContent, commentAttributes ) { | ||
| let value; | ||
| switch ( attributeSchema.source ) { |
There was a problem hiding this comment.
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
| let value; | ||
| switch ( attributeSchema.source ) { | ||
| case 'meta': | ||
| break; |
There was a problem hiding this comment.
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.
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.sourceand infer if it's defining a shape or the nested source
ad04011 to
18c5e05
Compare
f0aa05f to
456f054
Compare
|
A final review for this? |
aduth
left a comment
There was a problem hiding this comment.
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 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.
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 | |||
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
There was a problem hiding this comment.
This assignment seems unnecessary given let block = blocks[ name ] = { above.
blocks/api/registration.js
Outdated
There was a problem hiding this comment.
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.
No reason, just a rebase leftover
207d794 to
c013766
Compare
c013766 to
2aeb51a
Compare
|
|
||
| export { source }; | ||
| export { createBlock, switchToBlockType, createReusableBlock } from './factory'; | ||
| export { default as parse, getSourcedAttributes } from './parser'; |
There was a problem hiding this comment.
Bug here: getSourcedAttributes was removed from parser
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
hpqdirect dependency, no need to wrap the matchersChecklist: