I feel like I'm so close to just wrapping up this custom Gutenberg block.
What works:
- Adding new block
- Editing block / adding custom inner blocks
- Saving block
- Viewing block on frontend
What's broken:
- After saving block, if I refresh the editor page, the InnerBlock components are missing
code example
Essentially, the InnerBlock data is being saved but is not being rendered in the backend after page refresh. I've been staring at this documentation for so long, there's not many resources for coding in just plain JavaScript for InnerBlocks.
Edited code snippets on 26 August to include attributes and my latest attempts at resolving the problem.
I am using plain JavaScript, not JSX!
Parent Script
(function(blocks, element, blockEditor) {
const { registerBlockType } = blocks;
const el = element.createElement;
const useBlockProps = blockEditor.useBlockProps;
const InnerBlocks = blockEditor.InnerBlocks;
const useInnerBlocksProps = blockEditor.useInnerBlocksProps;
registerBlockType('aurise/video', {
title: 'Video',
description: 'Lorem ipsum',
category: 'aurise', //[ text | media | design | widgets | theme | embed ]
/* Block Settings */
attributes: {
//About the Video
id: {
type: 'string',
default: ''
},
title: {
type: 'string',
default: ''
},
width: {
type: 'integer',
default: 1280
},
height: {
type: 'integer'
},
poster_url: {
type: 'string',
default: ''
},
upload_date: {
type: 'string',
default: ''
},
duration: {
type: 'string',
default: '',
},
autoplay: {
type: 'string',
default: ''
},
captions: {
type: 'string',
default: 'on'
},
//About the Author
author_name: {
type: 'string',
default: ''
}
},
edit: function(props, setAttributes, className) {
function update_video_id(event) {
props.setAttributes({ id: event.target.value });
};
function update_title(event) {
props.setAttributes({ title: event.target.value });
};
function update_width(event) {
props.setAttributes({ width: event.target.value });
};
function update_height(event) {
props.setAttributes({ height: event.target.value });
};
function update_poster_url(event) {
props.setAttributes({ poster_url: event.target.value });
};
function update_upload_date(event) {
props.setAttributes({ upload_date: event.target.value });
};
function update_duration(event) {
props.setAttributes({ duration: event.target.value });
};
function update_autoplay(event) {
if (event.target.checked) {
props.setAttributes({ autoplay: 'on' });
} else {
props.setAttributes({ autoplay: '' });
}
};
function update_captions(event) {
if (event.target.checked) {
props.setAttributes({ captions: 'on' });
} else {
props.setAttributes({ captions: '' });
}
};
function update_author_name(event) {
props.setAttributes({ author_name: event.target.value });
};
let blockProps = useBlockProps(),
innerBlockProps = useInnerBlocksProps(blockProps),
output = el(
//Block Header
'div', { className: 'au-video au-video-youtube' },
el(
'h3',
null,
'Embedded YouTube Video'
),
//Row with Video ID and Title
el(
'div', { className: 'au-row' },
//Video ID
el(
'label', { className: 'col-xs-12 col-md-6' },
el(
'span', { className: 'au-input-label' },
'Video ID'
),
el('input', {
type: 'text',
required: 'required',
value: props.attributes.id,
onChange: update_video_id
})
),
//Video Title
el(
'label', { className: 'col-xs-12 col-md-6' },
el(
'span', { className: 'au-input-label' },
'Title'
),
el('input', {
type: 'text',
value: props.attributes.title,
onChange: update_title
})
)
),
//Row with video's height and width
el(
'div', { className: 'au-row' },
//Video Width
el(
'label', { className: 'col-xs-12 col-md-6' },
el(
'span', { className: 'au-input-label' },
'Width (in pixels)'
),
el('input', {
type: 'number',
required: 'required',
step: 1,
min: 0,
placeholder: 1280,
value: props.attributes.width,
onChange: update_width
})
),
//Video Height
el(
'label', { className: 'col-xs-12 col-md-6' },
el(
'span', { className: 'au-input-label' },
'Height (in pixels)'
),
el('input', {
type: 'number',
step: 1,
min: 0,
placeholder: 720,
value: props.attributes.height,
onChange: update_height
})
)
),
//Row with video's upload date and duration
el(
'div', { className: 'au-row' },
//Duration
el(
'label', { className: 'col-xs-12 col-md-6' },
el(
'span', { className: 'au-input-label' },
'Total Video Duration (in seconds)'
),
el('input', {
type: 'text',
placeholder: '2640',
value: props.attributes.duration,
onChange: update_duration
})
),
//Upload Date
el(
'label', { className: 'col-xs-12 col-md-6' },
el(
'span', { className: 'au-input-label' },
'Date video was Uploaded'
),
el('input', {
type: 'date',
value: props.attributes.upload_date,
onChange: update_upload_date
})
)
),
//Row with poster image
el(
'div', { className: 'au-row' },
//Poster Image
el(
'label', { className: 'col-xs-12' },
el(
'span', { className: 'au-input-label' },
'Poster Image (insert from URL)'
),
el('input', {
type: 'text',
value: props.attributes.poster_url,
placeholder: 'https://via.placeholder.com/1280x720',
onChange: update_poster_url
})
)
),
//Row with autoplay & caption checkboxes
el(
'div', { className: 'au-row' },
//Autoplay
el(
'label', { className: 'col-xs-12 col-md-6' },
el('input', {
type: 'checkbox',
value: 'autoplay',
checked: props.attributes.autoplay ? 'checked' : '',
onChange: update_autoplay
}),
el(
'span', { className: 'au-input-label' },
'autoplay video'
)
),
//Captions
el(
'label', { className: 'col-xs-12 col-md-6' },
el('input', {
type: 'checkbox',
value: 'closed-captions',
checked: props.attributes.captions ? 'checked' : '',
onChange: update_captions
}),
el(
'span', { className: 'au-input-label' },
'display closed captions by default if available'
)
)
),
//Row with author
el(
'div', { className: 'au-row' },
//Author
el(
'label', { className: 'col-xs-12 col-md-6' },
el(
'span', { className: 'au-input-label' },
'Name of Video\'s Author'
),
el('input', {
type: 'text',
value: props.attributes.author_name,
onChange: update_author_name
})
)
),
//Row with video clips
el('div', { className: 'au-video-clips' },
el(
'h4',
null,
'Video Clips'
),
el('div', innerBlockProps, el(InnerBlocks, { allowedBlocks: ['aurise/videoclip'] }))
)
);
return output;
},
save: function(props) {
let blockProps = useBlockProps.save(),
innerBlockProps = useInnerBlocksProps.save(blockProps),
output = '[au_youtube'; //Output the shortcode using the property attributes
for (property in props.attributes) {
output += ' ' + property + '="' + props.attributes[property] + '"';
}
output += ']';
output += el('div', blockProps, el('div', innerBlockProps, el(InnerBlocks.Content))); //Outputs: [object Object] in code editor
output += '[/au_youtube]'; //Close parent shortcode
console.info('[YOUTUBE VIDEO] Properties', props);
console.info('[YOUTUBE VIDEO] Block Props', blockProps);
console.info('[YOUTUBE VIDEO] Inner Block Props', innerBlockProps);
console.info('[YOUTUBE VIDEO] Inner Content', InnerBlocks.Content);
console.info('[YOUTUBE VIDEO] Output', output);
return output;
}
});
})(
window.wp.blocks,
window.wp.element,
window.wp.blockEditor
);
And then here's the custom child block code
(function(blocks, element, blockEditor) {
const { registerBlockType } = blocks;
const el = element.createElement;
const useBlockProps = blockEditor.useBlockProps;
const useInnerBlocksProps = blockEditor.useInnerBlocksProps;
registerBlockType('aurise/videoclip', {
parent: ['aurise/video'],
title: 'Video Clip',
description: 'Lorem ipsum',
category: 'aurise', //[ text | media | design | widgets | theme | embed ]
/* Block Settings */
attributes: {
name: {
type: 'string',
default: ''
},
description: {
type: 'string',
default: ''
},
startOffset: {
type: 'integer',
default: 0
},
endOffset: {
type: 'integer',
default: 0
}
},
edit: function(props) {
function update_name(event) {
props.setAttributes({ name: event.target.value });
};
function update_description(event) {
props.setAttributes({ description: event.target.value });
};
function update_start(event) {
props.setAttributes({ startOffset: event.target.value });
};
function update_end(event) {
props.setAttributes({ endOffset: event.target.value });
};
let blockProps = useBlockProps(),
innerBlockProps = useInnerBlocksProps(blockProps),
output = el(
'div', { className: 'au-video au-video-clip' },
el(
'h3',
null,
'Video Clip'
),
//Row with Name and Description
el(
'div', { className: 'au-row' },
//Clip Name
el(
'label', { className: 'col-xs-12 col-md-6' },
el(
'span', { className: 'au-input-label' },
'Name'
),
el('input', {
type: 'text',
required: 'required',
value: props.attributes.name,
onChange: update_name
})
),
//Clip Description
el(
'label', { className: 'col-xs-12 col-md-6' },
el(
'span', { className: 'au-input-label' },
'Description'
),
el('input', {
type: 'text',
value: props.attributes.description,
onChange: update_description
})
)
),
//Row with Start and End offsets
el(
'div', { className: 'au-row' },
//Clip Start Offset
el(
'label', { className: 'col-xs-12 col-md-6' },
el(
'span', { className: 'au-input-label' },
'Start Time (in seconds)'
),
el('input', {
type: 'number',
required: 'required',
step: 1,
min: 0,
placeholder: 20,
value: props.attributes.startOffset,
onChange: update_start
})
),
//Clip End Offset
el(
'label', { className: 'col-xs-12 col-md-6' },
el(
'span', { className: 'au-input-label' },
'End Time (in seconds)'
),
el('input', {
type: 'number',
required: 'required',
step: 1,
min: 0,
placeholder: 25,
value: props.attributes.endOffset,
onChange: update_end
})
)
)
);
return output;
},
save: function(props) {
let blockProps = useBlockProps.save(),
innerBlockProps = useInnerBlocksProps.save(blockProps),
output = '[au_video_clip'; //Open shortcode
//Output the shortcode using the property attributes
for (property in props.attributes) {
output += ' ' + property + '="' + props.attributes[property] + '"';
}
output += ' from-clip="true"]'; //Close shortcode
console.info('[VIDEO CLIP] Properties', props);
console.info('[VIDEO CLIP] Block Props', blockProps);
console.info('[VIDEO CLIP] Inner Block Props', innerBlockProps);
console.info('[VIDEO CLIP] Output', output);
return output;
}
});
})(
window.wp.blocks,
window.wp.element,
window.wp.blockEditor
);
As far as I understand, the save function in the child block isn't actually being hit in the backend, which is why I rendered the shortcode using the props in the parent. But I think that's hacky and unsure what to do? Like... it seems the data is being saved and shows up on the frontend just fine, but I can't edit it since they don't load up on the backend.
By adding innerBlockProps = useInnerBlocksProps.save(blockProps)
to the parent block's save function, I can finally see the console info lines from the child showing up in the log so I can see it's actually triggering that now. I've removed my hacky bit to use InnerBlocks.Content
as documentation says, but it still outputs [object Object]
in the editor instead of what the child block's output which is a string, wouldn't matter if it was a shortcode or not. I also added it to the child block's save function thinking maybe that'll do it?? At any rate, I think the solution is somewhere between the child's save function and the parent's edit and/or save function.
Any help is appreciated!