Custom Blocks Volto

Hi, I'm attempting to create a custom block following this documentation

I've setup my Edit.jsx and View.jsx

Edit.jsx-

import React from 'react';

const Edit = props => {
  return <div>I'm the MainSlider edit component!</div>;
};

export default Edit;

View.jsx

import React from 'react';

const View = props => {
  return <div>I'm the MainSlider view component!</div>;
};

export default View;

Added and exported them from index.js


import TalkView from "./Views/Talk";
import TalkListView from "./Views/TalkList";
import NewKnowledgeView from "./Views/NewKnowledge";
import PrimaryAuthorWidget from "./Widgets/KnowledgeWidgets/PrimaryAuthor";
import IndustryWidget from "./Widgets/KnowledgeWidgets/Industry";
import MarketWidget from "./Widgets/KnowledgeWidgets/Market";
import AddView from "./Views/Add";

import Edit from "./Blocks/MainSlider/Edit";
import View from "./Blocks/MainSlider/View";


export {TalkView, TalkListView, NewKnowledgeView, PrimaryAuthorWidget, IndustryWidget,  MarketWidget, AddView, Edit, View};
/**
 * Add your components here.
 * @module components
 * @example
 * import Footer from './Footer/Footer';
 *
 * export {
 *   Footer,
 * };
 */

My config.js looks like this -

import {Edit, View} from './components';
...
export default function applyConfig(config) {
  let custom_block = {
    id: 'mainslider',
    title: 'Main Slider',
    icon: titleSVG,
    group: 'common',
    view: View,
    edit: Edit,
    restricted: false,
    mostUsed: true,
    security: {
      addPermission: [],
      view: [],
    }
  };
config.blocks.blocksConfig.MainSlider = custom_block;
return config;
}

It looks like Volto is picking up that I've added a custom block, when I go to use it in the UI though, this is what ends up happening -

image

Have I missed some configuration?

Thanks!

Edit -

Seeing this in the console on page load

Warning: Failed prop type: The prop `onMutateBlock` is marked as required in `BlockChooser`, but its value is `undefined`.
    in BlockChooser (created by Context.Consumer)
    in injectIntl(BlockChooser) (at Edit.jsx:335)
    in Edit (created by Context.Consumer)
    in injectIntl(Edit) (at Edit.jsx:168)
    in div (at Edit.jsx:136)
    in Edit (created by Connect(Edit))
    in Connect(Edit) (at ObjectBrowser.jsx:83)
    in _class (created by Context.Consumer)
    in injectIntl(_class) (at BlocksForm.jsx:206)
    in div (at EditBlockWrapper.jsx:50)
    in div (at EditBlockWrapper.jsx:39)
    in div (at EditBlockWrapper.jsx:34)
    in EditBlockWrapper (created by Context.Consumer)
    in injectIntl(EditBlockWrapper) (at BlocksForm.jsx:146)
    in Draggable (created by ConnectFunction)
    in ConnectFunction (created by PrivateDraggable)
    in PrivateDraggable (created by PublicDraggable)
    in PublicDraggable (at DragDropList.jsx:115)
    in div (at DragDropList.jsx:106)
    in Droppable (created by ConnectFunction)
    in ConnectFunction
    in ConnectFunction (at DragDropList.jsx:104)
    in Provider (created by App)
    in App (created by ErrorBoundary)
    in ErrorBoundary (created by DragDropContext)
    in DragDropContext (at DragDropList.jsx:96)
    in DragDropList (at BlocksForm.jsx:156)
    in fieldset (at BlocksForm.jsx:155)
    in div (at BlocksForm.jsx:154)
    in BlocksForm (at Form.jsx:523)
    in div (at Form.jsx:505)
    in Form (created by Context.Consumer)
    in injectIntl(Form) (created by ForwardRef)
    in ForwardRef (at Add.jsx:297)
    in div (at Add.jsx:291)
    in Add (at Loadable.js:87)
    in PreloadLoadables(cms)(Add) (at Loadable.js:98)
    in ForwardRef (created by Connect(Component))
    in Connect(Component) (created by Context.Consumer)
    in injectIntl(Connect(Component)) (created by DragDropContext(injectIntl(Connect(Component))))
    in DragDropContext(injectIntl(Connect(Component))) (created by Context.Consumer)
    in Route (created by App)
    in Switch (created by App)
    in main (at App.jsx:138)
    in div (created by Segment)
    in Segment (at App.jsx:137)
    in MultilingualRedirector (at App.jsx:136)
    in PluggablesProvider (at App.jsx:109)
    in App (created by Connect(App))
    in Connect(App) (created by Connect(Connect(App)))
    in Connect(Connect(App)) (created by Context.Consumer)
    in Route (created by Context.Consumer)
    in Switch (created by Context.Consumer)
    in Route (at AsyncConnect.jsx:89)
    in AsyncConnect (at AsyncConnect.jsx:129)
    in AsyncConnectWithContext (created by Context.Consumer)
    in withRouter(AsyncConnectWithContext) (created by Connect(withRouter(AsyncConnectWithContext)))
    in Connect(withRouter(AsyncConnectWithContext)) (at start-client.jsx:56)
    in ScrollToTop (created by Context.Consumer)
    in withRouter(ScrollToTop) (at start-client.jsx:55)
    in Router (created by ConnectedRouter)
    in ConnectedRouter (created by Context.Consumer)
    in ConnectedRouterWithContext (created by Connect(ConnectedRouterWithContext))
    in Connect(ConnectedRouterWithContext) (at start-client.jsx:54)
    in IntlProvider (created by Connect(IntlProvider))
    in Connect(IntlProvider) (at start-client.jsx:53)
    in Provider (at start-client.jsx:52)

