30. Creating a custom block#
Creating a new block type
We want to provide some information for speakers of the conference: Which topics are possible? What do I have to consider speaking at an online conference? FAQ section would come in handy. This could be done by creating a block type that offers a form for question and answer pairs and displays an accordion.
Let's start with our fresh add-on we created in the last chapter Extending Volto with a custom add-on package.
We need a view and an edit form for the block. Create a src/FAQ/BlockView.jsx
and src/FAQ/BlockEdit.jsx
.
The BlockView is a simple function component that displays a FAQ component with the data stored on the block.
1import React from 'react';
2import FAQ from './FAQ';
3
4const View = ({ data }) => {
5 return (
6 <div className="block faq">
7 <FAQ data={data} />
8 </div>
9 );
10};
11
12export default View;
We outsource the FAQ component to file srch/FAQ/FAQ.jsx
and make heavy use of Semantic UI components especially of an accordion with its respective behavior of expanding and collapsing.
1const FAQ = ({ data }) => {
2 const [activeIndex, setActiveIndex] = useState(new Set());
3
4 return data.faq_list?.faqs ? (
5 {data.faq_list.faqs.map((id_qa) => (
We primarily loop over the accordion elements and we remember the extended (not collapsed) elements.
Let's see how the data is stored on the block. Open your BlockEdit. See the helper component SidebarPortal
. Everything inside is displayed in the Sidebar.
1import React from 'react';
2import { SidebarPortal } from '@plone/volto/components';
3
4import FAQSidebar from './FAQSidebar';
5import FAQ from './FAQ';
6
7const Edit = ({ data, onChangeBlock, block, selected }) => {
8 return (
9 <div className={'block faq'}>
10 <SidebarPortal selected={selected}>
11 <FAQSidebar data={data} block={block} onChangeBlock={onChangeBlock} />
12 </SidebarPortal>
13
14 <FAQ data={data} />
15 </div>
16 );
17};
18
19export default Edit;
We outsource the edit form in a file FAQSidebar.jsx
which displays the form according a schema of question and answers. The onChangeBlock event handler is inherited, it stores the value on the block.
1import React from 'react';
2import { FAQSchema } from './schema';
3import InlineForm from '@plone/volto/components/manage/Form/InlineForm';
4
5const FAQSidebar = ({ data, block, onChangeBlock }) => {
6 return (
7 <InlineForm
8 schema={FAQSchema}
9 title={FAQSchema.title}
10 onChangeField={(id, value) => {
11 onChangeBlock(block, {
12 ...data,
13 [id]: value,
14 });
15 }}
16 formData={data}
17 />
18 );
19};
20
21export default FAQSidebar;
We define the schema in schema.js
.
1export const FAQSchema = {
2 title: 'FAQ',
3 fieldsets: [
4 {
5 id: 'default',
6 title: 'Default',
7 fields: ['faq_list'],
8 },
9 ],
10 properties: {
11 faq_list: {
12 title: 'Question and Answers',
13 type: 'faqlist',
14 },
15 },
16 required: [],
17};
The field faq_list has a type 'faqlist'. This has to be registered as a widget in src/index.js
. This configuration is the central place where your add-on can customize the hosting Volto app. It's the place where we later also register our new block type with information about its view and edit form.
1import FAQListEditWidget from './FAQ/FAQListEditWidget';
2
3export default function applyConfig(config) {
4 config.widgets.type.faqlist = FAQListEditWidget;
5
6 return config;
7}
Now we will code the important part of the whole block type: the widget FAQListEditWidget
.
We need a form that consists of a list of existing questions and answers.
The text should be editable.
Additional pairs of questions and answers should be addable.
Next step will be to let the list be drag- and droppable to reorder the items.
Also should an item be deletable.
That's a lot. Let's start with the list of fields displaying the existing values.
Create a FAQListEditWidget.jsx
.
1import { Form as VoltoForm } from '@plone/volto/components';
2
3const FAQListEditWidget = (props) => {
4 const { value = {}, id, onChange } = props;
5 // id is the field name: faq_list
6 // value is the form data (see example in schema.js)
7
8 // qaList: array of [id_question, [question, answer]]
9 const qaList = (value.faqs || []).map((key) => [key, value.faqs_layout[key]]);
10
11 return (
12 // loop over question answer pairs *qaList*
13 <VoltoForm
14 onSubmit={({ question, answer }) => {
15 onSubmitQAPair(childId, question, answer);
16 }}
17 formData={{
18 question: value.faqs_layout[childId][0],
19 answer: value.faqs_layout[childId][1],
20 }}
21 schema={QuestionAnswerPairSchema(
22 props.intl.formatMessage(messages.question),
23 props.intl.formatMessage(messages.answer),
24 )}
25 />
You see the Volto Form
component with its onSubmit event, the form data and the schema to be used.
The form is fructified by the schema QuestionAnswerPairSchema. It's simple, just a string field with a TextArea widget for the question and a such for the answer, but with a RichText widget to have some editing and styling tools available.
src/FAQ/schema.js
1export const QuestionAnswerPairSchema = (title_question, title_answer) => {
2 return {
3 title: 'Question and Answer Pair',
4 fieldsets: [
5 {
6 id: 'default',
7 title: 'QA pair',
8 fields: ['question', 'answer'],
9 },
10 ],
11 properties: {
12 question: {
13 title: title_question,
14 type: 'string',
15 widget: 'textarea',
16 },
17 answer: {
18 title: title_answer,
19 type: 'string',
20 widget: 'richtext',
21 },
22 },
23 required: ['question', 'answer'],
24 };
25};
What's left to do? You created a block type with view and edit form and even a nice widget for the editor to fill in questions and answers. Register the block type and you are good to start your app and create an FAQ for the conference speakers.
Go to src/index.js
and register your block type.
1import icon from '@plone/volto/icons/list-bullet.svg';
2
3import FAQBlockEdit from './FAQ/BlockEdit';
4import FAQBlockView from './FAQ/BlockView';
5import FAQListEditWidget from './FAQ/FAQListEditWidget';
6
7export default function applyConfig(config) {
8 config.blocks.blocksConfig.faq_viewer = {
9 id: 'faq_viewer',
10 title: 'FAQ',
11 edit: FAQBlockEdit,
12 view: FAQBlockView,
13 icon: icon,
14 group: 'text',
15 restricted: false,
16 mostUsed: false,
17 sidebarTab: 1,
18 security: {
19 addPermission: [],
20 view: [],
21 },
22 };
23
24 config.widgets.type.faqlist = FAQListEditWidget;
25
26 return config;
27}
As we now apply our configuration of the new block type, the app is enriched with an accordion block.
Run
make start
See the complete add-on code @rohberg/volto-accordion-block [1]
30.1. Save your work to Github#
Your add-on is ready to use. As by now your repository is on GitHub. As soon as it is published, you can share it with others.
An official release is done on npm. Switch to section Release a Volto add-on.