Building subforms in Plone Mosaic

In a recent project, I had to build out a mosaic tile that allows editors/designers to add multiple collapsible contents as they like. To do this, I ended up using schema.Object with schema.List to create new content on the fly.
Building subforms in Plone Mosaic

In a recent project, I had to build out a mosaic tile that allows editors/designers to add multiple collapsible contents as they like. Of course, I could have used one tile per collapsible content, but it would not have been sufficient for a group of collapsible content tiles seen in the screenshot below. 

Accordion

With that said, I had two approaches to achieve this.

  • The first approach was to use collective.z3cform.datagridfield to add new collapsible content to a accordions tile property, which will be stored as dictionary field.
  • The second approach was to use a list of schema.Object to save each accordion or collapsible content.

After various attempts to get both approaches to work, I got schema.Object in a list to work. Here's what I did.

Create a schema interface for the subform

Create a class for an individual collapsible content called IAccordion and add the title and description fields to it.

from zope import schema
from plone.supermodel import model

from your.package import _


class IAccordion(model.Schema):
    
    title = schema.TextLine(
        title=_(u'label_title', default=u'Title'),
        required=True
    )
    
    description = RichText(
        title=_(u'label_description', default=u'Description'),
        required=True
    )

Create a factory class for the subform

Afterwards, create a factory class for the IAccordion class called Accordion and assign each IAccordion field to their respective FieldProperty property. After creating the subform and the factory, register the factory as an adapter using the registerFactoryAdapter function. For more information, see the tutorial on ObjectWidget.

from zope.interface import implementer
from zope.schema.fieldproperty import FieldProperty

@implementer(IAccordion)
class Accordion(object):

    title = FieldProperty(IAccordion['title'])
    description = FieldProperty(IAccordion['description'])

from z3c.form.object import registerFactoryAdapter
registerFactoryAdapter(IAccordion, Accordion)
Note: without the factory class for the subform, in this case Accordion, the data cannot not be saved through the main form. In fact, it will throw the following error:
ValueError: No IObjectFactory adapter registered for z3c.form.testing.IMySubObject

Create the schema interface for the main form

Now that we've accounted for the saving of sub-schema forms for each collapsible content, we need save all collapsible to the main schema form. The following code creates a form, which consists of the title and the list of collapsible contents.

class IAccordionTile(model.Schema):    
    
    title = schema.TextLine(
        title=_(u'label_title', default=u'Title'),
        required=False
    )
    
    accordions = schema.List(
        title=_(u'Collapsible Content'),
        value_type=schema.Object(IAccordion),
        required=False
    )
Note, in the code above, we're using schema.List to store each IAccordion into the schema.Object field.

Create the tile class for the main form

We're  finish with the editing and saving of the data, now we need to render the tile with the data. To do this, we create the tile with the following code:

from plone.tiles import PersistentTile
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile


class AccordionTile(PersistentTile):

    template = ViewPageTemplateFile('templates/accordions.pt')

    def __call__(self):
        self.update()
        return self.template()

    def update(self):
        self.title = self.data.get('title', 'Collapsible Content')
        self.accordions = self.data.get('accordions', [])

Notice, in the code above, we've used the classViewPageTemplateFile to assign the tile to the accordions.pt template file found in the templates folder.  The accordions.pt file consist of the following code:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xml:lang="en"
      lang="en"
      xmlns:tal="http://xml.zope.org/namespaces/tal"
      xmlns:metal="http://xml.zope.org/namespaces/metal"
      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
      i18n:domain="plone">
  <body tal:define="accordions nocall:view/accordions;
                    title nocall:view/title">
      
      <div class="accordions">
          <h2 tal:content="title" class="accordions-title"></h2>
          <tal:block tal:repeat="accordion accordions">
              <div tal:condition="accordion/title" class="accordion-holder" >
                  <button class="accordion">
                    <span  tal:content="accordion/title">Section 1</span>
                    <i class="accordion-icon fas fa-plus"></i>
                  </button>
                    <div class="panel accordion-panel">
                      <p tal:condition="accordion/description" tal:replace="structure accordion/description/output" >Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
                    </div>
              </div>
              
          </tal:block>
      </div>

  </body>
</html>

In addition, you will need to add the accordion static files to your js/css registry or to the page using your theme and diazo. See my accordion gist for the files.

Register the tile in the ZCML and the Registry

Finally, we register the tile in the ZCML with the following code:

<plone:tile
    name="leap_site_accordions"
    title="Collapsible Content"
    description="Accordion Layout"
    add_permission="cmf.ModifyPortalContent"
    class=".accordions.AccordionTile"
    for="*"
    permission="zope.Public"
    schema=".accordions.IAccordionTile"
    template="templates/accordions.pt"
    />

Then in the registry.xml file:

<?xml version="1.0"?>
<registry>

  <record name="plone.app.tiles">
    <value purge="false">
      <element>leap_site_accordions</element>
    </value>
  </record>

  <records prefix="plone.app.mosaic.app_tiles.leap_site_accordions"
           interface="plone.app.mosaic.interfaces.ITile">
    <value key="name">leap_site_accordions</value>
    <value key="label">Collapsible Content</value>
    <value key="category">advanced</value>
    <value key="tile_type">app</value>
    <value key="default_value"></value>
    <value key="read_only">false</value>
    <value key="settings">true</value>
    <value key="favorite">false</value>
    <value key="rich_text">false</value>
    <value key="weight">20</value>
  </records>


</registry>

For more information on registering a tile see plone.tiles documentation.  

Note, the first approach, datagridfield wasn't working for me. The data wasn't saving and there were some CSS errors floating around the mosaic editor. In addition, I found out that this issue has already been reported on community.plone.org.
Go Back
Menu
×