Take the community feedback survey now.

CMS 12 - Custom EditorDescriptor For PropertyList

Vote:
 

I am trying to implement a custom EditorDescriptor for a PropertyList. My goal is to show a Grid for the list and a button to delete item from the List. For that I have written a EditorDescriptor and the following js code. Now I can see the List getting rendered properly along with the buttons. Also when I click on a Button, it removes the row from the list. But it does not trigger the on change event which would enable the Publish button to publish the changes. Also, after deleting some rows, if I make changes to any other property and then Publish the changes, my grid values seem to be unchanged. Meaning the delete operation does not get saved. Can anyone tell me what I am doing wrong here.



define([
  "dojo/_base/declare",
  "dijit/_WidgetBase",
  "dijit/_TemplatedMixin"
], function (
  declare,
  _WidgetBase,
  _TemplatedMixin
) {
  return declare([_WidgetBase, _TemplatedMixin], {
    templateString: `
      <div>
          <div data-dojo-attach-point="gridNode">
          </div>
      </div>
    `,
    intermediateChanges: true,
    value: null,

    postCreate: function () {
      this.inherited(arguments);
      this._renderGrid();
    },

    _renderGrid: function () {
      if (!Array.isArray(this.value)) {
        this.gridNode.innerHTML = `
          <div class="dgrid-no-data">
            <span>
              There are no items available.
            </span>
          </div>`;
        return;
      }

      const self = this;
      const rows = this.value.map(function (item, index) {
        const listType = item.listType || "N/A";
        const indexInList = item.indexInList || "Untitled";
        const contentListBlockLink = item.contentListBlockLink || "#";
        return `
          <tr>
            <td class="dgrid-cell dgrid-cell-padding"><a href="${contentListBlockLink}">${listType}</a></td>
            <td class="dgrid-cell dgrid-cell-padding">${indexInList}</td>
            <td class="dgrid-cell dgrid-cell-padding"><button type="button" class="delete-btn" data-index="${index}">Delete</button></td>
          </tr>
        `;
      });

      this.gridNode.innerHTML = `
        <table class="content-list-grid dgrid-row-table" style="width: 100%; border-collapse: collapse;">
            <thead>
                <tr>
                  <th class="dgrid-cell dgrid-cell-padding">List Type</th>
                  <th class="dgrid-cell dgrid-cell-padding">Index In List</th>
                  <th class="dgrid-cell dgrid-cell-padding">Actions</th>
                </tr>
            </thead>
            <tbody>
                ${rows.join("")}
            </tbody>
        </table>
      `;

      this.gridNode.querySelectorAll(".delete-btn").forEach(function (btn) {
        btn.addEventListener("click", function (e) {
          const index = parseInt(e.target.getAttribute("data-index"));
          self._deleteItem(index);
        });
      });
    },

    _setValueAttr: function (value) {
      const newValue = Array.isArray(value) ? value.slice() : [];
      this._set("value", newValue);
      this._renderGrid();
    },

    _getValueAttr: function () {
      return this.value;
    },
    getValue: function () {
      return this.value ? this.value.slice() : [];
    },
    _deleteItem: function (index) {
      const updatedValue = this.getValue();
      updatedValue.splice(index, 1);
      this._set("value", updatedValue);
      this._renderGrid();
      if (typeof this.onChange === "function") {
        this.onChange(updatedValue, true);
      }
      this._set("dirty", true);
      this._set("intermediateChanges", true);
    }
  });
});
#339792
Edited, Jul 22, 2025 16:13
Vote:
 

Try to create new object references when modifying arrays or objects

_deleteItem: function (index) {
    // Create a completely new array to ensure change detection
    const currentValue = this.getValue();
    const updatedValue = currentValue.filter((item, i) => i !== index);
    
    // Set the new value using the proper setter
    this.set("value", updatedValue);
    
    // Trigger change notification
    if (typeof this.onChange === "function") {
        this.onChange(updatedValue);
    }
    
    // Mark as dirty for change tracking
    this.set("dirty", true);
    this.set("intermediateChanges", true);
    
    // Call inherited to ensure proper processing
    this.inherited(arguments);
}
#339795
Jul 22, 2025 17:24
Vote:
 

Ravindra S. Rathore, Thanks for the suggestion. However, it did not fix the issue that I am having. Once I delete an item, I want to trigger the saving mechanism which will enable the Publish option. And after publishing the changed data should get reflected just like it would do if I had used the CollectionEditorDescriptor. But with my current code, if I delete an item, it does not enable the Publish option.

#339796
Jul 22, 2025 19:29
Vote:
 

To me it seems like you are trying to add a custom list UI component. It brings more complexities in terms of managing the flow with existing UI components out of the box. Such as triggering "save/publish" always works on correct "parent" event. The part of the problem could be here - 

this.onChange(updatedValue, true);

Try to trace the event, as where it is trying to trigger onChange (i.e, does it notify it's parent - in this case, the page where you have this component). If that all works, I would also make sure that the data goes through the formData (i.e. save event - check XHR request).

But you can simply avoid this complexity by managing things out-of-the-box. 

I had some kind of same scenario, where my component was a custom list of block type  (in commerce, particularly). 

Where - 

  1. The promotion should have an ability to list items with quantity.
  2. It should save/publish it along with the discount.

Steps of development - 

Create a PropertyList of your custom class: 

 [Serializable]
    public class PromotionWithQuantityCore
    {
        [Display(
            Order = 10,
            Description = "Minimum quantity 2"
        )]
        [Required]
        [Range(2, int.MaxValue)]
        public int Quantity { get; set; }

        [Display(
            Order = 20
        )]
        [AllowedTypes(typeof(VariationContent), typeof(PackageContent))]
        [Required]
        [UIHint(UIHint.CatalogContent)] // or use "catalogcontent" directly as string
        public ContentReference Target { get; set; }
    }

Use it wherever you want - 

 [ContentType(DisplayName ="Spend Amount to get gift items with quantity", GUID = "B302DE61-AA72-49F1-8B32-B4E3E8A26FE5", GroupName = "entrypromotion", Order = 10600)]
    [ImageUrl("Images/SpendAmountGetGiftItems.png")]
    public class SpendForFreeWithQuantity : EntryPromotion, IPurchaseAmount
    {
        /// <summary>
        ///  The condition for the promotion that needs to be fulfilled before the discount is applied.
        /// </summary>
        [Display(Order = 10)]
        [PromotionRegion("Condition")]
        public virtual PurchaseAmount Condition { get; set; }
        
        [Display(Order = 20)]
        [PromotionRegion("Reward")]
        [EditorDescriptor(EditorDescriptorType = typeof(CollectionEditorDescriptor<PromotionWithQuantityCore>))]
        public virtual IList<PromotionWithQuantityCore> GiftItems { get; set; }
    }
    
    [PropertyDefinitionTypePlugIn()]
    public class PromotionQuantityBlockProperty : PropertyList<PromotionWithQuantityCore>
    {
    }

In my case it was promotion with special type of condition, also PropertyList is only required in episerver < 12.

 

And, that's it. Now you should have your events triggered an saved along with page/block/wherever properties.

#339799
Edited, Jul 23, 2025 8:53
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.