getblocklab / block-lab

A WordPress Admin interface and a simple templating system for building custom Gutenberg blocks.
https://getblocklab.com
GNU General Public License v2.0
416 stars 63 forks source link

Block builder API #400

Closed lukecarbis closed 5 years ago

lukecarbis commented 5 years ago

A feature request I hear a lot is a way of programatically adding new blocks, using PHP to define them.

For example:

block_lab()
    ->new_block( 'Team Member', 'team-member' )
        ->add_field( 'Name', 'name', 'text' )
        ->add_field( 'Position', 'position', 'text' )
        ->add_field( 'Photo', 'photo', 'image' )
        ->add_field( 'Twitter', 'twitter', 'url' );

Inspired by this and this.

kienstra commented 5 years ago

Yeah, that's a good idea. I've heard requests for this also.

lukecarbis commented 5 years ago

In terms of how a PHP API should be structured, here are some options.

  1. Helper functions, with config array.

ACF uses a single large configuration array, and provides two helper functions. Here's an example of how this might look in Block Lab.

block_lab_add_block(
    array(
        'name' => 'example-block',
        'title' => __( 'Example Block' ),
        'icon' => 'sentiment_satisfied_alt',
        'category' => 'common',
        'keywords => array(
            'foo',
            'bar',
        ),
        fields = array(
            array(
                'name' => 'first',
                'label' => __( 'First Name' ),
                'control' => 'text',
                'help' => __( 'Type your first name here' ),
            ),
        ),
    )
);
block_lab_add_field(
    array(
        'block' => 'example-block',
        'name' => 'last',
        'label' => __( 'Last Name' ),
        'control' => 'text',
    )
);
  1. Class based, with config array.

Gravity Forms uses a similar approach, but with a class (GFAPI), and instead of a configuration array, it requests an instantiated Form object. Translating this to Block Lab, it would look something like:

$block = new Block();

$block->from_array(
    'name' => 'example-block',
    'title' => __( 'Example Block' ),
    'icon' => 'sentiment_satisfied_alt',
    'category' => 'common',
    'keywords => array(
        'foo',
        'bar',
    ),
    'fields' => array(
        'first' => array(
            'name' => 'first',
            'label' => __( 'First Name' ),
            'control' => 'text,
            'type' => 'string',
            'order' => 1,
        ),
    ),
);

block_lab()->api->register_block( $block );

block_lab()->api->register_field(
    $block,
    array(
        'name' => 'last',
        'label' => __( 'Last Name' ),
        'control' => 'text,
        'type' => 'string',
        'order' => 2,
    ),
);
  1. Filter based.

The method above actually nearly works as is. It's just the block_lab()->api() part that doesn't yet exist (there's nothing stopping you from actually instantiating a block using the method above right now). Given this, all we would need to do, technically, is add a filter to Blocks\Loader->retrieve_blocks().

function add_my_example_block( $blocks ) {
    $example_block = new Block();

    $example_block->from_array(
        'name' => 'example-block',
        'title' => __( 'Example Block' ),
        'icon' => 'sentiment_satisfied_alt',
        'category' => 'common',
        'keywords => array(
            'foo',
            'bar',
        ),
        'fields' => array(
            'first' => array(
                'name' => 'first',
                'label' => __( 'First Name' ),
                'control' => 'text,
                'type' => 'string',
                'order' => 1,
            ),
            'last' => array(
                'name' => 'last',
                'label' => __( 'Last Name' ),
                'control' => 'text,
                'type' => 'string',
                'order' => 1,
            ),
        ),
    );

    $blocks[] = $example_block;
    return $blocks;
}
add_filter( 'block_lab_blocks', 'add_my_example_block' );

Just in case it isn't clear, this third method literally requires a single line of code to be added.

  1. The "Return Self" config generation method.

ACF Builder is a custom PHP API for quickly building out ACF field groups. People like it because it is very descriptive. It could work alongside any of the approaches above, as all it does is outputs a config array.

One of the key aspects of this approach is that the Builder would make opinionated assumptions for default values. For example, the block slug, if not specified, would be auto-generated.

Here's how a similar approach could work with Block Lab:

// You could optionally provide a config array for icon, category, etc. as the second argument.
$example_block = new block_lab()->builder( __( 'Example Block' ) );
$example_block
    ->add_text( __( 'First Name' ) )
    ->add_text( __( 'Last Name' ), array( 'name => 'last' ) )
    ->add_image( __( 'Profile Photo' ) )
    ->add_URL( __( 'Website' ) );

// We could use this with option 1 (Helper functions, with config array).
block_lab_add_block( $example_block->build() );

// Or we could use this with option 2 (Class based, with config array).
block_lab()->api->register_block( $example_block->build() );

// Or with option 3 (Filter based).
add_filter( 'block_lab_blocks', function( $blocks ) {
    // In this case we could have the build method return an instantiated Block.
    $blocks[] = $example_block->build();
} );

// Or just on its own.
$example_block->register();

Thanks for reading! Which do you think is the best path forward?

RobStino commented 5 years ago

I'm more familiar with the style of Option 1, so I'd lean that way, but it comes down to which is better really. My experience is limited. :) I do like the idea of the Builder alongside. It would be interesting to get insight on how widely used/appreciated that method is.

lukecarbis commented 5 years ago

@RobStino What about if it was just the builder, standalone? No helper functions. Would work like this:

$example_block = new block_lab()->builder(
    __( 'Example Block' ),
    array( 'category' => 'formatting' )
);
$example_block
    ->add_text( __( 'First Name' ) )
    ->add_text( __( 'Last Name' ), array( 'name => 'last' ) )
    ->add_image( __( 'Profile Photo' ) )
    ->add_url( __( 'Website' ) )
    ->register();

Or do you prefer the helper functions:

$example_block = new block_lab()->builder(
    __( 'Example Block' ),
    array( 'category' => 'formatting' )
);
$example_block
    ->add_text( __( 'First Name' ) )
    ->add_text( __( 'Last Name' ), array( 'name => 'last' ) )
    ->add_image( __( 'Profile Photo' ) )
    ->add_url( __( 'Website' ) );

block_lab_add_block( $example_block->build() );
kienstra commented 5 years ago

Hi @lukecarbis, Wow, what a great writeup!

Option 1 (helper function) looks really good to me, and maybe option 4 after that.

I think option 1 would be the most similar to existing 'registration' functions in WordPress:

Options 2 and 3 aren't bad, but they expose internals, like the Block class:

$block = new Block_Lab\BlocksBlock();

...and its method from_array().

RobStino commented 5 years ago

@lukecarbis I think I like the helper functions. But I could learn new things if they're considered better. :) It probably comes down to:

marsjaninzmarsa commented 4 years ago

Late on the party, but besides ACF they're plugins like @CMB2 if you're looking for nice API for inspiration/reference. 😎

kienstra commented 4 years ago

Hi @marsjaninzmarsa, Thanks for checking this out 😄

Block Lab now has a PHP API for registering blocks: https://github.com/getblocklab/block-lab/pull/434#issue-320112300