Add Remove Objects Inline Tutorial

Last modified by Raluca Stavro on 2022/05/26

Explains how to implement adding and removing objects in XWiki when editing application entries.

We are going to create a simple application built with Application Within Minutes, called 'Stores'. Since a store can have many headquarters, what we want is to allow the user to add / remove headquarters when editing a Stores entry.

Step 1. Create the Stores Application

The first step is to create the application with AWM. So, create an app called "Stores" and when designing the form use one Short Text field and one Long Text field, as shown in the following screenshot:

stores-form.png

Step 2: Modify the Stores Class Sheet

Let's now modify Stores.Code.StoresSheet to implement dynamically adding / removing headquarters. Modify the default content to be:

For latest versions of XWiki

{{velocity}}
#macro(cleanClassname $classname)$!{escapetool.xml($classname.substring($classname.indexOf('.')).substring(1))}#end

{{html clean="false"}}
#set($discard = $xwiki.jsfx.use('js/xwiki/editors/dataeditors.js', true))
#set($discard = $xwiki.ssfx.use('js/xwiki/editors/dataeditors.css', true))
#set($class = 'Stores.Code.StoresClass')
#set($discard = $doc.use($class))
<div class="xform">
 <div id="add_xobject" class="add_xobject">
 <div id="xclass_Stores.Code.StoresClass" class="xclass collapsable">
   <div id="xclass_${escapetool.xml($class)}_content" class="xclass-content">
     <div>
        #foreach($obj in $doc.getObjects($class))
          #set($objNumber = $obj.number)
          #set($index = $foreach.index + 1)
         <div id="xobject_${class}_${objNumber}" class="xobject collapsable">
            #if($xcontext.action == 'edit')
             <div class="xobject-title">
               <h3>#cleanClassname(${class}) <span class="editor-objectNumber">${obj.number}</span>#if ($firstField): $firstFieldSummary#end
                 <a href="$doc.getURL('objectremove', "form_token=$!{services.csrf.token}&amp;classname=${escapetool.url($class)}&amp;classid=${obj.number}&amp;xredirect=${escapetool.url($doc.getURL('edit'))}")" class="xobject-action delete" title="$services.localization.render('core.editors.object.removeObject.tooltip')">$services.localization.render('core.editors.object.removeObject')</a>
               </h3>
             </div>
            #end
           <div class="xobject-content">
             <dl>
               <dt><label#if ($xcontext.action == 'edit') for="${class}_${objNumber}_0_name"#end>$escapetool.xml($doc.displayPrettyName('name', false, false))</label></dt>
               <dd>{{/html}}$doc.display('name', $obj){{html clean="false"}}</dd>
               <dt><label#if ($xcontext.action == 'edit') for="${class}_${objNumber}_0_address"#end>$escapetool.xml($doc.displayPrettyName('address', false, false))</label></dt>
               <dd>{{/html}}$doc.display('address', $obj){{html clean="false"}}</dd>
             </dl>
           </div>
         </div>
        #end
        #if($xcontext.action == 'edit')
          #set($xredirect = $escapetool.url(${doc.getURL('edit', "template=$!{request.template}&title=$!{request.title}&parent=$!{request.parent}")}))
         <div id="add_xobject_${escapetool.xml($class)}" class="add_xobject">
           <div id="add_xobject_${escapetool.xml($class)}" class="add_xobject" style="display: block;">
             <div id="add_xobject_${escapetool.xml($class)}_title" class="add_xobject-title" style="display: block;">
               <a href="$doc.getURL('edit', "xpage=editobject&amp;xaction=addObject&amp;form_token=$!{services.csrf.token}&amp;classname=$escapetool.url(${class})&amp;xredirect=${xredirect}")" class="xobject-add-control" title="$services.localization.render('core.editors.object.add.label')">$services.localization.render('core.editors.object.add.label')</a>
             </div>
           </div>
         </div>
        #end
     </div>
   </div>
 </div>
 </div>
</div>
{{/html}}
{{/velocity}}

For older versions up to 12.3

{{velocity}}
{{html wiki="true"}}
#set($class = 'Stores.Code.StoresClass')
#set ($discard = $doc.use($class))
(% class="xform" %)
(((
  #foreach($obj in $doc.getObjects($class))
    #set($objNumber = $obj.number)
    #set($index = $foreach.index + 1)
    (% id="xwikiobjects" class="xclass" %)(((
      (% class="xobject" %)(((
        #if($xcontext.action == 'edit')
          (% class="xobject-title" %)(((
           <h3>Headquarter $index
             <a href="$doc.getURL('objectremove', "form_token=$!{services.csrf.getToken()}&amp;classname=${escapetool.url($class)}&amp;classid=${objNumber}&amp;xredirect=${escapetool.url($doc.getURL('edit'))}")" class="xobject-action delete" title="$services.localization.render('core.editors.object.removeObject.tooltip')">$services.localization.render('core.editors.object.removeObject')</a>
           </h3>
          )))
        #end
        (% class="xobject-content" %)(((
          ; <label#if ($xcontext.action == 'edit') for="${class}_${objNumber}_0_name"#end>$escapetool.xml($doc.displayPrettyName('name', false, false))</label>
          : $doc.display('name', $obj)
          ; <label#if ($xcontext.action == 'edit') for="${class}_${objNumber}_0_address"#end>$escapetool.xml($doc.displayPrettyName('address', false, false))</label>
          : $doc.display('address', $obj)
        )))
      )))
    )))
  #end

  #if($xcontext.action == 'edit')
    (% id="add_xobject_${escapetool.xml($class)}" class="add_xobject" style="display: block;" %)(((
      (% id="add_xobject_${escapetool.xml($class)}_title" class="add_xobject-title" style="display: block;" %)((( ## Have to overwrite a CSS rule from dataeditors.css
       <a href="$doc.getURL('edit', "xpage=editobject&amp;xaction=addObject&amp;className=$escapetool.url(${class})&amp;xredirect=$escapetool.url(${doc.getURL('edit')})")" class="xobject-add-control" title="$services.localization.render('core.editors.object.add.label')">$services.localization.render('core.editors.object.add.label')</a>
      )))
    )))
   $xwiki.jsfx.use('js/xwiki/editors/dataeditors.js', true)##
   $xwiki.ssfx.use('js/xwiki/editors/dataeditors.css', true)##
  #end
)))
{{/html}}
{{/velocity}}

