As for now (in version 9.3) Sitecore JSS does not support Edit Frames, which were traditionally used to manage metadata fields in Experience Editor. Instead, Sitecore offers small enhancement in Experience Editor, a button where content authors can edit all non standard fields of data source item for selected component:
But what if we have more complex component, which displays child items, for example slider, or hero carousel. Fortunately Sitecore gives you another button, which you can assign to your rendering item:
If you setup correctly Datasource Location, Datasource Template and Insert Options for child items, this button allows content authors to add child items for your component’s data source (like e.g. adding new slides).
Additionally Sitecore gives you another button allowing to sort child items, which can be enhanced to allow to delete the items, like described in this blog. In following article we will extend this idea to allow all operations: sorting, deleting and editing child items from Experience Editor.
Complete source code of this module is available in Github.
Implementation
Command
Let’s start with inserting new command item in core database: /sitecore/content/Applications/WebEdit/Custom Experience Buttons/Manage Child Items inheriting from WebEdit Button template:
We can assign this command to our rendering item in “Experience Editor Buttons” field:
Now we need to implement the command:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
namespace My.Feature.ManageChildItems.Commands { using System; using My.Feature.ManageChildItems.Dialogs; using Sitecore; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Shell.Applications.Dialogs.SortContent; using Sitecore.Shell.Applications.WebEdit.Commands; using Sitecore.Web; using Sitecore.Web.UI.Sheer; /// <summary> /// Opens manage child items dialog. /// </summary> [Serializable] public class ManageContent : SortContent { /// <inheritdoc/> protected override SortContentOptions GetOptions(ClientPipelineArgs args) { Assert.ArgumentNotNull((object)args, "args"); Item obj = Client.ContentDatabase.GetItem(args.Parameters["itemid"], WebEditUtil.GetClientContentLanguage() ?? Context.Language); Assert.IsNotNull(obj, "item"); return new ManageContentOptions(obj); } } } |
And command options class, which tells Sitecore which pop-up it shall open:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
namespace My.Feature.ManageChildItems.Dialogs { using Sitecore.Data.Items; using Sitecore.Shell.Applications.Dialogs.SortContent; /// <summary> /// Manage Content Dialog Options. /// </summary> public class ManageContentOptions : SortContentOptions { /// <summary> /// Initializes a new instance of the <see cref="ManageContentOptions"/> class. /// </summary> /// <param name="item">Item.</param> public ManageContentOptions(Item item) : base(item) { } /// <inheritdoc/> protected override string GetXmlControl() { return "Sitecore.Shell.Applications.Dialogs.Manage"; } } } |
You may notice that we inherit from Sitecore OOTB sort items command and options. We’ll do it all over the place, cause or pop-up just extend standard sort content window.
Next we link our implementation with command in Sitecore item:
1 2 3 4 5 6 7 |
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/"> <sitecore role:require="Standalone or ContentManagement"> <commands> <command name="webedit:managecontent" type="My.Feature.ManageChildItems.Commands.ManageContent, My.Feature.ManageChildItems" /> </commands> </sitecore> </configuration> |
Form
To implement the form we need to re-use some old-school SheerUI and SPEAK code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
namespace My.Feature.ManageChildItems.Dialogs { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Sitecore; using Sitecore.Data; using Sitecore.Data.Comparers; using Sitecore.Data.Items; using Sitecore.Data.Managers; using Sitecore.Diagnostics; using Sitecore.SecurityModel; using Sitecore.Shell.Applications.ContentEditor; using Sitecore.Shell.Applications.Dialogs.Sort; using Sitecore.Shell.Applications.Dialogs.SortContent; using Sitecore.Text; using Sitecore.Web; using Sitecore.Web.UI.Sheer; /// <summary> /// Child items sort and delete form. /// </summary> public class ManageForm : SortForm { /// <inheritdoc/> protected override void OnLoad(EventArgs e) { Assert.ArgumentNotNull((object)e, "e"); base.OnLoad(e); if (Context.ClientPage.IsEvent) { return; } var sortContentOptions = SortContentOptions.Parse(); var contentToSortQuery = sortContentOptions.ContentToSortQuery; Assert.IsNotNullOrEmpty(contentToSortQuery, "query"); // use reflection to call private method, we don't want to copy&paste it from Sitecore with bunch of other needed private methods var getItemsMethod = typeof(SortForm).GetMethod("GetItemsToSort", BindingFlags.NonPublic | BindingFlags.Instance); Item[] itemsToSort = (Item[])getItemsMethod.Invoke(this, new object[] { sortContentOptions.Item, contentToSortQuery }); Array.Sort(itemsToSort, new DefaultComparer()); // we need to change behaviour to check if there's at least 1 child item, istead of 2 if (itemsToSort.Length < 1) { this.OK.Disabled = true; } else { this.OK.Disabled = false; this.MainContainer.Controls.Clear(); // use reflection to call private method, we don't want to copy&paste it from Sitecore with bunch of other needed private methods var renderMethod = typeof(SortForm).GetMethod("Render", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(IEnumerable<Item>) }, null); this.MainContainer.InnerHtml = (string)renderMethod.Invoke(this, new object[] { itemsToSort }); } } /// <inheritdoc/> protected override void OnOK(object sender, EventArgs args) { Assert.ArgumentNotNull(sender, "sender"); Assert.ArgumentNotNull(args, "args"); var idsToSort = new ListString(WebUtil.GetFormValue("sortorder")); var idsToDelete = new ListString(WebUtil.GetFormValue("deleteItem")); if (idsToSort.Count == 0 && idsToDelete.Count == 0) { base.OnOK(sender, args); } else { if (idsToDelete.Count > 0) { this.DeleteItems(from i in idsToDelete select ShortID.DecodeID(i)); SheerResponse.SetDialogValue("1"); base.OnOK(sender, args); } if (idsToSort.Count > 0) { base.OnOK(sender, args); } } } /// <summary> /// Edit item. /// </summary> protected virtual void OnEdit() { var itemId = WebUtil.GetFormValue("editItem"); this.OpenEditDialog(Client.ContentDatabase.GetItem(new ID(itemId))); // force to reload page after closing the dialog, not too optimal, but we don't have feedback from dialog SheerResponse.SetDialogValue("1"); } private void DeleteItems(IEnumerable<ID> toDeleteList) { Assert.ArgumentNotNull(toDeleteList, "toDeleteList"); foreach (ID id in toDeleteList) { ID idToFind = id; var itemToDelete = Client.ContentDatabase.GetItem(ID.Parse(idToFind)); if (itemToDelete != null) { using (new SecurityDisabler()) { itemToDelete.Delete(); } } } } private void OpenEditDialog(Item item) { if (item == null) { return; } var fieldList = this.CreateFieldDescriptors(item); var fieldEditorOptions = new FieldEditorOptions(fieldList) { SaveItem = true, }; var url = fieldEditorOptions.ToUrlString().ToString(); SheerResponse.ShowModalDialog(new ModalDialogOptions(url) { Width = "800", Height = "600", Response = false, ForceDialogSize = false, }); } private IEnumerable<FieldDescriptor> CreateFieldDescriptors(Item item) { var fieldDescriptors = new List<FieldDescriptor>(); var template = TemplateManager.GetTemplate(item.TemplateID, Client.ContentDatabase); var allFields = template.GetFields(true); foreach (var field in allFields) { if (field.Name.StartsWith("__")) { continue; } fieldDescriptors.Add(new FieldDescriptor(item, field.Name)); } return fieldDescriptors; } } } |
Next we need to create UI for our form. Most of the code is a copy of Sitecore’s sort form and this blog. Under: /sitecore/shell/Applications/Dialogs/ we create new folder “Manage” with following files:
1. Manage.xml, full source code: https://github.com/whuu/Sc.ManageChildItems/blob/master/src/Feature/ManageChildItems/website/sitecore/shell/Applications/Dialogs/Manage/Manage.xml
Comparing to standard sort form we change code besides to our ManageForm class and add tow buttons:
1 2 3 4 5 6 |
<Border> <Button ID="Edit" Click="javascript:scEdit()" Disabled="true" Header="Edit"/> </Border> <Border> <Button ID="Delete" Click="javascript:scDelete()" Disabled="true" Header="Delete"/> </Border> |
2. Manage.js, full source code: https://github.com/whuu/Sc.ManageChildItems/blob/master/src/Feature/ManageChildItems/website/sitecore/shell/Applications/Dialogs/Manage/Manage.js where we handle new buttons:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
function scUpdateMoveButtonsState() { var moveUp = $("MoveUp"); var moveDown = $("MoveDown"); var deleteBtn = $("Delete"); var editBtn = $("Edit"); var selectedItem = scGetSelectedItem(); if (!selectedItem) { moveUp.disable(); moveDown.disable(); deleteBtn.disable(); editBtn.disable(); return; } deleteBtn.enable(); editBtn.enable(); ... } function scDelete() { var deleteItem = $("deleteItem"); if (!deleteItem) { deleteItem = new Element("input", { type: "hidden", id: "deleteItem" }); var form = document.forms[0]; if (!form) { return; } form.appendChild(deleteItem); } scGetSelectedItem().addClassName("deleted"); var ids = $$(".deleted").map(function (item) { return item.id; }) || []; var serialized = ids.join("|") || ""; deleteItem.value = serialized; return false; } function scEdit() { var editItem = $("editItem"); if (!editItem) { editItem = new Element("input", { type: "hidden", id: "editItem" }); var form = document.forms[0]; if (!form) { return; } form.appendChild(editItem); } editItem.value = scGetSelectedItem().id; return scForm.postEvent(this, event, 'OnEdit'); } |
3. Manage.css, full source code: https://github.com/whuu/Sc.ManageChildItems/blob/master/src/Feature/ManageChildItems/website/sitecore/shell/Applications/Dialogs/Manage/Manage.css where we just add additional style for removed item:
1 2 3 |
.sort-item.deleted{ display:none; } |
Results
Finally we can open Experience Editor, click our “Manage Child Items” button, select child item, sort, remove or edit fields: