Skip to content

Instantly share code, notes, and snippets.

@5ally

5ally/README.md Secret

Created August 31, 2021 18:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 5ally/edaa7a3d788bc73efe1d8196c714327a to your computer and use it in GitHub Desktop.
Save 5ally/edaa7a3d788bc73efe1d8196c714327a to your computer and use it in GitHub Desktop.
get-coached/sandc block - see https://wordpress.stackexchange.com/a/393967/137402 for details

What I did/changed

  1. I added width: 300 to MY_TEMPLATE in index.js
  2. I removed both posts and options from the block type's attributes list
  3. I used getEntityRecords() and getEntityRecord() in place of wp-api
  4. I changed reps and sets to "actual" number, i.e. type is number and source is the block comment delimiter tag. (but source should not be set in the attribute object)
  5. So because they're now actual numbers, I used Number() (to cast the input to a number) when saving the attribute to ensure it's saved properly.
  6. I used the optional chaining operator, e.g. selectedPost?.featured_media
  7. There are other changes, but just see the lines with //<number> like //1.

PS: I didn't reindent the code, but I did remove unwanted whitespaces and lines.

/**
* Retrieves the translation of text.
*
* @see https://developer.wordpress.org/block-editor/packages/packages-i18n/
*/
import { __ } from '@wordpress/i18n';
/**
* React hook that is used to mark the block wrapper element.
* It provides all the necessary props like the class name.
*
* @see https://developer.wordpress.org/block-editor/packages/packages-block-editor/#useBlockProps
*/
import {
InnerBlocks,
useBlockProps,
InspectorControls,
} from '@wordpress/block-editor';
/**
* Other things needed
*
*/
import { SelectControl, TextControl } from '@wordpress/components';
import { Component } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
/**
* Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
* Those files can contain any CSS code that gets applied to the editor.
*
* @see https://www.npmjs.com/package/@wordpress/scripts#using-css
*/
import './editor.scss';
function mySelectPosts({clientId, attributes, setAttributes }) {
// Used to reset inner blocks if we've changed the selected exercise
const { replaceInnerBlocks } = useDispatch("core/block-editor");
const { inner_blocks } = useSelect(select => ({
inner_blocks: select("core/block-editor").getBlocks(clientId)
}));
/**
* Called when the inspector controls select box is changed
* Saves the value of the selected post to the selectedPost attribute
* and resets the innerblock
**/
const onSelectPost = ( post ) => {
// reset the inner
let inner_blocks_new = [];
replaceInnerBlocks(clientId, inner_blocks_new, false);
setAttributes( {
selectedPost: parseInt(post),
} );
};
const posts = useSelect( 'core' ).getEntityRecords( 'postType', 'exercise', { //1
// *no _embed here
// This should always be set to avoid duplicates in the post selection dropdown.
// But of course, you can change the value, e.g. to 20 or, the max value - 100.
per_page: 10,
} );
const selectedPost = useSelect( select => { //2
const { getEntityRecord } = select( 'core' );
const post_id = attributes.selectedPost;
// Fetch the post from the REST API, if we have a valid post ID.
return post_id && getEntityRecord( 'postType', 'exercise', post_id, {
// Request featured media along with the standard post data.
_embed: 'wp:featuredmedia',
// *no per_page here
} );
// This useSelect callback has one dependency - the selected post ID.
}, [ attributes.selectedPost ] );
/* Set of functions for saving out attributes
* reps, sets, notes
*/
const onChangeReps = ( reps ) => {
setAttributes( {
reps: Number( reps ), //3
} );
}
const onChangeSets = ( sets ) => {
setAttributes( {
sets: Number( sets ), //4
} );
}
const onChangeNotes = ( notes ) => {
setAttributes( {
notes: notes,
} );
}
// will contain the text output for the edit.
let output = "";
const blockProps = useBlockProps( {
className: "sandcexercise",
} );
// we've selected a post, so grab the bits from that post to put into the html
if (selectedPost) { //5
// grab the things we are putting in the innerblock from the post
let mediaID = selectedPost?.featured_media || 0; //6
let mediaURL = selectedPost?._embedded?.['wp:featuredmedia'][0]?.source_url || ''; //7
let exerciseLink = selectedPost.link;
// remove any html content from the excerpt as it explodes the inner block :/
let strippedContent = selectedPost.excerpt.rendered.replace(/(<([^>]+)>)/gi, "");
// create a linked heading
let heading = '<a href="' + exerciseLink + '">' + selectedPost.title.rendered + '</a>';
// build the innerblocks template
const MY_TEMPLATE = [
[ 'core/heading', { content: heading } ],
[ 'core/image', { id: mediaID, url: mediaURL, href: exerciseLink, align: "right", sizeSlug: "medium", caption: "Click for instructions", width: 300 } ], //*
[ 'core/paragraph', { content: strippedContent } ]
];
output = <InnerBlocks
template={ MY_TEMPLATE }
templateLock=""
/>
} else {
output = <p>Select an exercise from the right</p>
}
//8 I changed the SelectControl's options - a "loading" message is shown if "posts"
// is yet filled.
// return the edit html.
return [
<div { ...blockProps } key="sandcexercise">
<InspectorControls>
<div id="sandcexercise-controls">
<SelectControl
onChange={ onSelectPost }
value={ attributes.selectedPost }
label={ __( 'Select a Post' ) }
options={posts ? [
...[ { value: 0, label: __( 'Select an exercise' ) } ],
...posts.map( post => ( {
value: post.id,
label: post.title.rendered,
})),
] : [
{ value: 0, label: __( 'Loading the posts list..' ) },
]}
/>
</div>
</InspectorControls>
{output}
<TextControl
{ ...blockProps }
label="Reps:"
type="number"
className="thereps"
onChange={ onChangeReps }
value={ attributes.reps }
/>
<TextControl
{ ...blockProps }
label="Sets:"
type="number"
className="thesets"
onChange={ onChangeSets }
value={ attributes.sets }
/>
<TextControl
{ ...blockProps }
label="Notes:"
type="text"
className="thenotes"
onChange={ onChangeNotes }
value={ attributes.notes }
/>
</div>
]
}
export default mySelectPosts;
/**
* Registers a new block provided a unique name and an object defining its behavior.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
*/
import { registerBlockType} from '@wordpress/blocks';
import {
InnerBlocks,
useBlockProps,
} from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n'; // Import __() from wp.i18n
/**
* Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
* All files containing `style` keyword are bundled together. The code used
* gets applied both to the front of your site and to the editor.
*
* @see https://www.npmjs.com/package/@wordpress/scripts#using-css
*/
import './style.scss';
/**
* Internal dependencies
*/
import Edit from './edit';
/**
* Every block starts by registering a new block type definition.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
*/
registerBlockType('get-coached/sandc', {
title: 'Foo Block', //1
apiVersion: 2,
attributes: {
/*2 don't add this as an attribute - posts: {
type: 'array',
default: [],
},*/
/*3 unused attribute - post: {
type: 'object',
},*/
selectedPost: {
type: 'number',
default: 0
},
/*4 don't add this as an attribute - options: {
type: 'array',
default: [ { value: 0, label: __( 'Select a Post' ) } ],
},*/
reps: {
type: 'number',
//5 source: 'number', //* should've been source: 'text'
//6 selector: '.thereps',
default: 15,
},
sets: {
type: 'number', //7 it's "type" and not "types"
//8 source: 'number', //* should've been source: 'text'
//9 selector: '.thesets',
default: 3,
},
notes: {
type: 'string', //10 was - types: 'text',
source: 'text',
selector: '.thenotes',
default: '',
},
},
edit: Edit,
save: function( props ) {
const blockProps = useBlockProps.save();
const { attributes } = props;
return (
<div { ...blockProps } key="sandcexercise">
<InnerBlocks.Content />
<div className="repsrow"><div className="repslabel">Reps:</div><div className="thereps">{ attributes.reps }</div></div>
<div className="setsrow"><div className="setslabel">Sets:</div><div className="thesets">{ attributes.sets }</div></div>
<div className="notesrow"><div className="noteslabel">Notes:</div><div className="thenotes">{ attributes.notes }</div></div>
</div>
);
},
});
@delanthear
Copy link