Note that if you edit the Stores Application after making the changes in Stores.Code.StoresSheet, these changes will be lost. You will have to redo the steps above if you want to set back the add / remove objects feature in the application.

You're all set! Let's now try it!

Step 3: Create an entry in the Stores Application

Navigate back to the Stores Application and Create an entry. You should now have the options to expand the object that was added by default, to remove this object and to add a new object. When loading the edit form, the objects are collapsed because this is the default behavior of the dataeditors application that we reused to make our feature work.

stores-results1.png

This is what the form looks like when you add a new object :

stores-results2.png

And when viewing the page:

stores-results3.png

A simplified version of edit form

This code was tested on versions up to 12.3.

You can personalize even more your application by updating translations, by changing the styles, by removing the elements that don't suit your use case (like the expand / collapse feature, for example).

The AWM version

This is a simplified UI of an application built with AWM, that doesn't contain icons or the expand/collapse feature, but contains the basic elements that are needed in order to make it possible to add / remove objects in the application entries. This code is meant to be put in the Stores Class Sheet (Stores.Code.StoresSheet document).

{{velocity}}
{{html wiki="true"}}
#set($class = 'Stores.Code.StoresClass')
(% class="xform" %)
(((
  #foreach($obj in $doc.getObjects($class))
    #set($objNumber = $obj.number)
    (% id="xwikiobjects" class="xclass" %)(((
      (% class="xobject" %)(((
       <span class="xobject-title"></span> ## We need this line, because we base our code on dataeditors.js and for now, this class is mandatory

        ; <label#if ($xcontext.action == 'edit') for="${class}_${objNumber}_0_name"#end>$escapetool.xml($doc.displayPrettyName('name', false, false))</label>
        : $doc.display('name', $obj)
        ; <label#if ($xcontext.action == 'edit') for="${class}_${objNumber}_0_address"#end>$escapetool.xml($doc.displayPrettyName('address', false, false))</label>
        : $doc.display('address', $obj)

        #if($xcontext.action == 'edit')
         <a href="$doc.getURL('objectremove', "form_token=$!{services.csrf.getToken()}&amp;classname=${escapetool.url($class)}&amp;classid=${objNumber}&amp;xredirect=${escapetool.url($doc.getURL('edit'))}")" class="xobject-action delete" title="$services.localization.render('core.editors.object.removeObject.tooltip')">$services.localization.render('core.editors.object.removeObject')</a>
        #end
      )))
    )))
  #end

  #if($xcontext.action == 'edit')
   $xwiki.jsfx.use('js/xwiki/editors/dataeditors.js', true)##
   <a href="$doc.getURL('edit', "xpage=editobject&amp;xaction=addObject&amp;className=$escapetool.url(${class})&amp;xredirect=$escapetool.url(${doc.getURL('edit')})")" class="xobject-add-control button" title="$services.localization.render('core.editors.object.add.label')">$services.localization.render('core.editors.object.add.label')</a>
  #end
)))
{{/html}}
{{/velocity}}

The non-AWM version

Now, let's move outside AWM.
If you have an application that was not built with AWM, these are the elements that you need to add in your sheet if you want to enable the option to add / remove objects :

{{velocity}}
## Defining a variable that stores the name of the XClass that is part of MyApp
#set($class = 'MyApp.MyClass')

## Iterating over all objects of this class
#foreach($obj in $doc.getObjects($class))

  ## Container used for detecting objects of a certain class
 (% id="xwikiobjects" class="xclass" %)(((

    ## Container used for detecting individual objects
   (% class="xobject" %)(((

      ## We need this element, for now, because it is used by dataeditors.js and you would get JS errors without it
     (% class="xobject-title" %)

      ## Displaying the properties of the current object
     $doc.display('my_prop_name', $obj)
      ...

      ## The DELETE object button, displayed in edit mode
      #if($xcontext.action == 'edit')
        {{html}}
         <a href="$doc.getURL('objectremove', "form_token=$!{services.csrf.getToken()}&amp;classname=${escapetool.url($class)}&amp;classid=${obj.number}&amp;xredirect=${escapetool.url($doc.getURL('edit'))}")" class="xobject-action delete" title="$services.localization.render('core.editors.object.removeObject.tooltip')">$services.localization.render('core.editors.object.removeObject')</a>
        {{/html}}
      #end
   )))
 )))
#end

#if($xcontext.action == 'edit')
  ## Adding dataeditors.js to the page
  $xwiki.jsfx.use('js/xwiki/editors/dataeditors.js', true)##

  ## The ADD object button
  {{html}}
   <a href="$doc.getURL('edit', "xpage=editobject&amp;xaction=addObject&amp;className=$escapetool.url(${class})&amp;xredirect=$escapetool.url(${doc.getURL('edit')})")" class="xobject-add-control" title="$services.localization.render('core.editors.object.add.label')">$services.localization.render('core.editors.object.add.label')</a>
  {{/html}}
#end
{{/velocity}}
Tags:
   

Get Connected