Your code seems to be OK. Did you change

config.blocks.blocksConfig.MainSlider = custom_block;

between creating a block of this type 'MainSlider' and viewing the page? Uppercase/lowercase matters.

You may want to read my mini tutorial on custom Volto blocks:
https://training.plone.org/5/mastering-plone/volto_custom_addon2.html
and Tiberius elaborated training:
https://training.plone.org/5/voltoaddons/01-addon-basics.html#create-a-new-block

Your block id should manage the one in blocksConfig.

Basically, instead of blocksConfig.MainSlider you should do blocksConfig.mainslider = ...

Thank you both!

With those changes I was able to get everything to work.

I had one other question with this I was hoping you could point me to some documentation for, let's say I have a custom content type in dexterity

When I'm adding an instance of that content type in Volto it shows all the fields for the content type

image

Is there a way to do data-binding on a custom block so that, for example, if someone types something into an input, it sets/saves that as the value on the content type, or is that something I would need to look at the rest api for?

For example -

If I have a select, it looks like I can set the
this.props.properties.my_custom_property = selectedOption.value;

    handleChange = (selectedOption) => {
        this.setState({ selectedOption });
        this.props.properties.primary_author = selectedOption.value;
        //this.props.onChange(this.props.id, selectedOption.value);
      };

Is there a way to persist that change to the Plone db?

You'll find this useful: GitHub - eea/volto-metadata-block: Metadata Block Volto add-on that enables Document metadata insertion within the Blocks area.

Also, be aware that you can create a customized "default initial blocks" per content type if you go in Volto site setup > dexterity types > pick a type > choose "Layout" in the context menu and then you get to a form where you "compose" what the blocks editing page would look like for a content type.

Once you get past this stage you may want to customize, per content type, how a block renders. My go-to method would be: create the content type view but inside it render the blocks using the RenderBlocks component. To this component you can pass a custom blocksConfig, so imagine that you'd have something like this (pseudo-code-ish) to override how the title block is rendered:

import RenderBlocks from '@plone/volto/components/theme/view/RenderBlocks';
import config from '@plone/volto/registry';

const MyCustomTypeView = (props) => {
    const {blocksConfig} = config.blocks;
    const customBlocksConfig = {...blocksConfig, title: {...blocksConfig.title, view: (props) => <div>I'm a custom title</div>}}
    return <RenderBlocks {...props } blocksConfig={customBlocksConfig} />
}

Thanks!! I'll take a look at those links they look like they will be very helpful,

One more question along the lines of how blocks work.

As far as creating an instance of a content type in Volto -
I could do this by making a bunch of inputs, capturing the data on the inputs and sending a post request to http://localhost:8080/Plone/knowledge with the data for my content type

function submitKnowledge(){
    let topic_name = document.getElementById("topic_name_input").value;
    let primary_author = document.getElementById("primary_author_select").value;
    // api call here
    alert(`Submitting knowledge topic name: ${topic_name} Primary Author: ${primary_author}`);
}
render() {
        console.log("render happening");
        return (
            <div>
                <Form>
                    <input type="text" placeholder="Enter topic name..." id="topic_name_input"></input>
                    <Field id="primary_author_select" widget="primary_author" title="Prim Author" />
                    <Field id="secondary_author_select" widget="primary_author" title="Secondary Author" />
                    <input type="text" placeholder="Enter any external contributors..." id="extern_contributors_input"></input>
                    <Field id="industry_select" widget="industry" title="Industry" />
                    <Field id="service_select" widget="dhg_service" title="Service" />
                    <button onClick={() => submitKnowledge()}>Submit</button>
                </Form>
            </div>
        );
    }

But let's say that I have someone who isn't a programmer someone we just want to use the CMS from the UI, is it possible for them to design an input form with blocks and when a client goes to the form they've designed and submits, it creates an instance of a content type?

GitHub - collective/volto-form-block: Volto addon for a customizable form block provides a TTW method to build forms, as well as GitHub - kitconcept/volto-form-builder: An interactive form builder for Volto, that allows editors to assemble forms via drag and drop.. I've used the first one, and it's more geared toward building a custom contact form. They both need Plone Python backend addons. For sure something could be hacked together, basically you'll need the Python counterpart. IMHO the Plone "submit endpoint" for a form builder should not do anything in particular, just trigger an content-rules enabled event that can then be used to build a custom engine based on the content rules.