From c70d4cdc8b805ff6f91f42578ecfa6dac0be7101 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Tue, 25 Nov 2025 13:25:36 +0100 Subject: [PATCH 01/12] sorter docs --- .../utilities/sorter/sorter-configuration.md | 264 +++++++++++++++++ .../utilities/sorter/sorter-enable-disable.md | 91 ++++++ .../sorter/sorter-multiple-containers.md | 120 ++++++++ .../utilities/sorter/sorter-setting-model.md | 278 ++++++++++++++++++ .../utilities/{ => sorter}/sorting.md | 0 5 files changed, 753 insertions(+) create mode 100644 17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md create mode 100644 17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md create mode 100644 17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md create mode 100644 17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md rename 17/umbraco-cms/customizing/utilities/{ => sorter}/sorting.md (100%) diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md new file mode 100644 index 00000000000..b3cabb60b47 --- /dev/null +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md @@ -0,0 +1,264 @@ +# Sorter Configuration Options + +The [`UmbSorterController`](sorter.controller.ts) accepts a comprehensive configuration object. This guide covers all available options. + +## Required Configuration + +### `getUniqueOfElement` +```typescript +getUniqueOfElement: (element: ElementType) => string | symbol | number | null | undefined +``` +Returns the unique identifier from a DOM element. Return `undefined` or `null` to cancel a move operation. + +**Example:** +```typescript +getUniqueOfElement: (element) => element.getAttribute('data-id') +``` + +### `getUniqueOfModel` +```typescript +getUniqueOfModel: (modelEntry: T) => string | symbol | number | null | undefined +``` +Returns the unique identifier from a model entry. + +**Example:** +```typescript +getUniqueOfModel: (model) => model.id +``` + +### `itemSelector` +```typescript +itemSelector: string +``` +A CSS selector matching the draggable items. + +**Example:** +```typescript +itemSelector: '.sortable-item' +``` + +## Optional Configuration + +### `identifier` +```typescript +identifier?: string | symbol | number +``` +A unique identifier for the sorter instance. All sorters sharing the same identifier can interact with each other (drag items between them). Defaults to a new `Symbol()` if not provided. + +**Example:** +```typescript +identifier: 'my-shared-sorter-group' +``` + +### `containerSelector` +```typescript +containerSelector?: string +``` +A CSS selector for the container element. If not provided, the host element is used. + +**Example:** +```typescript +containerSelector: '.items-container' +``` + +### `disabledItemSelector` +```typescript +disabledItemSelector?: string +``` +A CSS selector for items that should not be draggable. + +**Example:** +```typescript +disabledItemSelector: '.disabled-item' +``` + +### `ignorerSelector` +```typescript +ignorerSelector?: string +``` +A CSS selector for elements within items that should not trigger dragging. Defaults to `'a,img,iframe,input,textarea,select,option'`. + +**Example:** +```typescript +ignorerSelector: 'a,button,input' +``` + +### `placeholderClass` +```typescript +placeholderClass?: string +``` +CSS class applied to the dragged element while dragging. + +**Example:** +```typescript +placeholderClass: 'dragging-placeholder' +``` + +### `placeholderAttr` +```typescript +placeholderAttr?: string +``` +Attribute set on the dragged element while dragging. If neither `placeholderClass` nor `placeholderAttr` is provided, defaults to `'drag-placeholder'`. + +**Example:** +```typescript +placeholderAttr: 'data-dragging' +``` + +### `draggableSelector` +```typescript +draggableSelector?: string +``` +CSS selector for the specific element within an item that should be draggable. Useful for adding a drag handle. + +**Example:** +```typescript +draggableSelector: '.drag-handle' +``` + +### `handleSelector` +```typescript +handleSelector?: string +``` +CSS selector for the interactive handle within an item. Only this element can initiate dragging. + +**Example:** +```typescript +handleSelector: '.drag-handle-button' +``` + +## Callback Configuration + +### `onChange` +```typescript +onChange?: (args: { item: T; model: Array }) => void +``` +Called whenever the model changes. Not called if more specific callbacks are provided. + +**Example:** +```typescript +onChange: ({ model, item }) => { + console.log('Model changed:', model); + this._items = model; +} +``` + +### `onContainerChange` +```typescript +onContainerChange?: (args: { + item: T; + model: Array; + from: UmbSorterController | undefined +}) => void +``` +Called when an item moves from another container to this one. + +**Example:** +```typescript +onContainerChange: ({ item, model, from }) => { + console.log(`Item ${item.id} moved from another container`); + this._items = model; +} +``` + +### `onStart` +```typescript +onStart?: (args: { item: T; element: ElementType }) => void +``` +Called when dragging starts. + +**Example:** +```typescript +onStart: ({ item }) => { + console.log('Started dragging:', item.name); +} +``` + +### `onEnd` +```typescript +onEnd?: (args: { item: T; element: ElementType }) => void +``` +Called when dragging ends. + +**Example:** +```typescript +onEnd: ({ item }) => { + console.log('Finished dragging:', item.name); +} +``` + +### `onRequestMove` +```typescript +onRequestMove?: (args: { item: T }) => boolean +``` +Called to validate if an item can be moved into this container. Return `false` to prevent the move. + +**Example:** +```typescript +onRequestMove: ({ item }) => { + return item.type === 'allowed-type'; +} +``` + +### `onDisallowed` / `onAllowed` +```typescript +onDisallowed?: (args: { item: T; element: ElementType }) => void +onAllowed?: (args: { item: T; element: ElementType }) => void +``` +Visual feedback callbacks for when moves are disallowed/allowed. + +**Example:** +```typescript +onDisallowed: ({ element }) => { + element.classList.add('drop-not-allowed'); +}, +onAllowed: ({ element }) => { + element.classList.remove('drop-not-allowed'); +} +``` + +## Advanced Callbacks + +### `performItemMove` +```typescript +performItemMove?: (args: { + item: T; + newIndex: number; + oldIndex: number +}) => Promise | boolean +``` +Custom handler for moving items within the same container. Return `false` to cancel. + +**Example:** +```typescript +performItemMove: async ({ item, newIndex, oldIndex }) => { + await this.saveToServer(item, newIndex); + return true; +} +``` + +### `performItemInsert` +```typescript +performItemInsert?: (args: { item: T; newIndex: number }) => Promise | boolean +``` +Custom handler for inserting items into the container. + +### `performItemRemove` +```typescript +performItemRemove?: (args: { item: T }) => Promise | boolean +``` +Custom handler for removing items from the container. + +### `resolvePlacement` +```typescript +resolvePlacement?: (args: UmbSorterResolvePlacementArgs) => UmbSorterResolvePlacementReturn +``` +Custom logic for determining where to place an item during drag. Return `true` to place after, `false` to place before, or `null` to cancel. + +**Example:** +```typescript +resolvePlacement: ({ pointerY, relatedRect }) => { + // Place after if pointer is in bottom half + return pointerY > relatedRect.top + relatedRect.height * 0.5; +} +``` \ No newline at end of file diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md new file mode 100644 index 00000000000..b41eeca0969 --- /dev/null +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md @@ -0,0 +1,91 @@ +# Enabling and Disabling the Sorter + +The Sorter can be dynamically enabled or disabled based on your application's state. This is useful when you want to toggle between viewing and editing modes. + +## Methods + +### `enable()` +Enables the sorter, allowing sorting interactions to occur. + +### `disable()` +Disables the sorter, preventing any sorting interactions. + +## Example Usage + +The following example shows how to toggle the sorter based on a "sort mode" state: + +```typescript +export class UmbContentTypeDesignEditorElement extends UmbLitElement { + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute('data-id'), + getUniqueOfModel: (model) => model.id, + itemSelector: '.sortable-item', + containerSelector: '.sortable-container', + onChange: ({ model }) => { + this._items = model; + }, + }); + + @state() + private _sortModeActive = false; + + constructor() { + super(); + + // Initially disable the sorter + this.#sorter.disable(); + + // Watch for changes to sort mode + this.observe( + this.#designContext.isSorting, + (isSorting) => { + this._sortModeActive = isSorting; + if (isSorting) { + this.#sorter.enable(); + } else { + this.#sorter.disable(); + } + } + ); + } + + #toggleSortMode() { + this._sortModeActive = !this._sortModeActive; + if (this._sortModeActive) { + this.#sorter.enable(); + } else { + this.#sorter.disable(); + } + } + + override render() { + return html` + + ${this._sortModeActive ? 'Done' : 'Sort'} + + +
+ ${repeat( + this._items, + (item) => item.id, + (item) => html` +
+ ${item.name} +
+ ` + )} +
+ `; + } +} +``` + +## Key Points + +- The sorter is **enabled by default** when instantiated +- Call [`disable()`](sorter.controller.ts) to prevent sorting interactions +- Call [`enable()`](sorter.controller.ts) to re-enable sorting +- These methods are idempotent - calling them multiple times has no additional effect +- Disabling the sorter removes all drag event listeners, improving performance when sorting is not needed \ No newline at end of file diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md new file mode 100644 index 00000000000..2d11c6b625f --- /dev/null +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md @@ -0,0 +1,120 @@ +# Sorting Across Multiple Containers + +The Sorter supports dragging items between multiple containers by using a shared [`identifier`](sorter.controller.ts). This allows you to create complex drag-and-drop interfaces. + +## Configuration + +To enable cross-container sorting, ensure all sorter instances use the same `identifier`: + +```typescript +const sharedIdentifier = 'my-shared-sorter-group'; + +// Container 1 +#sorter1 = new UmbSorterController(this, { + identifier: sharedIdentifier, + // ... other config +}); + +// Container 2 +#sorter2 = new UmbSorterController(this, { + identifier: sharedIdentifier, + // ... other config +}); +``` + +## Example: Two Connected Containers + +This example from [`sorter-group.ts`](examples/sorter-with-two-containers/sorter-group.ts) shows two independent containers that can exchange items: + +```typescript +export type ModelEntryType = { + name: string; +}; + +@customElement('example-sorter-group') +export class ExampleSorterGroup extends UmbElementMixin(LitElement) { + @property({ type: Array, attribute: false }) + public get items(): ModelEntryType[] { + return this._items ?? []; + } + public set items(value: ModelEntryType[]) { + // Only set model initially, not on re-renders + if (this._items !== undefined) return; + this._items = value; + this.#sorter.setModel(this._items); + } + private _items?: ModelEntryType[]; + + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.name, + getUniqueOfModel: (modelEntry) => modelEntry.name, + // This identifier connects all sorter instances + identifier: 'string-that-identifies-all-example-sorters', + itemSelector: 'example-sorter-item', + containerSelector: '.sorter-container', + onChange: ({ model }) => { + const oldValue = this._items; + this._items = model; + this.requestUpdate('items', oldValue); + }, + }); + + removeItem = (item: ModelEntryType) => { + this._items = this._items!.filter((r) => r.name !== item.name); + this.#sorter.setModel(this._items); + }; + + override render() { + return html` +
+ ${repeat( + this.items, + (item) => item.name, + (item) => html` + + + + ` + )} +
+ `; + } +} +``` + +### Usage + +```html + + + +``` + +## Key Points + +- **Shared Identifier**: All containers must use the same `identifier` value +- **Independent Models**: Each container maintains its own model via [`setModel()`](sorter.controller.ts) +- **Automatic Updates**: The `onChange` callback handles model updates when items are moved between containers +- **Item Removal**: When setting the model after removing an item, call [`setModel()`](sorter.controller.ts) to sync the state + +## Validation + +Use [`onRequestMove`](sorter.controller.ts) to control which items can be dropped into specific containers: + +```typescript +#sorter = new UmbSorterController(this, { + // ... other config + onRequestMove: ({ item }) => { + // Only allow items of a specific type + return item.type === 'allowed-in-this-container'; + }, +}); +``` \ No newline at end of file diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md new file mode 100644 index 00000000000..41e8dfb2628 --- /dev/null +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md @@ -0,0 +1,278 @@ +# Setting the Sorter Model + +The sorter's model must be set using the [`setModel()`](sorter.controller.ts) method whenever the data changes from an external source. This guide covers different scenarios for managing the model. + +## The setModel() Method + +```typescript +setModel(model: Array | undefined): void +``` + +This method updates the sorter's internal model. Call it whenever: +- Initial data is loaded +- Data is fetched from a server +- Data is updated programmatically (not via drag-and-drop) +- Items are added or removed from the list + +## Scenario 1: Property Setter with Initial Data + +When data is provided via a property and should only be set once: + +```typescript +@customElement('my-sortable-list') +export class MySortableList extends UmbLitElement { + @property({ type: Array, attribute: false }) + public get items(): ModelEntryType[] { + return this._items ?? []; + } + public set items(value: ModelEntryType[]) { + // Only set model initially, prevent re-setting on re-renders + if (this._items !== undefined) return; + this._items = value; + this.#sorter.setModel(this._items); + } + private _items?: ModelEntryType[]; + + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute('data-id'), + getUniqueOfModel: (model) => model.id, + itemSelector: '.item', + containerSelector: '.container', + onChange: ({ model }) => { + const oldValue = this._items; + this._items = model; + this.requestUpdate('items', oldValue); + }, + }); +} +``` + +**Why the guard?** The `if (this._items !== undefined) return;` prevents re-setting the model when Lit re-renders, which would interfere with drag-and-drop operations. + +## Scenario 2: Fetching Data + +When data is loaded asynchronously from a server: + +```typescript +@customElement('my-async-list') +export class MyAsyncList extends UmbLitElement { + @state() + private _items?: ModelEntryType[]; + + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute('data-id'), + getUniqueOfModel: (model) => model.id, + itemSelector: '.item', + containerSelector: '.container', + onChange: ({ model }) => { + this._items = model; + }, + }); + + async connectedCallback() { + super.connectedCallback(); + await this.#loadData(); + } + + async #loadData() { + try { + const response = await fetch('/api/items'); + const items = await response.json(); + this._items = items; + // Set the model after fetching + this.#sorter.setModel(this._items); + } catch (error) { + console.error('Failed to load items:', error); + } + } + + async #refreshData() { + await this.#loadData(); + } + + override render() { + return html` + +
+ ${repeat( + this._items ?? [], + (item) => item.id, + (item) => html` +
+ ${item.name} +
+ ` + )} +
+ `; + } +} +``` + +## Scenario 3: Using willUpdate() + +For reactive updates when dependent properties change: + +```typescript +@customElement('my-reactive-list') +export class MyReactiveList extends UmbLitElement { + @property({ type: String }) + public filter = ''; + + @state() + private _allItems: ModelEntryType[] = []; + + @state() + private _filteredItems: ModelEntryType[] = []; + + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute('data-id'), + getUniqueOfModel: (model) => model.id, + itemSelector: '.item', + containerSelector: '.container', + onChange: ({ model }) => { + // Update the filtered items + this._filteredItems = model; + // Also update the source if needed + this.#updateAllItems(model); + }, + }); + + override willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + + // Update filtered items when filter changes + if (changedProperties.has('filter') || changedProperties.has('_allItems')) { + this._filteredItems = this.#filterItems(this._allItems, this.filter); + // Update the sorter model + this.#sorter.setModel(this._filteredItems); + } + } + + #filterItems(items: ModelEntryType[], filter: string): ModelEntryType[] { + if (!filter) return [...items]; + return items.filter(item => + item.name.toLowerCase().includes(filter.toLowerCase()) + ); + } + + #updateAllItems(sortedFiltered: ModelEntryType[]) { + // Merge sorted filtered items back into all items + // This is application-specific logic + } + + override render() { + return html` + this.filter = (e.target as HTMLInputElement).value} + placeholder="Filter items..."> + +
+ ${repeat( + this._filteredItems, + (item) => item.id, + (item) => html` +
+ ${item.name} +
+ ` + )} +
+ `; + } +} +``` + +## Scenario 4: Manual Item Management + +When adding or removing items programmatically: + +```typescript +@customElement('my-managed-list') +export class MyManagedList extends UmbLitElement { + @state() + private _items: ModelEntryType[] = []; + + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute('data-id'), + getUniqueOfModel: (model) => model.id, + itemSelector: '.item', + containerSelector: '.container', + onChange: ({ model }) => { + this._items = model; + }, + }); + + #addItem() { + const newItem: ModelEntryType = { + id: crypto.randomUUID(), + name: `Item ${this._items.length + 1}` + }; + this._items = [...this._items, newItem]; + // Update sorter model after manual change + this.#sorter.setModel(this._items); + } + + #removeItem(item: ModelEntryType) { + this._items = this._items.filter(i => i.id !== item.id); + // Update sorter model after manual change + this.#sorter.setModel(this._items); + } + + override render() { + return html` + +
+ ${repeat( + this._items, + (item) => item.id, + (item) => html` +
+ ${item.name} + +
+ ` + )} +
+ `; + } +} +``` + +## Best Practices + +1. **Always call `setModel()` after programmatic changes** to the item array +2. **Don't call `setModel()` in the `onChange` callback** - this creates an infinite loop +3. **Use guards in setters** to prevent unnecessary updates during re-renders +4. **Maintain immutability** - create new arrays instead of mutating existing ones +5. **Use `willUpdate()`** for reactive updates based on property changes + +## Common Pitfalls + +❌ **Don't do this:** +```typescript +// Calling setModel in onChange creates a loop +onChange: ({ model }) => { + this._items = model; + this.#sorter.setModel(model); // ❌ Wrong! +} +``` + +❌ **Don't do this:** +```typescript +// Mutating without updating the sorter +#addItem() { + this._items.push(newItem); // ❌ Mutation without setModel +} +``` + +✅ **Do this:** +```typescript +// Create new array and update sorter +#addItem() { + this._items = [...this._items, newItem]; + this.#sorter.setModel(this._items); // ✅ Correct! +} +``` \ No newline at end of file diff --git a/17/umbraco-cms/customizing/utilities/sorting.md b/17/umbraco-cms/customizing/utilities/sorter/sorting.md similarity index 100% rename from 17/umbraco-cms/customizing/utilities/sorting.md rename to 17/umbraco-cms/customizing/utilities/sorter/sorting.md From 40a91d9e610227490b09d8373ce636c792ca1969 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Tue, 25 Nov 2025 14:47:39 +0100 Subject: [PATCH 02/12] Refactor to use willUpdate() for model sync Refactor reactive updates using willUpdate() for better model synchronization and cleaner code. --- .../utilities/sorter/sorter-setting-model.md | 75 +++++++++---------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md index 41e8dfb2628..42210b7b4f7 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md @@ -109,73 +109,64 @@ export class MyAsyncList extends UmbLitElement { } ``` -## Scenario 3: Using willUpdate() +## Scenario 3: Using willUpdate() for Reactive Updates -For reactive updates when dependent properties change: +When you need to synchronize external changes to the sorter model: ```typescript -@customElement('my-reactive-list') -export class MyReactiveList extends UmbLitElement { - @property({ type: String }) - public filter = ''; - - @state() - private _allItems: ModelEntryType[] = []; - - @state() - private _filteredItems: ModelEntryType[] = []; +interface UmbDomainPresentationModel { + unique: string; + domainName: string; + isoCode: string; +} +@customElement('my-async-list') +export class MyAsyncList extends UmbLitElement { #sorter = new UmbSorterController(this, { getUniqueOfElement: (element) => element.getAttribute('data-id'), getUniqueOfModel: (model) => model.id, itemSelector: '.item', containerSelector: '.container', onChange: ({ model }) => { - // Update the filtered items - this._filteredItems = model; - // Also update the source if needed - this.#updateAllItems(model); + const oldValue = this._items; + this._items = model; + this.requestUpdate('_items', oldValue); }, }); - override willUpdate(changedProperties: PropertyValues) { - super.willUpdate(changedProperties); + @state() + private _items?: ModelEntryType[]; - // Update filtered items when filter changes - if (changedProperties.has('filter') || changedProperties.has('_allItems')) { - this._filteredItems = this.#filterItems(this._allItems, this.filter); - // Update the sorter model - this.#sorter.setModel(this._filteredItems); + override willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has('_items')) { + // Sync model whenever _items changes + this.#sorter.setModel(this._items); } } - #filterItems(items: ModelEntryType[], filter: string): ModelEntryType[] { - if (!filter) return [...items]; - return items.filter(item => - item.name.toLowerCase().includes(filter.toLowerCase()) - ); + #addItem() { + const newItem: ModelEntryType = { + id: crypto.randomUUID(), + name: `Item ${this._items.length + 1}` + }; + this._items = [...this._items, newItem]; } - #updateAllItems(sortedFiltered: ModelEntryType[]) { - // Merge sorted filtered items back into all items - // This is application-specific logic + #removeItem(item: ModelEntryType) { + this._items = this._items.filter(i => i.id !== item.id); } override render() { return html` - this.filter = (e.target as HTMLInputElement).value} - placeholder="Filter items..."> - +
${repeat( - this._filteredItems, + this._items, (item) => item.id, (item) => html`
${item.name} +
` )} @@ -185,6 +176,12 @@ export class MyReactiveList extends UmbLitElement { } ``` +**Why use `willUpdate()`?** +- Centralizes model synchronization in one place +- Automatically syncs whenever `_items` changes +- Works for adds, removes, and external updates +- Cleaner than calling `setModel()` in every method that modifies `_items` + ## Scenario 4: Manual Item Management When adding or removing items programmatically: @@ -275,4 +272,4 @@ onChange: ({ model }) => { this._items = [...this._items, newItem]; this.#sorter.setModel(this._items); // ✅ Correct! } -``` \ No newline at end of file +``` From 6c84bccde6817b6390e7c82c18985870ee4074f1 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Tue, 25 Nov 2025 14:49:27 +0100 Subject: [PATCH 03/12] Remove UmbDomainPresentationModel interface Removed the UmbDomainPresentationModel interface definition from the sorter-setting-model.md file. --- .../customizing/utilities/sorter/sorter-setting-model.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md index 42210b7b4f7..585ddfe4fe0 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md @@ -114,12 +114,6 @@ export class MyAsyncList extends UmbLitElement { When you need to synchronize external changes to the sorter model: ```typescript -interface UmbDomainPresentationModel { - unique: string; - domainName: string; - isoCode: string; -} - @customElement('my-async-list') export class MyAsyncList extends UmbLitElement { #sorter = new UmbSorterController(this, { From 3e6011b0ac4942ac2e378d605c8a06248c1a8447 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Tue, 25 Nov 2025 15:37:54 +0100 Subject: [PATCH 04/12] Update sorter-setting-model.md --- .../customizing/utilities/sorter/sorter-setting-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md index 585ddfe4fe0..83b545907d1 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md @@ -69,7 +69,7 @@ export class MyAsyncList extends UmbLitElement { }, }); - async connectedCallback() { + override async connectedCallback() { super.connectedCallback(); await this.#loadData(); } From c2982a61be0ba6e55bfb3bb7c499fae4fba65aa3 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Fri, 28 Nov 2025 13:01:49 +0100 Subject: [PATCH 05/12] Simplify sorter initialization in constructor Removed unnecessary observer for sort mode changes. --- .../utilities/sorter/sorter-enable-disable.md | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md index b41eeca0969..89ba6acc14e 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md @@ -26,28 +26,15 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement { }, }); + @state() private _sortModeActive = false; - constructor() { - super(); - - // Initially disable the sorter - this.#sorter.disable(); - - // Watch for changes to sort mode - this.observe( - this.#designContext.isSorting, - (isSorting) => { - this._sortModeActive = isSorting; - if (isSorting) { - this.#sorter.enable(); - } else { - this.#sorter.disable(); - } - } - ); - } + // Initially disable the sorter + constructor() { + super(); + this.#sorter.disable(); + } #toggleSortMode() { this._sortModeActive = !this._sortModeActive; @@ -88,4 +75,4 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement { - Call [`disable()`](sorter.controller.ts) to prevent sorting interactions - Call [`enable()`](sorter.controller.ts) to re-enable sorting - These methods are idempotent - calling them multiple times has no additional effect -- Disabling the sorter removes all drag event listeners, improving performance when sorting is not needed \ No newline at end of file +- Disabling the sorter removes all drag event listeners, improving performance when sorting is not needed From 0427e6c2ed8d2a6f128cd3ddabb5afc14e4dc60e Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Fri, 28 Nov 2025 13:06:45 +0100 Subject: [PATCH 06/12] Fix markdown formatting in sorter-multiple-containers.md --- .../utilities/sorter/sorter-multiple-containers.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md index 2d11c6b625f..33cfc14cc46 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md @@ -1,6 +1,6 @@ # Sorting Across Multiple Containers -The Sorter supports dragging items between multiple containers by using a shared [`identifier`](sorter.controller.ts). This allows you to create complex drag-and-drop interfaces. +The Sorter supports dragging items between multiple containers by using a shared `identifier`. This allows you to create complex drag-and-drop interfaces. ## Configuration @@ -24,7 +24,7 @@ const sharedIdentifier = 'my-shared-sorter-group'; ## Example: Two Connected Containers -This example from [`sorter-group.ts`](examples/sorter-with-two-containers/sorter-group.ts) shows two independent containers that can exchange items: +This example shows two independent containers that can exchange items: ```typescript export type ModelEntryType = { @@ -101,13 +101,13 @@ export class ExampleSorterGroup extends UmbElementMixin(LitElement) { ## Key Points - **Shared Identifier**: All containers must use the same `identifier` value -- **Independent Models**: Each container maintains its own model via [`setModel()`](sorter.controller.ts) +- **Independent Models**: Each container maintains its own model via `setModel()` - **Automatic Updates**: The `onChange` callback handles model updates when items are moved between containers -- **Item Removal**: When setting the model after removing an item, call [`setModel()`](sorter.controller.ts) to sync the state +- **Item Removal**: When setting the model after removing an item, call `setModel()` to sync the state ## Validation -Use [`onRequestMove`](sorter.controller.ts) to control which items can be dropped into specific containers: +Use `onRequestMove` to control which items can be dropped into specific containers: ```typescript #sorter = new UmbSorterController(this, { @@ -117,4 +117,4 @@ Use [`onRequestMove`](sorter.controller.ts) to control which items can be droppe return item.type === 'allowed-in-this-container'; }, }); -``` \ No newline at end of file +``` From 1d9b4b99343b79f57ae7fda386ffecacf1fbec2f Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Fri, 28 Nov 2025 13:09:37 +0100 Subject: [PATCH 07/12] Fix formatting of sorter enable/disable methods --- .../customizing/utilities/sorter/sorter-enable-disable.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md index 89ba6acc14e..fea97c9c2d3 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md @@ -72,7 +72,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement { ## Key Points - The sorter is **enabled by default** when instantiated -- Call [`disable()`](sorter.controller.ts) to prevent sorting interactions -- Call [`enable()`](sorter.controller.ts) to re-enable sorting +- Call `disable()`to prevent sorting interactions +- Call `enable()` to re-enable sorting - These methods are idempotent - calling them multiple times has no additional effect - Disabling the sorter removes all drag event listeners, improving performance when sorting is not needed From b1215af0f207799172448c9295251346ee452a26 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Fri, 28 Nov 2025 13:10:13 +0100 Subject: [PATCH 08/12] Fix formatting in sorter-configuration.md --- .../customizing/utilities/sorter/sorter-configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md index b3cabb60b47..4f82b33b8bd 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md @@ -1,6 +1,6 @@ # Sorter Configuration Options -The [`UmbSorterController`](sorter.controller.ts) accepts a comprehensive configuration object. This guide covers all available options. +The `UmbSorterController` accepts a comprehensive configuration object. This guide covers all available options. ## Required Configuration @@ -261,4 +261,4 @@ resolvePlacement: ({ pointerY, relatedRect }) => { // Place after if pointer is in bottom half return pointerY > relatedRect.top + relatedRect.height * 0.5; } -``` \ No newline at end of file +``` From fbdde2a9b66735093d304606cfd95e0fc8ed2f56 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Fri, 28 Nov 2025 13:12:15 +0100 Subject: [PATCH 09/12] Fix formatting of the sorter model documentation --- .../customizing/utilities/sorter/sorter-setting-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md index 83b545907d1..6bbe6ff6f9a 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md @@ -1,6 +1,6 @@ # Setting the Sorter Model -The sorter's model must be set using the [`setModel()`](sorter.controller.ts) method whenever the data changes from an external source. This guide covers different scenarios for managing the model. +The sorter's model must be set using the `setModel()` method whenever the data changes from an external source. This guide covers different scenarios for managing the model. ## The setModel() Method From 1bed2d5faab49315ba1033e839a2c9b89ed97a51 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Fri, 28 Nov 2025 13:47:42 +0100 Subject: [PATCH 10/12] Remove common pitfalls from sorter-setting-model.md Removed common pitfalls section and examples for sorter settings. --- .../utilities/sorter/sorter-setting-model.md | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md index 6bbe6ff6f9a..a286013e9b6 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md @@ -239,31 +239,3 @@ export class MyManagedList extends UmbLitElement { 3. **Use guards in setters** to prevent unnecessary updates during re-renders 4. **Maintain immutability** - create new arrays instead of mutating existing ones 5. **Use `willUpdate()`** for reactive updates based on property changes - -## Common Pitfalls - -❌ **Don't do this:** -```typescript -// Calling setModel in onChange creates a loop -onChange: ({ model }) => { - this._items = model; - this.#sorter.setModel(model); // ❌ Wrong! -} -``` - -❌ **Don't do this:** -```typescript -// Mutating without updating the sorter -#addItem() { - this._items.push(newItem); // ❌ Mutation without setModel -} -``` - -✅ **Do this:** -```typescript -// Create new array and update sorter -#addItem() { - this._items = [...this._items, newItem]; - this.#sorter.setModel(this._items); // ✅ Correct! -} -``` From 41e239ce4df219a4de12717ae0e03eacda8aa402 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Fri, 28 Nov 2025 14:29:14 +0100 Subject: [PATCH 11/12] Enhance documentation with usage examples Added examples for sorting across multiple containers. --- .../utilities/sorter/sorter-multiple-containers.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md index 33cfc14cc46..8ca861b17ff 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md @@ -2,6 +2,11 @@ The Sorter supports dragging items between multiple containers by using a shared `identifier`. This allows you to create complex drag-and-drop interfaces. +If you want to test it out and see it used, there are two examples available. +One for a sorter with nested containers, and another for two sorter containers. +It can be found in [Examples and Playground](https://docs.umbraco.com/umbraco-cms/customizing/examples-and-playground) + + ## Configuration To enable cross-container sorting, ensure all sorter instances use the same `identifier`: From af4eb5034f835bf4fce5470733b0e74ccf02028e Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Fri, 28 Nov 2025 14:45:25 +0100 Subject: [PATCH 12/12] Alligning the docs to match the rest of docs --- .../utilities/sorter/sorter-configuration.md | 165 ++++++++++++------ .../utilities/sorter/sorter-enable-disable.md | 40 +++-- .../sorter/sorter-multiple-containers.md | 32 ++-- .../utilities/sorter/sorter-setting-model.md | 96 +++++----- .../customizing/utilities/sorter/sorting.md | 93 +++++----- 5 files changed, 246 insertions(+), 180 deletions(-) diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md index 4f82b33b8bd..17376c66686 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md @@ -1,213 +1,266 @@ # Sorter Configuration Options -The `UmbSorterController` accepts a comprehensive configuration object. This guide covers all available options. +The `UmbSorterController` accepts a configuration object with various options. This article covers all available configuration options. ## Required Configuration ### `getUniqueOfElement` + ```typescript -getUniqueOfElement: (element: ElementType) => string | symbol | number | null | undefined +getUniqueOfElement: (element: ElementType) => + string | symbol | number | null | undefined; ``` + Returns the unique identifier from a DOM element. Return `undefined` or `null` to cancel a move operation. **Example:** + ```typescript -getUniqueOfElement: (element) => element.getAttribute('data-id') +getUniqueOfElement: (element) => element.getAttribute("data-id"); ``` ### `getUniqueOfModel` + ```typescript -getUniqueOfModel: (modelEntry: T) => string | symbol | number | null | undefined +getUniqueOfModel: (modelEntry: T) => + string | symbol | number | null | undefined; ``` + Returns the unique identifier from a model entry. **Example:** + ```typescript -getUniqueOfModel: (model) => model.id +getUniqueOfModel: (model) => model.id; ``` ### `itemSelector` + ```typescript -itemSelector: string +itemSelector: string; ``` -A CSS selector matching the draggable items. + +A CSS selector that matches the draggable items. **Example:** + ```typescript -itemSelector: '.sortable-item' +itemSelector: ".sortable-item"; ``` ## Optional Configuration ### `identifier` + ```typescript identifier?: string | symbol | number ``` -A unique identifier for the sorter instance. All sorters sharing the same identifier can interact with each other (drag items between them). Defaults to a new `Symbol()` if not provided. + +A unique identifier for the sorter instance. All sorters sharing the same identifier can interact with each other (drag items between containers). If not provided, a new `Symbol()` is used by default. **Example:** + ```typescript -identifier: 'my-shared-sorter-group' +identifier: "my-shared-sorter-group"; ``` ### `containerSelector` + ```typescript containerSelector?: string ``` -A CSS selector for the container element. If not provided, the host element is used. + +A CSS selector for the container element. If not provided, the host element is used as the container. **Example:** + ```typescript -containerSelector: '.items-container' +containerSelector: ".items-container"; ``` ### `disabledItemSelector` + ```typescript disabledItemSelector?: string ``` -A CSS selector for items that should not be draggable. + +A CSS selector for items that cannot be dragged. **Example:** + ```typescript -disabledItemSelector: '.disabled-item' +disabledItemSelector: ".disabled-item"; ``` ### `ignorerSelector` + ```typescript ignorerSelector?: string ``` -A CSS selector for elements within items that should not trigger dragging. Defaults to `'a,img,iframe,input,textarea,select,option'`. + +A CSS selector for elements within items that do not trigger dragging. The default value is `'a,img,iframe,input,textarea,select,option'`. **Example:** + ```typescript -ignorerSelector: 'a,button,input' +ignorerSelector: "a,button,input"; ``` ### `placeholderClass` + ```typescript placeholderClass?: string ``` -CSS class applied to the dragged element while dragging. + +CSS class applied to the dragged element during the drag operation. **Example:** + ```typescript -placeholderClass: 'dragging-placeholder' +placeholderClass: "dragging-placeholder"; ``` ### `placeholderAttr` + ```typescript placeholderAttr?: string ``` -Attribute set on the dragged element while dragging. If neither `placeholderClass` nor `placeholderAttr` is provided, defaults to `'drag-placeholder'`. + +Attribute set on the dragged element during the drag operation. If neither `placeholderClass` nor `placeholderAttr` is provided, the default value is `'drag-placeholder'`. **Example:** + ```typescript -placeholderAttr: 'data-dragging' +placeholderAttr: "data-dragging"; ``` ### `draggableSelector` + ```typescript draggableSelector?: string ``` -CSS selector for the specific element within an item that should be draggable. Useful for adding a drag handle. + +CSS selector for the specific element within an item that can be dragged. This is useful for adding a drag handle. **Example:** + ```typescript -draggableSelector: '.drag-handle' +draggableSelector: ".drag-handle"; ``` ### `handleSelector` + ```typescript handleSelector?: string ``` -CSS selector for the interactive handle within an item. Only this element can initiate dragging. + +CSS selector for the interactive handle within an item. Only this element can initiate the drag operation. **Example:** + ```typescript -handleSelector: '.drag-handle-button' +handleSelector: ".drag-handle-button"; ``` ## Callback Configuration ### `onChange` + ```typescript onChange?: (args: { item: T; model: Array }) => void ``` -Called whenever the model changes. Not called if more specific callbacks are provided. + +Called when the model changes. This callback is not invoked if more specific callbacks are provided. **Example:** + ```typescript onChange: ({ model, item }) => { - console.log('Model changed:', model); + console.log("Model changed:", model); this._items = model; -} +}; ``` ### `onContainerChange` + ```typescript -onContainerChange?: (args: { - item: T; - model: Array; - from: UmbSorterController | undefined +onContainerChange?: (args: { + item: T; + model: Array; + from: UmbSorterController | undefined }) => void ``` -Called when an item moves from another container to this one. + +Called when an item is moved from another container to this container. **Example:** + ```typescript onContainerChange: ({ item, model, from }) => { console.log(`Item ${item.id} moved from another container`); this._items = model; -} +}; ``` ### `onStart` + ```typescript onStart?: (args: { item: T; element: ElementType }) => void ``` -Called when dragging starts. + +Called when a drag operation starts. **Example:** + ```typescript onStart: ({ item }) => { - console.log('Started dragging:', item.name); -} + console.log("Started dragging:", item.name); +}; ``` ### `onEnd` + ```typescript onEnd?: (args: { item: T; element: ElementType }) => void ``` -Called when dragging ends. + +Called when a drag operation ends. **Example:** + ```typescript onEnd: ({ item }) => { - console.log('Finished dragging:', item.name); -} + console.log("Finished dragging:", item.name); +}; ``` ### `onRequestMove` + ```typescript onRequestMove?: (args: { item: T }) => boolean ``` -Called to validate if an item can be moved into this container. Return `false` to prevent the move. + +Called to validate whether an item can be moved into this container. Return `false` to prevent the move operation. **Example:** + ```typescript onRequestMove: ({ item }) => { - return item.type === 'allowed-type'; -} + return item.type === "allowed-type"; +}; ``` ### `onDisallowed` / `onAllowed` + ```typescript onDisallowed?: (args: { item: T; element: ElementType }) => void onAllowed?: (args: { item: T; element: ElementType }) => void ``` -Visual feedback callbacks for when moves are disallowed/allowed. + +Callbacks for providing visual feedback when moves are disallowed or allowed. **Example:** + ```typescript onDisallowed: ({ element }) => { element.classList.add('drop-not-allowed'); @@ -220,45 +273,55 @@ onAllowed: ({ element }) => { ## Advanced Callbacks ### `performItemMove` + ```typescript -performItemMove?: (args: { - item: T; - newIndex: number; - oldIndex: number +performItemMove?: (args: { + item: T; + newIndex: number; + oldIndex: number }) => Promise | boolean ``` -Custom handler for moving items within the same container. Return `false` to cancel. + +Custom handler for moving items within the same container. Return `false` to cancel the move operation. **Example:** + ```typescript performItemMove: async ({ item, newIndex, oldIndex }) => { await this.saveToServer(item, newIndex); return true; -} +}; ``` ### `performItemInsert` + ```typescript performItemInsert?: (args: { item: T; newIndex: number }) => Promise | boolean ``` + Custom handler for inserting items into the container. ### `performItemRemove` + ```typescript performItemRemove?: (args: { item: T }) => Promise | boolean ``` + Custom handler for removing items from the container. ### `resolvePlacement` + ```typescript resolvePlacement?: (args: UmbSorterResolvePlacementArgs) => UmbSorterResolvePlacementReturn ``` -Custom logic for determining where to place an item during drag. Return `true` to place after, `false` to place before, or `null` to cancel. + +Custom logic for determining where to place an item during a drag operation. Return `true` to place after, `false` to place before, or `null` to cancel. **Example:** + ```typescript resolvePlacement: ({ pointerY, relatedRect }) => { // Place after if pointer is in bottom half return pointerY > relatedRect.top + relatedRect.height * 0.5; -} +}; ``` diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md index fea97c9c2d3..88781b84094 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md @@ -1,40 +1,41 @@ # Enabling and Disabling the Sorter -The Sorter can be dynamically enabled or disabled based on your application's state. This is useful when you want to toggle between viewing and editing modes. +The Sorter can be dynamically enabled or disabled based on your application's state. This is useful when toggling between viewing and editing modes. ## Methods ### `enable()` + Enables the sorter, allowing sorting interactions to occur. ### `disable()` + Disables the sorter, preventing any sorting interactions. -## Example Usage +## Example The following example shows how to toggle the sorter based on a "sort mode" state: ```typescript export class UmbContentTypeDesignEditorElement extends UmbLitElement { #sorter = new UmbSorterController(this, { - getUniqueOfElement: (element) => element.getAttribute('data-id'), + getUniqueOfElement: (element) => element.getAttribute("data-id"), getUniqueOfModel: (model) => model.id, - itemSelector: '.sortable-item', - containerSelector: '.sortable-container', + itemSelector: ".sortable-item", + containerSelector: ".sortable-container", onChange: ({ model }) => { this._items = model; }, }); - @state() private _sortModeActive = false; // Initially disable the sorter - constructor() { - super(); - this.#sorter.disable(); - } + constructor() { + super(); + this.#sorter.disable(); + } #toggleSortMode() { this._sortModeActive = !this._sortModeActive; @@ -47,12 +48,13 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement { override render() { return html` - - ${this._sortModeActive ? 'Done' : 'Sort'} + label=${this._sortModeActive ? "Done Sorting" : "Sort Items"} + > + ${this._sortModeActive ? "Done" : "Sort"} - +
${repeat( this._items, @@ -71,8 +73,8 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement { ## Key Points -- The sorter is **enabled by default** when instantiated -- Call `disable()`to prevent sorting interactions -- Call `enable()` to re-enable sorting -- These methods are idempotent - calling them multiple times has no additional effect -- Disabling the sorter removes all drag event listeners, improving performance when sorting is not needed +- The sorter is **enabled by default** when instantiated. +- Call `disable()` to prevent sorting interactions. +- Call `enable()` to re-enable sorting. +- These methods are idempotent - calling them multiple times has no additional effect. +- Disabling the sorter removes all drag event listeners, improving performance when sorting is not needed. diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md index 8ca861b17ff..40500a21d44 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md @@ -2,17 +2,14 @@ The Sorter supports dragging items between multiple containers by using a shared `identifier`. This allows you to create complex drag-and-drop interfaces. -If you want to test it out and see it used, there are two examples available. -One for a sorter with nested containers, and another for two sorter containers. -It can be found in [Examples and Playground](https://docs.umbraco.com/umbraco-cms/customizing/examples-and-playground) - +You can test this functionality using the examples available in the [Examples and Playground](https://docs.umbraco.com/umbraco-cms/customizing/examples-and-playground). Two examples are available: one for a sorter with nested containers, and another for two sorter containers. ## Configuration To enable cross-container sorting, ensure all sorter instances use the same `identifier`: ```typescript -const sharedIdentifier = 'my-shared-sorter-group'; +const sharedIdentifier = "my-shared-sorter-group"; // Container 1 #sorter1 = new UmbSorterController(this, { @@ -36,7 +33,7 @@ export type ModelEntryType = { name: string; }; -@customElement('example-sorter-group') +@customElement("example-sorter-group") export class ExampleSorterGroup extends UmbElementMixin(LitElement) { @property({ type: Array, attribute: false }) public get items(): ModelEntryType[] { @@ -54,13 +51,13 @@ export class ExampleSorterGroup extends UmbElementMixin(LitElement) { getUniqueOfElement: (element) => element.name, getUniqueOfModel: (modelEntry) => modelEntry.name, // This identifier connects all sorter instances - identifier: 'string-that-identifies-all-example-sorters', - itemSelector: 'example-sorter-item', - containerSelector: '.sorter-container', + identifier: "string-that-identifies-all-example-sorters", + itemSelector: "example-sorter-item", + containerSelector: ".sorter-container", onChange: ({ model }) => { const oldValue = this._items; this._items = model; - this.requestUpdate('items', oldValue); + this.requestUpdate("items", oldValue); }, }); @@ -77,7 +74,10 @@ export class ExampleSorterGroup extends UmbElementMixin(LitElement) { (item) => item.name, (item) => html` - @@ -105,10 +105,10 @@ export class ExampleSorterGroup extends UmbElementMixin(LitElement) { ## Key Points -- **Shared Identifier**: All containers must use the same `identifier` value -- **Independent Models**: Each container maintains its own model via `setModel()` -- **Automatic Updates**: The `onChange` callback handles model updates when items are moved between containers -- **Item Removal**: When setting the model after removing an item, call `setModel()` to sync the state +- **Shared Identifier**: All containers must use the same `identifier` value. +- **Independent Models**: Each container maintains its own model via `setModel()`. +- **Automatic Updates**: The `onChange` callback handles model updates when items are moved between containers. +- **Item Removal**: When setting the model after removing an item, call `setModel()` to synchronize the state. ## Validation @@ -119,7 +119,7 @@ Use `onRequestMove` to control which items can be dropped into specific containe // ... other config onRequestMove: ({ item }) => { // Only allow items of a specific type - return item.type === 'allowed-in-this-container'; + return item.type === "allowed-in-this-container"; }, }); ``` diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md index a286013e9b6..a21c3524296 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md @@ -1,6 +1,6 @@ # Setting the Sorter Model -The sorter's model must be set using the `setModel()` method whenever the data changes from an external source. This guide covers different scenarios for managing the model. +The sorter's model must be set using the `setModel()` method when data changes from an external source. This article covers different scenarios for managing the model. ## The setModel() Method @@ -8,18 +8,19 @@ The sorter's model must be set using the `setModel()` method whenever the data c setModel(model: Array | undefined): void ``` -This method updates the sorter's internal model. Call it whenever: -- Initial data is loaded -- Data is fetched from a server -- Data is updated programmatically (not via drag-and-drop) -- Items are added or removed from the list +This method updates the sorter's internal model. Call it when: + +- Initial data is loaded +- Data is fetched from a server +- Data is updated programmatically (not via drag-and-drop) +- Items are added or removed from the list ## Scenario 1: Property Setter with Initial Data When data is provided via a property and should only be set once: ```typescript -@customElement('my-sortable-list') +@customElement("my-sortable-list") export class MySortableList extends UmbLitElement { @property({ type: Array, attribute: false }) public get items(): ModelEntryType[] { @@ -34,36 +35,36 @@ export class MySortableList extends UmbLitElement { private _items?: ModelEntryType[]; #sorter = new UmbSorterController(this, { - getUniqueOfElement: (element) => element.getAttribute('data-id'), + getUniqueOfElement: (element) => element.getAttribute("data-id"), getUniqueOfModel: (model) => model.id, - itemSelector: '.item', - containerSelector: '.container', + itemSelector: ".item", + containerSelector: ".container", onChange: ({ model }) => { const oldValue = this._items; this._items = model; - this.requestUpdate('items', oldValue); + this.requestUpdate("items", oldValue); }, }); } ``` -**Why the guard?** The `if (this._items !== undefined) return;` prevents re-setting the model when Lit re-renders, which would interfere with drag-and-drop operations. +**Why the guard?** The `if (this._items !== undefined) return;` statement prevents re-setting the model when Lit re-renders, which would interfere with drag-and-drop operations. ## Scenario 2: Fetching Data When data is loaded asynchronously from a server: ```typescript -@customElement('my-async-list') +@customElement("my-async-list") export class MyAsyncList extends UmbLitElement { @state() private _items?: ModelEntryType[]; #sorter = new UmbSorterController(this, { - getUniqueOfElement: (element) => element.getAttribute('data-id'), + getUniqueOfElement: (element) => element.getAttribute("data-id"), getUniqueOfModel: (model) => model.id, - itemSelector: '.item', - containerSelector: '.container', + itemSelector: ".item", + containerSelector: ".container", onChange: ({ model }) => { this._items = model; }, @@ -76,13 +77,13 @@ export class MyAsyncList extends UmbLitElement { async #loadData() { try { - const response = await fetch('/api/items'); + const response = await fetch("/api/items"); const items = await response.json(); this._items = items; // Set the model after fetching this.#sorter.setModel(this._items); } catch (error) { - console.error('Failed to load items:', error); + console.error("Failed to load items:", error); } } @@ -98,9 +99,7 @@ export class MyAsyncList extends UmbLitElement { this._items ?? [], (item) => item.id, (item) => html` -
- ${item.name} -
+
${item.name}
` )}
@@ -114,17 +113,17 @@ export class MyAsyncList extends UmbLitElement { When you need to synchronize external changes to the sorter model: ```typescript -@customElement('my-async-list') +@customElement("my-async-list") export class MyAsyncList extends UmbLitElement { #sorter = new UmbSorterController(this, { - getUniqueOfElement: (element) => element.getAttribute('data-id'), + getUniqueOfElement: (element) => element.getAttribute("data-id"), getUniqueOfModel: (model) => model.id, - itemSelector: '.item', - containerSelector: '.container', + itemSelector: ".item", + containerSelector: ".container", onChange: ({ model }) => { const oldValue = this._items; this._items = model; - this.requestUpdate('_items', oldValue); + this.requestUpdate("_items", oldValue); }, }); @@ -132,7 +131,7 @@ export class MyAsyncList extends UmbLitElement { private _items?: ModelEntryType[]; override willUpdate(changedProperties: PropertyValues) { - if (changedProperties.has('_items')) { + if (changedProperties.has("_items")) { // Sync model whenever _items changes this.#sorter.setModel(this._items); } @@ -141,13 +140,13 @@ export class MyAsyncList extends UmbLitElement { #addItem() { const newItem: ModelEntryType = { id: crypto.randomUUID(), - name: `Item ${this._items.length + 1}` + name: `Item ${this._items.length + 1}`, }; this._items = [...this._items, newItem]; } #removeItem(item: ModelEntryType) { - this._items = this._items.filter(i => i.id !== item.id); + this._items = this._items.filter((i) => i.id !== item.id); } override render() { @@ -160,7 +159,9 @@ export class MyAsyncList extends UmbLitElement { (item) => html`
${item.name} - +
` )} @@ -171,26 +172,27 @@ export class MyAsyncList extends UmbLitElement { ``` **Why use `willUpdate()`?** -- Centralizes model synchronization in one place -- Automatically syncs whenever `_items` changes -- Works for adds, removes, and external updates -- Cleaner than calling `setModel()` in every method that modifies `_items` + +- Centralizes model synchronization in one location +- Automatically synchronizes when `_items` changes +- Works for additions, removals, and external updates +- Cleaner than calling `setModel()` in every method that modifies `_items` ## Scenario 4: Manual Item Management When adding or removing items programmatically: ```typescript -@customElement('my-managed-list') +@customElement("my-managed-list") export class MyManagedList extends UmbLitElement { @state() private _items: ModelEntryType[] = []; #sorter = new UmbSorterController(this, { - getUniqueOfElement: (element) => element.getAttribute('data-id'), + getUniqueOfElement: (element) => element.getAttribute("data-id"), getUniqueOfModel: (model) => model.id, - itemSelector: '.item', - containerSelector: '.container', + itemSelector: ".item", + containerSelector: ".container", onChange: ({ model }) => { this._items = model; }, @@ -199,7 +201,7 @@ export class MyManagedList extends UmbLitElement { #addItem() { const newItem: ModelEntryType = { id: crypto.randomUUID(), - name: `Item ${this._items.length + 1}` + name: `Item ${this._items.length + 1}`, }; this._items = [...this._items, newItem]; // Update sorter model after manual change @@ -207,7 +209,7 @@ export class MyManagedList extends UmbLitElement { } #removeItem(item: ModelEntryType) { - this._items = this._items.filter(i => i.id !== item.id); + this._items = this._items.filter((i) => i.id !== item.id); // Update sorter model after manual change this.#sorter.setModel(this._items); } @@ -222,7 +224,9 @@ export class MyManagedList extends UmbLitElement { (item) => html`
${item.name} - +
` )} @@ -234,8 +238,8 @@ export class MyManagedList extends UmbLitElement { ## Best Practices -1. **Always call `setModel()` after programmatic changes** to the item array -2. **Don't call `setModel()` in the `onChange` callback** - this creates an infinite loop -3. **Use guards in setters** to prevent unnecessary updates during re-renders -4. **Maintain immutability** - create new arrays instead of mutating existing ones -5. **Use `willUpdate()`** for reactive updates based on property changes +1. **Always call `setModel()` after programmatic changes** to the item array. +2. **Do not call `setModel()` in the `onChange` callback** - this creates an infinite loop. +3. **Use guards in setters** to prevent unnecessary updates during re-renders. +4. **Maintain immutability** - create new arrays instead of mutating existing ones. +5. **Use `willUpdate()`** for reactive updates based on property changes. diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorting.md b/17/umbraco-cms/customizing/utilities/sorter/sorting.md index 1e7449deeee..db9a81cbdd3 100644 --- a/17/umbraco-cms/customizing/utilities/sorter/sorting.md +++ b/17/umbraco-cms/customizing/utilities/sorter/sorting.md @@ -8,14 +8,13 @@ description: Enable sorting elements via drag and drop This page is a work in progress and may undergo further revisions, updates, or amendments. The information contained herein is subject to change without notice. {% endhint %} -The Umbraco Sorter enables you to make a list of elements sortable via drag-and-drop interaction. You have to set up the sorter once on the Element that renders the items to be sorted. As part of the configuration, you shall provide an `onChange` callback method, which will be executed every time the sorter makes a difference to the data. +The Umbraco Sorter enables you to make a list of elements sortable via drag-and-drop interaction. You must set up the sorter once on the element that renders the items to be sorted. As part of the configuration, you provide an `onChange` callback method that is executed every time the sorter changes the data. -### Configuration +## Configuration -The following example shows a basic setup of the Sorter. +The following example shows a basic setup of the Sorter: ```typescript - type ModelEntryType = { id: string; name: string; @@ -38,59 +37,57 @@ this.#sorter = new UmbSorterController(this, { }); ``` -The properties provided are the following: +The configuration properties are: -* `itemSelector`: A query selector that matches the items that should be draggable. -* `containerSelector`: A query elector that matches the parent element of the items. -* `getUniqueOfElement`: A method that returns the unique element -* `getUniqueOfModel`: Provide a method that returns the unique of a given model entry -* `onChange`: Provide a method to retrieve the changed model. This is called every time the model is changed, including when the user is dragging around. +* `itemSelector`: A query selector that matches the items that can be dragged. +* `containerSelector`: A query selector that matches the parent element of the items. +* `getUniqueOfElement`: A method that returns the unique identifier of an element. +* `getUniqueOfModel`: A method that returns the unique identifier of a model entry. +* `onChange`: A method to retrieve the changed model. This is called every time the model changes, including when the user is dragging items. -### Data Model +## Data Model -The model given to the Sorter must be an Array. The following example extends the example from above: +The model provided to the Sorter must be an Array. The following example extends the previous example: ```typescript - - const model: Array = [ - { - id: 1, - name: 'First item' - }, - { - id: 2, - name: 'second item' - } - { - id: 3, - name: 'Third item' - } - ] - - // Set the Model, if you have changes to the model not coming from the Sorter. Then set the model again: - this.#sorter.setModel(model); +const model: Array = [ + { + id: '1', + name: 'First item' + }, + { + id: '2', + name: 'Second item' + }, + { + id: '3', + name: 'Third item' + } +] + +// Set the Model. If you have changes to the model not coming from the Sorter, set the model again: +this.#sorter.setModel(model); ``` -### Rendering +## Rendering -The Sorter does not move elements, instead, it updates the model as the user drags an item around. This puts higher pressure on the rendering of the sortable Elements. This means we need to make sure that the rendering re-uses the same element despite sorting the data differently. +The Sorter does not move elements. Instead, it updates the model as the user drags an item. This places higher demands on the rendering of the sortable elements. You must ensure that the rendering reuses the same element despite sorting the data differently. -Lit does provide a render helper method called `repeat` that does this for us. The following example shows a render method that continues the work of the examples above: +Lit provides a render helper method called `repeat` that handles this. The following example shows a render method that continues the previous examples: ```typescript - - - render() { - return html` - - ${repeat( - this._items, - (item) => item.id, - (item) => - html`${item.name} - `, - )} - - `; - } +render() { + return html` +
+ ${repeat( + this._items, + (item) => item.id, + (item) => + html`
+ ${item.name} +
`, + )} +
+ `; +} ```