Hi. Thanks for this. I'm still seeing two weird issues with this approach to getting the exercise for the dropdown:

  1. I'm still seeing the top result overwritten by the selected result. It's not every time and I can't work out what the trigger is. It just occurred to me that perhaps it's autosave entries in the posts table perhaps?

  2. With "_embed: 'wp:featuredmedia'," removed from the posts use select call, I don't get the images outputting in the innerblocks template, instead seeing the placeholder. This I don't understand at all because the url is being called from the selectedPost select so it should have nothing to do it the previous one! boggle If I put it back in the posts select, they work again!

@5ally
Copy link
Author

5ally commented Sep 1, 2021

@delanthear Honestly, I had yet finalized the getEntityRecord(s) parts and I removed the _embed because the single request is also requesting the featured media data, hence I thought posts would not need the _embed parameter.. but it did come across my mind that those issues in your reply (particularly the 2nd one) would happen, and I guessed it's because the getEntityRecord(s) caches the responses.

So perhaps for now just continue using wp-api and apply the other changes manually on your code (e.g. #4, #5 and #6 in the README.md file)?

But actually I got another version which uses wp-api, but posts is stored as a local state and not block attribute. I can share the code, but right now I'm using another device and far from the other one.

@5ally
Copy link
Author

5ally commented Sep 1, 2021

Or maybe you can try swapping the getEntityRecord() and getEntityRecords() calls, i.e. define selectedPost above/before the posts? That might fix both those issues.

@delanthear
Copy link

Sorry, been away for a couple of days! Right, swapping the calls around so selectedPost is defined before posts works. Both still need to have the _embed line though.

I have NO IDEA why this works though, which is annoying, therefore it feels like there is something wrong somewhere :D I don't think I@m going to figure it out though so this will have to do!

@tomjn
Copy link

tomjn commented Sep 8, 2021

This code commits the cardinal sin of putting InnerBlocks alongside other components. This is the best way to get your block to fail validation.

Blocks that contain other blocks cannot put additional elements alongside. It needs to be on its own inside an element

Good:

<div>
    <innerblocks/>
</div>
<things>

Bad:

<innerblocks/>
<things>

Inner blocks is always an only child! It never ever shares its container with other elements or containers. No exceptions.

@delanthear
Copy link

@tomjn so replacing:

</InspectorControls>
{output}
<TextControl

with

</InspectorControls>
<div>
{output}
</div>
<TextControl

is advised? Thanks!

@tomjn
Copy link

tomjn commented Sep 8, 2021

Yes, though I don't like how you're conditionally showing the inner blocks component. It implies that you can change the template to change the inner blocks which is not true.

Also, the way the heading is being created by building HTML out of strings is dangerous, and unnecessary, just use JSX like the rest of the code

@delanthear
Copy link

delanthear commented Sep 8, 2021

Yes, though I don't like how you're conditionally showing the inner blocks component. It implies that you can change the template to change the inner blocks which is not true.

The innerblocks change depending on which exercise you've selected from the selectControl within the InspectorControls. If you've not selected one, I'm displaying a prompt to go and select an exercise. Then the innerblocks is outputting. Is this a bad approach?

Also, the way the heading is being created by building HTML out of strings is dangerous, and unnecessary, just use JSX like the rest of the code

I just Googled JSX.....
let heading = '<a href="' + exerciseLink + '">' + selectedPost.title.rendered + '</a>';

should be
let heading = <a href={ exerciseLink }>{ selectedPost.title.rendered }</a>;

Doing this though means I can't do this further down:
[ 'core/heading', { content: heading ],

as heading is no longer a string, but react object. I can't work out how to output it into the template from this :/

Apologies if this is all schoolboy stuff but the last time I used Javascript was in probably 2004 to make things blink and popup! :D Trying to build a Gutenberg block has been somewhat of a ride....

@tomjn
Copy link

tomjn commented Sep 8, 2021

I see, generally this kind of situation is solved by never being in that situation in the first place

Then the innerblocks is outputting. Is this a bad approach?

I see, though this is the first time I've encountered it. Usually the inner blocks or block is a custom block that has a heading/image/richtext field, or at least in the blocks I've seen.

@tomjn
Copy link

tomjn commented Sep 8, 2021

Additionally, the best practice depends on what you're trying to do which isn't clear.

For reference I keep losing track of the WPSE question that lead me here:

https://wordpress.stackexchange.com/questions/395364/output-content-of-post-excerpt-into-innerblocks-within-gutenberg-block

@tomjn
Copy link

tomjn commented Sep 8, 2021

Something people forget is that when they're building something they get lost in the technical details, and ask about them. They forget that me and other people reading it have no idea what it was they were trying to build in the first place and have to reverse engineer that. it's all the more frustrating when you ask someone why they want an articulating hinge to open more than 60 degrees and they respond with "I needed rotational motion to move the plank of wood, do you know why it won't move to 70 degrees?" when what I actually needed was "I'm trying to build a door for my house"

From what I can tell this is for a workout program post type, and you have a block called sadc which you use to say "do Squats, 5 times, at 4 reps". You can then pick an exercise post type ( e.g. the "squats" post ) and it shows some info. For some reason this information iis copied from the squats post and turned into raw content placed inside as nested blocks, but I'm not sure why. Why not create an exercise block that displays info about an exercise post? Or just have a component that displays this information when given an ID ( you don't need to store anything more than the post ID, just fetch the data in JS in the edit component, or render it dynamically on the frontend )

@delanthear
Copy link

I'm not following what you are suggesting here. because what you're saying I should do, I think I'm actually doing!

We might have 30 different exercises defined, which will have lots of content on them, videos, progressions, tips, comments etc. They exist as a generic library which will grow. This block is a way to pull out a small subset of that information into another post type. In the typical use case (but could be others), it will be when a coach wants to create a specific Strength and Conditioning Plan for a person. They will add this block 3 or 4 times, select the required exercises and add some notes, sets and reps. The user can then view their own plan, see what they have to do, and if they want to know more, click the title of the exercise and see all the additional generic information about that exercise.

It might be better to pull this information dynamically when it's viewed, rather than "copying" it I guess, but how would that work? Can you do that within a published Gutenberg block?

@tomjn
Copy link

tomjn commented Sep 9, 2021

I think I'm actually doing!

You're not, you're copying the data into the post as paragraphs/image/heading blocks, no embedding or displaying is occurring. E.g. if the original post changes none of these blocks would change

You don't need to create inner/nested blocks to display a post from a saved ID.

It might be better to pull this information dynamically when it's viewed, rather than "copying" it I guess,

Yes

but how would that work? Can you do that within a published Gutenberg block?

Yes, the recent posts block does it, and there are many ways to do it, just as there are many ways a shortcode could do it. Maybe you render the block server side in PHP? Maybe you fetch it in JS? Maybe your inner block isn't headings and paragraph blocks but a custom block that represents that exercise? There are lots of ways to do it, very few of them rely on special Gutenberg sauce to do it. Why would it be any different to the way it worked for shortcodes? You could even do a literal embed of the post and use a customized embed HTML via the embed filters for that post type. There are lots and lots of ways to do it.

@delanthear
Copy link

I've approached it as a Gutenberg block because

  1. The person who writes the plans needs a quick way to get post out multiple plans using lots of different exercises and then will be customising the output to see fit for each person. Having the details come out as Blocks elements seemed sensible for that
  2. The approach WP seems to be going seems to be do due everything in via Blocks and this seemed to be the right way to approach it based on reading as many guides and documentation as I could get my head around :)

On reflection, copying the data is important because the plan is a point in time thing. I.e. you don't want the plan to change because the exercise does and it will have customisations done by the author of the plan as it's created, so it changing dynamically might make that stop making sense, or be confusing when you review later on.

Am I right in thinking that if I want the author of the plan to be able to tweak the heading that's been outputting, that will need to come put as a heading block, and therefore innerblocks is the right way to do it? Or is this a fundamental misunderstanding I have?

@tomjn
Copy link

tomjn commented Sep 9, 2021

Am I right in thinking that if I want the author of the plan to be able to tweak the heading that's been outputting, that will need to come put as a heading block, and therefore innerblocks is the right way to do it? Or is this a fundamental misunderstanding I have?

I think it's a miscommunicated requirement, though I imagine that's what the notes section of the block was for? A squat is still a squat regardless of the person. You coulld allow them to add paragraphs underneath etc. Though if your excerpt contains HTML tags then I don't see how that means it can't be turned into a paragraph tag, paragraphs have HTML tags too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment