While this is not an actual answer to your question "How can I programmatically force the node's layout to be saved?", I wanted to point you to the Layout Builder IPE module, that I have created to solve a similar use-case as yours: Provide editing possibilities directly from the view page of a node.
If this is not an option for you, then the programatic solution probably evolves around:
- hooking into the form submit
- adding your custom submit handler
- grab the block config somehow
- update the OverridesSectionStorage for the node
- save the node
- delete the temporary storage for the layout builder of that node
At least that's what I recall. While working on Layout Builder IPE, I found that whole structure a bit complicated to be honest, which was the main reason to try and get this done once in a more generic way.
Update
Some more details I looked up that might help with this:
To get the sections for a layout builder enabled node you can use LayoutEntityHelperTrait::getSectionStorageForEntity, e.g.:
$section_storage = $this->getSectionStorageForEntity($node);
$sections = $section_storage->getSections();
This $sections
variable is an array of sections, each section containing section components. You can iterate over them until you find the block you are looking for and do what you need to do to update the configuration (untested code):
foreach ($sections as $delta => $section) {
foreach ($section->getComponents() as $component) {
// Each component is an object of type \Drupal\layout_builder\SectionComponent.
// To identify the block you can use the plugin id (or the UUID if
// multiple blocks of the same type exist).
$plugin_id = $component->getPluginId();
if ($plugin_id == 'BLOCK_PLUGIN_ID') {
$configuration = $component->get('configuration');
// Update the configuration, and then update the component.
$component->setConfiguration($configuration);
}
}
}
Once the necessary changes have been made, the sections can be saved back to the node by using this:
$node->get(OverridesSectionStorage::FIELD_NAME)->setValue($sections);
And to clear the tempstore something like this should work, though this should be using DI obviously:
\Drupal::service('layout_builder.tempstore_repository')->delete($section_storage);