Skip to content
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

Try supporting post meta in block attributes #2740

Merged
merged 12 commits into from Sep 25, 2017
2 changes: 1 addition & 1 deletion blocks/README.md
Expand Up @@ -237,7 +237,7 @@ editor interface where blocks are implemented.
- `attributes: Object | Function` - An object of attribute schemas, where the
keys of the object define the shape of attributes, and each value an object
schema describing the `type`, `default` (optional), and
[`source`](http://gutenberg-devdoc.surge.sh/reference/attribute-sources/)
[`source`](http://gutenberg-devdoc.surge.sh/reference/attributes/)
(optional) of the attribute. If `source` is omitted, the attribute is
serialized into the block's comment delimiters. Alternatively, define
`attributes` as a function which returns the attributes object.
Expand Down
4 changes: 2 additions & 2 deletions blocks/api/serializer.js
Expand Up @@ -101,8 +101,8 @@ export function getCommentAttributes( allAttributes, schema ) {
return result;
}

// Ignore values sources from content
if ( attributeSchema.source ) {
// Ignore values sources from content and post meta
if ( attributeSchema.source || attributeSchema.meta ) {
return result;
}

Expand Down
2 changes: 1 addition & 1 deletion blocks/editable/index.js
Expand Up @@ -70,7 +70,7 @@ export default class Editable extends Component {
`Invalid value of type ${ typeof value } passed to Editable ` +
'(expected array). Attribute values should be sourced using ' +
'the `children` source when used with Editable.\n\n' +
'See: http://gutenberg-devdoc.surge.sh/reference/attribute-sources/#children'
'See: http://gutenberg-devdoc.surge.sh/reference/attributes/#children'
);
}

Expand Down
68 changes: 0 additions & 68 deletions docs/attribute-sources.md

This file was deleted.

148 changes: 148 additions & 0 deletions docs/attributes.md
@@ -0,0 +1,148 @@
# Attributes

## Common Sources

Attribute sources are used to define the strategy by which block attribute values are extracted from saved post content. They provide a mechanism to map from the saved markup to a JavaScript representation of a block.

Each source accepts an optional selector as the first argument. If a selector is specified, the source behavior will be run against the corresponding element(s) contained within the block. Otherwise it will be run against the block's root node.

Under the hood, attribute sources are a superset of functionality provided by [hpq](https://github.com/aduth/hpq), a small library used to parse and query HTML markup into an object shape. In an object of attributes sources, you can name the keys as you see fit. The resulting object will assign as a value to each key the result of its attribute source.

### `attr`

Use `attr` to extract the value of an attribute from markup.

_Example_: Extract the `src` attribute from an image found in the block's markup.

```js
{
url: {
source: attr( 'img', 'src' )
}
}
// { "url": "https://lorempixel.com/1200/800/" }
```

### `children`

Use `children` to extract child nodes of the matched element, returned as an array of virtual elements. This is most commonly used in combination with the `Editable` component.

_Example_: Extract child nodes from a paragraph of rich text.

```js
{
content: {
source: children( 'p' )
}
}
// {
// "content": [
// "Vestibulum eu ",
// { "type": "strong", "children": "tortor" },
// " vel urna."
// ]
// }
```

### `query`

Use `query` to extract an array of values from markup. Entries of the array are determined by the selector argument, where each matched element within the block will have an entry structured corresponding to the second argument, an object of attribute sources.

_Example_: Extract `src` and `alt` from each image element in the block's markup.

```js
{
images: {
source: query( 'img', {
url: attr( 'src' )
alt: attr( 'alt' )
} )
}
}
// {
// "images": [
// { "url": "https://lorempixel.com/1200/800/", "alt": "large image" },
// { "url": "https://lorempixel.com/50/50/", "alt": "small image" }
// ]
// }
```

## Meta

Attributes may be obtained from a post's meta rather than from the block's representation in saved post content. For this, an attribute is required to specify its corresponding meta key under the `meta` key:

```js
attributes: {
author: {
type: 'string',
meta: 'author'
},
},
```

From here, meta attributes can be read and written by a block using the same interface as any attribute:

{% codetabs %}
{% ES5 %}
```js
edit: function( props ) {
function onChange( event ) {
props.setAttributes( { author: event.target.value } );
}

return el( 'input', {
value: props.attributes.author,
onChange: onChange,
} );
},
```
{% ESNext %}
```js
edit( { attributes, setAttributes } ) {
function onChange( event ) {
setAttributes( { author: event.target.value } );
}

return <input value={ attributes.author } onChange={ onChange } />;
},
```
{% end %}

### Considerations

By default, a meta field will be excluded from a post object's meta. This can be circumvented by explicitly making the field visible:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pento this would be nice to address in general :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's annoying.

@rmccue: I don't recall the history for why postmeta is only included in a response when it has the show_in_rest flag set. (As opposed to at least returning the visible postmeta when the user is appropriately authenticated.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meta fields aren't guaranteed to be "safe" by default. "Safe" is a few things, namely safe for JSON serialisation (which is lossy compared to PHP), and safe for capabilities (not all meta is always available).

The current behaviour exists for those reasons. When we initially didn't require show_in_rest, people noted that this would expose hidden fields created by plugin or users (turns out, a bunch of people use the "Custom Fields" metabox for random internal notes). Solving these problems in a consistent and safe way turned out to be essentially impossible.

show_in_rest is an indicator that plugin developers are accepting the limitations of the API for their meta fields.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These hidden fields would be shown if you do get_post_meta() calls, no? It feels like the API should adhere to the expectations of doing such authenticated calls.


```php
function gutenberg_my_block_init() {
register_meta( 'post', 'author', array(
'show_in_rest' => true,
) );
}
add_action( 'init', 'gutenberg_my_block_init' );
```

Furthermore, be aware that WordPress defaults to:

- not treating a meta datum as being unique, instead returning an array of values;
- treating that datum as a string.

If either behavior is not desired, the same `register_meta` call can be complemented with the `single` and/or `type` parameters as follows:

```php
function gutenberg_my_block_init() {
register_meta( 'post', 'author_count', array(
'show_in_rest' => true,
'single' => true,
'type' => 'integer',
) );
}
add_action( 'init', 'gutenberg_my_block_init' );
```

Lastly, make sure that you respect the data's type when setting attributes, as the framework does not automatically perform type casting of meta. Incorrect typing in block attributes will result in a post remaining dirty even after saving (_cf._ `isEditedPostDirty`, `hasEditedAttributes`). For instance, if `authorCount` is an integer, remember that event handlers may pass a different kind of data, thus the value should be cast explicitly:

```js
function onChange( event ) {
props.setAttributes( { authorCount: Number( event.target.value ) } );
}
```
2 changes: 1 addition & 1 deletion docs/block-api.md
Expand Up @@ -88,7 +88,7 @@ attributes: {
},
```

* **See: [Attribute Sources](/reference/attribute-sources/).**
* **See: [Attributes](/reference/attributes/).**

### Transforms (optional)

Expand Down
2 changes: 1 addition & 1 deletion docs/blocks-editable.md
Expand Up @@ -105,7 +105,7 @@ registerBlockType( 'gutenberg-boilerplate-esnext/hello-world-step-03', {
```
{% end %}

When registering a new block type, the `attributes` property describes the shape of the attributes object you'd like to receive in the `edit` and `save` functions. Each value is a [source function](attribute-sources.md) to find the desired value from the markup of the block.
When registering a new block type, the `attributes` property describes the shape of the attributes object you'd like to receive in the `edit` and `save` functions. Each value is a [source function](attributes.md) to find the desired value from the markup of the block.

In the code snippet above, when loading the editor, we will extract the `content` value as the children of the paragraph element in the saved post's markup.

Expand Down
6 changes: 3 additions & 3 deletions docs/index.js
Expand Up @@ -64,9 +64,9 @@ addStory( {

addStory( {
parents: [ 'reference' ],
name: 'attribute-sources',
title: 'Attribute Sources',
markdown: require( './attribute-sources.md' ),
name: 'attributes',
title: 'Attributes',
markdown: require( './attributes.md' ),
} );

addStory( {
Expand Down
2 changes: 1 addition & 1 deletion docs/reference.md
@@ -1,6 +1,6 @@
# Reference

- [Attribute Sources](./reference/attribute-sources)
- [Attributes](./reference/attributes)
- [Glossary](./reference/glossary)
- [Design Principles](./reference/design-principles)
- [Coding Guidelines](./reference/coding-guidelines)
Expand Down
43 changes: 33 additions & 10 deletions editor/modes/visual-editor/block.js
Expand Up @@ -4,7 +4,7 @@
import { connect } from 'react-redux';
import classnames from 'classnames';
import { Slot } from 'react-slot-fill';
import { partial } from 'lodash';
import { has, partial, reduce, size } from 'lodash';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';

/**
Expand All @@ -27,29 +27,31 @@ import BlockMover from '../../block-mover';
import BlockRightMenu from '../../block-settings-menu';
import BlockSwitcher from '../../block-switcher';
import {
updateBlockAttributes,
clearSelectedBlock,
editPost,
focusBlock,
mergeBlocks,
insertBlocks,
mergeBlocks,
removeBlocks,
clearSelectedBlock,
startTyping,
stopTyping,
replaceBlocks,
selectBlock,
startTyping,
stopTyping,
updateBlockAttributes,
} from '../../actions';
import {
getPreviousBlock,
getNextBlock,
getBlock,
getBlockFocus,
getBlockIndex,
getEditedPostAttribute,
getMultiSelectedBlockUids,
getNextBlock,
getPreviousBlock,
isBlockHovered,
isBlockSelected,
isBlockMultiSelected,
isBlockSelected,
isFirstMultiSelectedBlock,
isTyping,
getMultiSelectedBlockUids,
} from '../../selectors';

const { BACKSPACE, ESCAPE, DELETE, ENTER } = keycodes;
Expand Down Expand Up @@ -145,7 +147,23 @@ class VisualEditorBlock extends Component {

setAttributes( attributes ) {
const { block, onChange } = this.props;
const type = getBlockType( block.name );
onChange( block.uid, attributes );

const metaAttributes = reduce( attributes, ( result, value, key ) => {
if ( type && has( type, [ 'attributes', key, 'meta' ] ) ) {
result[ type.attributes[ key ].meta ] = value;
}

return result;
}, {} );

if ( size( metaAttributes ) ) {
this.props.onMetaChange( {
...this.props.meta,
...metaAttributes,
} );
}
}

maybeHover() {
Expand Down Expand Up @@ -428,6 +446,7 @@ export default connect(
isTyping: isTyping( state ),
order: getBlockIndex( state, ownProps.uid ),
multiSelectedBlockUids: getMultiSelectedBlockUids( state ),
meta: getEditedPostAttribute( state, 'meta' ),
};
},
( dispatch, ownProps ) => ( {
Expand Down Expand Up @@ -484,5 +503,9 @@ export default connect(
onReplace( blocks ) {
dispatch( replaceBlocks( [ ownProps.uid ], blocks ) );
},

onMetaChange( meta ) {
dispatch( editPost( { meta } ) );
},
} )
)( VisualEditorBlock );