-
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
Enhance attributes to specify types and defaults #1905
Conversation
Should the format here not try to conform to JSON Schema? This refactored Example from REST API schema: https://github.com/WordPress/wordpress-develop/blob/4.8.0/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L2123-L2124 |
@westonruter Hmm, maybe. JSON Schema certainly loses some of the expressiveness that the "compact" form allows here. Additionally, how would you propose a JSON Schema approach handle sourcing data, e.g. here |
@aduth I don't think the presence of a You can see then that when fetching the data for the schema, the So I think we should do the same for blocks: the data structure used when registering blocks should be a superset of JSON Schema, but we should re-use as many of the formats that are defined in the specification. This will hopefully make it easier to pass block attributes between the client and the server, and we could use any libraries developed for JSON Schema to validate and utilize block schemas. |
It's certainly tempting, as there's already quite a bit of overlap. The main downside is verbosity of JSON Schema, but the difference between I also don't know how much we want to commit to client/server interoperability of attributes. One possible need we'd have is custom transformations of parsed data during attributes parsing, supported here with custom Related to #672, JSON Schema could enable some automatic form generation, e.g. https://github.com/mozilla-services/react-jsonschema-form |
Is it not even less, since - foo: { type: Number, defaultValue: 'bar' }
+ foo: { type: 'number', 'default': 'bar' } But then with JSON Schema we'd be able to also describe attributes with all of the properties that it provides, including
Would you elaborate? |
Not in the compact form, which allows attribute values to be either the matcher or constructor directly. See included embed block as example: attributes: {
title: attr( 'iframe', 'title' ),
caption: children( 'figcaption' ),
url: String,
align: String,
},
The thought being parse-time derived attributes. Struggling to think of some good real-world examples, but maybe: rounding a parsed float value to a certain number of digits, calculating (gallery) columns based on number of matched elements. Not necessarily the case that these can't be performed in the render functions, but there may be value in being able to generate these attributes external to a render, and it's otherwise hard to guarantee consistency particularly between separate |
Thanks, @aduth, this seems much better than before. I like the simplicity of types for now – we can easily switch to proptypes or something similar in the future, if there’s interest. Do you think the implicit connection between attributes kept in the HTML comment and them marked as a matcher is clear enough? Are we running the default checks on each re-render or only when we create the block? Do you think that bundling types with defaults is better than a function like |
blocks/api/factory.js
Outdated
if ( undefined !== value ) { | ||
result[ key ] = value; | ||
} else { | ||
source = getNormalizedAttributeSource( 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.
Have you considered calling this function (getNormalizedAttributeSource
) when registering the block to avoid having to do it everytime we need the block attributes?
blocks/api/parser.js
Outdated
attributes = { | ||
...attributes, | ||
...getMatcherAttributes( rawContent, sources ), | ||
}; |
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 we avoid the getMatcherAttributes
call and handle this while looping over sources
?
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 the approach proposed here, explicitly defining all the attributes in the same place is a huge win.
Question about the types, have we surfaced use-cases for these? Is it necessary to have the attribute defined as a "Constructor"?
@nb @youknowriad Do you have any thoughts on @westonruter's proposal for aligning this even further to JSON Schema? |
Good question: Are we trying to match the JSON Schema just to match a specification or do we expect to use the JSON Schema validation/form generation mechanism? Maybe we could stick with our "simpler" config until we find a use-case for the JSON-Schema related features. We still have some time to break the blocks API. |
@aduth this hits a more philosophical and higher-level question – what’s the role of the server-side? The way I see the project and its strengths, the user experience when editing a block is the feature that will have the biggest impact and be the biggest differentiator. This means richer and richer experiences on the JavaScript side and will eventually bring the need for more sophisticated type checking. Back to the question at hand – JSON Schema looks fine and has a good amount of fidelity, lends itself to introspection. I also think it’s a bit verbose compared to, let’s say, React’s prop-types, but I agree with @westonruter that this would allow having the some attribute validation to be sent to the server. Of all the things we could have both in the browser and on the server, validation sounds the most useful. Looking at the only server-side example we have, Without more server-side examples, it’s hard to make an informed decision. Here are few open questions for me: how often would we need some JS logic to compute a default, or validation? Will there be sometimes a difference between attributes accepted on the server and in the browser? |
I've tried to gather up as many of the issues for server-side blocks (dynamic blocks) as possible in https://github.com/WordPress/gutenberg/projects/6
In the case of latest-posts or any dynamic block, there will be a need to obtain the default properties for the block to render. At the moment, the block's default attributes are duplicated since there is no common schema that can be used on both the client and the server. Duplicated default: gutenberg/lib/blocks/latest-posts.php Line 16 in 1c7aaf8
Likewise, in the case of validation, there is procedural ad hoc validation of the attributes to ensure that the values have the expected types: gutenberg/lib/blocks/latest-posts.php Lines 27 to 34 in 1c7aaf8
All of this logic could be replaced with calls to
Maybe the server would allow a number string when the schema asks for an integer. The REST API schema sanitization logic will do that type conversion. |
At worst, this feature makes comment attributes much more obvious. At best, it elevates them to the role of the default attribute type, whereas something with a matcher (or external source) is delegated, telling the editor "instead of HTML comments, look for the value here instead".
We should only need to when creating the block. I'm vaguely remembering an issue you'd raised about this previously, but cannot recall specifics.
You might also be interested in #892, which takes an approach similar to this and highlights some of the contrasting requirements. A main difference is in consolidation and formality of comment attributes. In the #892 implementation, I do find that the function approach succeeds in:
That sounds like a good idea.
Yeah, I see the inefficiency, and I think it can be improved. One potential hiccup is that |
Given the JSON Schema idea the argument where to see the default isn’t relevant.
There are plenty of opportunities to |
In that case, maybe check
|
This was on my mind a bit over the weekend. Thinking about JSON Schema, I was imagining a couple options:
{
"postsToShow": {
"type": "number",
"min": 0,
"max": 20
},
"align": {
"type": "string",
"enum": [ "left", "center", "right", "wide", "full" ],
"default": "center",
}
} import attributes from './attributes.json'; In PHP, this could be json_decode( file_get_contents( dirname( __FILE__ ) . '/attributes.json' ) ); (Or automatically detected in PHP)
register_block( 'core/latest-posts', [
'attributes' => [
'postsToShow' => [
'type' => 'number',
'min' => 0,
'max' => 20
],
'align' => [
'type' => 'string',
'enum' => [ 'left', 'center', 'right', 'wide', 'full' ],
'default' => 'center',
],
],
] ); In both cases, attribute definitions are consolidated to a single location, the main difference being in the latter example that all blocks would need to register themselves server-side, even if they don't have any server-specific logic. Another potential issue is recreating all of the attribute matching in JSON or PHP. At first I thought something like: {
"url": {
"type": "string",
"source": {
"type": "attribute",
"selector": "img",
"name": "src"
}
}
} But this only works well for the simplest matchers like {
"value": {
"type": "node",
"value": [ "query", "blockquote > p", [ "node" ] ]
}
} But then it became clear: How do we represent values like On the side of "Do we care to?", it's not clear that there are use-cases in the server where we'd need the node value, particularly because in most of these cases, there isn't markup for the block anyways (at least not that we care to source from). This could lead to a couple possible conclusions:
Anyways, this is all a brain dump. I don't have any solutions in mind. But I'd be reluctant to move on #1902 if we're still not settled to the actual structure of attributes. There are a few questions to the requirements though:
|
I don't know if we'd ever need to replicate the attributes parsing mechanism in PHP but regardless it's appealing to just make it a possibility (thinking block parsing API...). In their current form, the attributes can't be used server side. Anyway, we should try to move things forward and maybe split things into smaller steps.
|
@aduth This looks good to me. Is the type mandatory here? What impact the type will have in parsing since the JSON attribute serialization already keeps the "type" information. |
To be valid JSON schema, I'd say it'd be good to enforce as mandatory.
For comment attributes, it could serve as validation (on parse) and coercion (on save, or assignment). Imagine a <input onInput={ ( event ) => setAttributes( { myNumber: event.target.value } ) } /> ...which of course would assign number as a string. |
Looks good to me, too 👍 |
c102331
to
b64317c
Compare
I'm still giving the changes a thorough look over because it touches almost everything, but I've successfully rebased and effected latest proposal into this branch. This does not include any sort of type validation or coercion, but these don't really exist currently anyways, so I think it'd be fine to iterate in subsequent pull requests. |
Codecov Report
@@ Coverage Diff @@
## master #1905 +/- ##
==========================================
+ Coverage 24.96% 25.41% +0.45%
==========================================
Files 151 151
Lines 4679 4705 +26
Branches 795 792 -3
==========================================
+ Hits 1168 1196 +28
- Misses 2965 2967 +2
+ Partials 546 542 -4
Continue to review full report at Codecov.
|
@@ -136,7 +136,10 @@ add_action( 'enqueue_block_editor_assets', 'random_image_enqueue_block_editor_as | |||
category: 'media', | |||
|
|||
attributes: { | |||
category: query.attr( 'img', 'alt' ) | |||
category: { | |||
type: '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.
It's not clear here if this is optional.
blocks/api/parser.js
Outdated
return reduce( blockType.attributes, ( result, source, key ) => { | ||
let value = attributes[ key ]; | ||
|
||
// Return default if attribute value not assigned |
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.
"is not assigned"?
@aduth great work here. I am also slightly conflicted with the verbosity of In any case, this is a solid improvement in clarity.
I had been thinking about having a
Having access to a registered block, even if it doesn't have any purpose server-side, could still be beneficial for things like setting default blocks through configuration hooks, excluding blocks from certain post types, as well as defining templates in PHP that would declare certain client-side blocks to be used. |
0ae7a07
to
7f9dd2b
Compare
One use case is the added ability to source a number from an attribute, e.g. |
I've done another rebase pass, including review and incorporating newly added attributes, default attributes. Also as refactoring pass to extract type coercion for testing in 7f9dd2b, I added logging to warn about unexpected types for comment-serialized attributes (in development environments). Example: I am planning to merge this tomorrow AM (EDT), followed shortly thereafter by #1902, unless there are remaining concerns or objections. |
Closes #865
Closes #973
Supersedes #892, #988
Related: #391, #714
This pull request seeks to refactor the block type
attributes
property to specify not only attributes parsed from content, but also those also encoded in the block comment delimiter. Further, it optionally allows specifying type and default value of an attribute, replacing previous efforts includingdefaultAttributes
and render-time destructuring.The existing behavior of
attributes
matchers will continue to work as-is, but comment attributes are no longer inferred and must instead be explicitly defined. The value of each key inattributes
can be a matcher, a constructor (type), or an object specifying any oftype
,matcher
, anddefaultValue
.See the following comment for the "kitchen sink" example: #1905 (comment)
See original kitchen sink exampleBlock authors can potentially implement their own type coercion by specifying a constructor with(Removed)valueOf
implementation. There is some inspiration here from Vue.js prop validation andprop-types
.Testing instructions:
Ensure that tests pass:
Verify that there is no regressions in block usage, particularly: