If you’ve worked with Sitecore 9 Forms recently, you’ve probably come to know that all forms are saved under a default location, which is ‘/Sitecore/Forms’. This is fine if you have a single site on your instance, but when you get into a multi-site implementation, this gets tricky because you want to separate the folders for the forms for each site – for multiple reasons, a major concern being security, and information architecture in general.
There have been a number of issues reported and resolved by fellow MVPs Jason and Toby. Jason found an issue where the forms don’t show up because it didn’t get added to the index. The second issue Toby found is more related to this post, as it sheds some light on how Sitecore knows where the forms should get saved. It turns out that the item ID of the default forms folder that comes out of the Sitecore installation is stored in an item in the Core database – I believe this value is used to configure the location to index.
Now that we know where the search index looks to index the forms, I got the idea that it reads this value at some point in the SPEAK application that the Forms Designer uses. At this point I figured I could override where it reads this, but I still didn’t know where. I’m not very fluent in SPEAK (yet), so my next approach was to try to investigate what code saves the forms.
After some chrome debugging and dotpeek, I found the web API that gets called:
/sitecore/api/ssc/forms/formdesign/formdesign/save?sc_formmode=new&sc_formlang=en-GB
…and resulting pipeline that saves the forms:
<forms.saveForm> <processor type="Sitecore.ExperienceForms.Client.Pipelines.SaveForm.CreateModels, Sitecore.ExperienceForms.Client" resolve="true" /> <processor type="Sitecore.ExperienceForms.Client.Pipelines.SaveForm.GenerateNames, Sitecore.ExperienceForms.Client"> <defaultItemName>Form Item</defaultItemName> </processor> <processor type="Sitecore.ExperienceForms.Client.Pipelines.SaveForm.UpdateItems, Sitecore.ExperienceForms.Client" resolve="true" /> </forms.saveForm>
The pipeline receives JSON post data from the call, that looks something like this:
You can see here that the JSON data being passed has a bunch of models – they aren’t in any sort of heirarchy, except for sortOrder but that’s really just to make sure the order is correct for the form elements. Each model has a template ID – for the type of item it is, and parent ID – the location where it should be saved. You can also see here that the parentId field of the first model has the magic value – the item ID of the forms folder (‘/Sitecore/Forms’) which is also in the SearchConfig item in the Core database. Now that we know that, all we need to do is override that value. You can do this in the SPEAK application if you are more adept in SPEAK, but I decided to do in the pipeline.
First things first – you can either use the default forms folder and make sub-folders for each site, or you can make a brand new folder and make sub-folder for each site under the new folder. If you do the latter, you must update the SearchConfig item (as noted in Toby’s post) with the item ID of the folder you created.
Next, you can make a sub-folder for each site under the root folder. You probably want to put this in a config setting somewhere, different for each site.
The rest is easy – write a class to set the parentId of the model that contains the information for the main form element, to the item ID of your forms folder:
using Sitecore.Diagnostics; using Sitecore.ExperienceForms.Client.Models.Builder; using Sitecore.ExperienceForms.Client.Pipelines.SaveForm; using Sitecore.Mvc.Pipelines; using System.Linq; namespace Custom.Pipelines { public class UpdateParentLocation : MvcPipelineProcessor<SaveFormEventArgs> { public override void Process(SaveFormEventArgs args) { Assert.ArgumentNotNull((object)args, nameof(args)); if (args.ViewModelWrappers == null) { return; } //Only look for the model that has the form item (searching by template ID) ViewModelWrapper vm = (ViewModelWrapper)(from v in args.ViewModelWrappers where v.Model.TemplateId.ToLower() == "{6ABEE1F2-4AB4-47F0-AD8B-BDB36F37F64C}".ToLower() select v).FirstOrDefault(); if (vm == null) { return; } //Use whatever logic is neccessary to set where the form should be saved vm.ParentId = Sitecore.Configuration.Settings.GetSetting("FormsLocationRoot"); } } }
Put whatever logic you need to in the above set statment to determine the location where the forms should be saved. It could be based on logic that results in creating more folder, etc, but that’s upto you.
Note: There are multiple models being passed, starting with the main form, then all the form elements. Because each form element is a child of the parent, you must be careful not to set the parentID of ALL the models. Which is why we are looking only for the model that has the templateID of the form.
Make a patch config file to insert this step right before the items are saved/updated, and you should be all done.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/"> <sitecore role:require="ContentManagement"> <pipelines> <forms.saveForm> <processor patch:before="processor[@type='Sitecore.ExperienceForms.Client.Pipelines.SaveForm.UpdateItems, Sitecore.ExperienceForms.Client']" type="Custom.Pipelines.UpdateParentLocation, Custom.Pipelines" /> </forms.saveForm> </pipelines> </sitecore> </configuration>
Addendum: I *think* it would also be possible to save the forms under multiple root folders, but then the indexes would need to be reconfigured to look in multiple locations – being that there is only one item for the SearchConfig item, it would need to get customized to handle a string of item IDs , or something of that sort.
I hope future updates to Sitecore 9 Forms will take multi-site implementations into account, but until then, this pipeline should help!