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

Framework: Use a simple JS object to declare the attribute's source #2854

Merged
merged 13 commits into from
Nov 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions blocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ add_action( 'enqueue_block_editor_assets', 'random_image_enqueue_block_editor_as
attributes: {
category: {
type: 'string',
source: source.attr( 'img', 'alt' )
source: 'attribute',
attribute: 'alt',
selector: 'img',
}
},

Expand Down Expand Up @@ -238,8 +240,8 @@ editor interface where blocks are implemented.
keys of the object define the shape of attributes, and each value an object
schema describing the `type`, `default` (optional), and
[`source`](https://wordpress.org/gutenberg/handbook/reference/attributes/)
(optional) of the attribute. If `source` is omitted, the attribute is
serialized into the block's comment delimiters. Alternatively, define
(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.
- `category: string` - Slug of the block's category. The category is used to
organize the blocks in the block inserter.
Expand Down
6 changes: 0 additions & 6 deletions blocks/api/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
/**
* External dependencies
*/
import * as source from './source';

export { source };
export { createBlock, switchToBlockType, createReusableBlock } from './factory';
export { default as parse, getSourcedAttributes } from './parser';

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

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

export { default as rawHandler } from './raw-handling';
Expand Down
156 changes: 67 additions & 89 deletions blocks/api/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import { parse as hpqParse } from 'hpq';
import { mapValues, reduce, pickBy } from 'lodash';
import { mapValues } from 'lodash';

/**
* Internal dependencies
Expand All @@ -12,42 +12,7 @@ import { getBlockType, getUnknownTypeHandlerName } from './registration';
import { createBlock } from './factory';
import { isValidBlock } from './validation';
import { getCommentDelimitedContent } from './serializer';

/**
* Returns true if the provided function is a valid attribute source, or false
* otherwise.
*
* Sources are implemented as functions receiving a DOM node to select data
* from. Using the DOM is incidental and we shouldn't guarantee a contract that
* this be provided, else block implementers may feel inclined to use the node.
* Instead, sources are intended as a generic interface to query data from any
* tree shape. Here we pick only sources which include an internal flag.
*
* @param {Function} source Function to test
* @return {Boolean} Whether function is an attribute source
*/
export function isValidSource( source ) {
return !! source && '_wpBlocksKnownSource' in source;
}

/**
* Returns the block attributes parsed from raw content.
*
* @param {String} innerHTML Raw block content
* @param {Object} schema Block attribute schema
* @return {Object} Block attribute values
*/
export function getSourcedAttributes( innerHTML, schema ) {
const sources = mapValues(
// Parse only sources with source defined
pickBy( schema, ( attributeSchema ) => isValidSource( attributeSchema.source ) ),

// Transform to object where source is value
( attributeSchema ) => attributeSchema.source
);

return hpqParse( innerHTML, sources );
}
import { attr, prop, html, text, query, node, children } from './source';

/**
* Returns value coerced to the specified JSON schema type string
Expand Down Expand Up @@ -87,6 +52,67 @@ export function asType( value, type ) {
return value;
}

/**
* Returns an hpq matcher given a source object
*
* @param {Object} sourceConfig Attribute Source object
* @return {Function} hpq Matcher
*/
export function matcherFromSource( sourceConfig ) {
switch ( sourceConfig.source ) {
case 'attribute':
return attr( sourceConfig.selector, sourceConfig.attribute );
case 'property':
return prop( sourceConfig.selector, sourceConfig.property );
case 'html':
return html( sourceConfig.selector );
case 'text':
return text( sourceConfig.selector );
case 'children':
return children( sourceConfig.selector );
case 'node':
return node( sourceConfig.selector );
case 'query':
const subMatchers = mapValues( sourceConfig.query, matcherFromSource );
return query( sourceConfig.selector, subMatchers );
default:
// eslint-disable-next-line no-console
console.error( `Unkown source type "${ sourceConfig.source }"` );
}
}

/**
* Given an attribute key, an attribute's schema, a block's raw content and the commentAttributes
* returns the attribute value depending on its source definition of the given attribute key
*
* @param {string} attributeKey Attribute key
* @param {Object} attributeSchema Attribute's schema
* @param {string} innerHTML Block's raw content
* @param {Object} commentAttributes Block's comment attributes
*
* @return {*} Attribute value
*/
export function getBlockAttribute( attributeKey, attributeSchema, innerHTML, commentAttributes ) {
let value;
switch ( attributeSchema.source ) {
// undefined source means that it's an attribute serialized to the block's "comment"
case undefined:
value = commentAttributes ? commentAttributes[ attributeKey ] : undefined;
break;
case 'attribute':
case 'property':
case 'html':
case 'text':
case 'children':
case 'node':
case 'query':
value = hpqParse( innerHTML, matcherFromSource( attributeSchema ) );
break;
}

return value === undefined ? attributeSchema.default : asType( value, attributeSchema.type );
}

/**
* Returns the block attributes of a registered block node given its type.
*
Expand All @@ -96,57 +122,9 @@ export function asType( value, type ) {
* @return {Object} All block attributes
*/
export function getBlockAttributes( blockType, innerHTML, attributes ) {
// Retrieve additional attributes sourced from content
const sourcedAttributes = getSourcedAttributes(
innerHTML,
blockType.attributes
);

const blockAttributes = reduce( blockType.attributes, ( result, source, key ) => {
let value;
if ( sourcedAttributes.hasOwnProperty( key ) ) {
value = sourcedAttributes[ key ];
} else if ( attributes ) {
value = attributes[ key ];
}

// Return default if attribute value not assigned
if ( undefined === value ) {
// Nest the condition so that constructor coercion never occurs if
// value is undefined and block type doesn't specify default value
if ( 'default' in source ) {
value = source.default;
} else {
return result;
}
}

// Coerce value to specified type
const coercedValue = asType( value, source.type );

if ( 'development' === process.env.NODE_ENV &&
! sourcedAttributes.hasOwnProperty( key ) &&
value !== coercedValue ) {
// Only in case of sourcing attribute from content do we want to
// allow coercion, as comment attributes are serialized respecting
// original data type. In development environments, log if value
// coerced to specified type is not strictly equal. We still allow
// coerced value to be assigned into attributes to avoid errors.
//
// Example:
// Number( 5 ) === 5
// Number( '5' ) !== '5'

// eslint-disable-next-line no-console
console.error(
`Expected attribute "${ key }" of type ${ source.type } for ` +
`block type "${ blockType.name }" but received ${ typeof value }.`
);
}

result[ key ] = coercedValue;
return result;
}, {} );
const blockAttributes = mapValues( blockType.attributes, ( attributeSchema, attributeKey ) => {
return getBlockAttribute( attributeKey, attributeSchema, innerHTML, attributes );
} );

// If the block supports a custom className parse it
if ( blockType.className !== false && attributes && attributes.className ) {
Expand All @@ -170,7 +148,7 @@ export function createBlockWithFallback( name, innerHTML, attributes ) {

// Convert 'core/text' blocks in existing content to the new
// 'core/paragraph'.
if ( name === 'core/text' || name === 'core/cover-text' ) {
if ( 'core/text' === name || 'core/cover-text' === name ) {
name = 'core/paragraph';
}

Expand Down
7 changes: 4 additions & 3 deletions blocks/api/raw-handling/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { equal, deepEqual } from 'assert';
import rawHandler from '../index';
import { registerBlockType, unregisterBlockType, setUnknownTypeHandlerName } from '../../registration';
import { createBlock } from '../../factory';
import { children, prop } from '../../source';

describe( 'rawHandler', () => {
beforeAll( () => {
Expand All @@ -19,7 +18,8 @@ describe( 'rawHandler', () => {
attributes: {
content: {
type: 'array',
source: children( 'figure' ),
source: 'children',
selector: 'figure',
},
},
transforms: {
Expand All @@ -39,7 +39,8 @@ describe( 'rawHandler', () => {
attributes: {
content: {
type: 'string',
source: prop( 'innerHTML' ),
source: 'property',
property: 'innerHTML',
},
},
save: () => {},
Expand Down
3 changes: 2 additions & 1 deletion blocks/api/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,10 @@ export function registerBlockType( name, settings ) {
if ( ! settings.icon ) {
settings.icon = 'block-default';
}

settings = {
name,
attributes: get( window._wpBlocksAttributes, name ),
attributes: get( window._wpBlocksAttributes, name, {} ),
...settings,
};

Expand Down
5 changes: 3 additions & 2 deletions blocks/api/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ export function getCommentAttributes( allAttributes, blockType ) {
return result;
}

// Ignore values sources from content and post meta
if ( attributeSchema.source || attributeSchema.meta ) {
// Ignore all attributes but the ones with an "undefined" source
// "undefined" source refers to attributes saved in the block comment
if ( attributeSchema.source !== undefined ) {
return result;
}

Expand Down
37 changes: 6 additions & 31 deletions blocks/api/source.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,9 @@ import { createElement } from '@wordpress/element';
* External dependencies
*/
import { nodeListToReact, nodeToReact } from 'dom-react';
import { flow } from 'lodash';
import {
attr as originalAttr,
prop as originalProp,
html as originalHtml,
text as originalText,
query as originalQuery,
} from 'hpq';
export { attr, prop, html, text, query } from 'hpq';

/**
* Given a source function creator, returns a new function which applies an
* internal flag to the created source.
*
* @param {Function} fn Original source function creator
* @return {Function} Modified source function creator
*/
function withKnownSourceFlag( fn ) {
return flow( fn, ( source ) => {
source._wpBlocksKnownSource = true;
return source;
} );
}

export const attr = withKnownSourceFlag( originalAttr );
export const prop = withKnownSourceFlag( originalProp );
export const html = withKnownSourceFlag( originalHtml );
export const text = withKnownSourceFlag( originalText );
export const query = withKnownSourceFlag( originalQuery );
export const children = withKnownSourceFlag( ( selector ) => {
export const children = ( selector ) => {
return ( domNode ) => {
let match = domNode;

Expand All @@ -49,8 +23,9 @@ export const children = withKnownSourceFlag( ( selector ) => {

return [];
};
} );
export const node = withKnownSourceFlag( ( selector ) => {
};

export const node = ( selector ) => {
return ( domNode ) => {
let match = domNode;

Expand All @@ -60,4 +35,4 @@ export const node = withKnownSourceFlag( ( selector ) => {

return nodeToReact( match, createElement );
};
} );
};
Loading