I have an AlpineJS UI which allows an editor to add style rules or groups of style rules (it's not really important what these are but I've kept the concept so as to differentiate from a more simple application).
After adding rules and rule groups you can sort them by dragging/dropping. It uses SortableJS for this.
The rules created are output in JSON format so they can be copied/pasted into a third-party application.
<div class="configurator" x-data="ruleConfigurator()" x-init="init()">
<template x-for="(rule, index) in rules" :key="index">
<div class="rule-wrapper">
<template x-if="rule.type === 'single'">
<div class="rule rule--single">
<div class="rule__cell rule__cell--title">
<input type="text" placeholder="e.g. Heading 1" x-model="rule.title" @input="updateOutput">
</div>
<div class="rule__cell rule__cell--type">
<select x-model="rule.category" @change="updateOutput">
<option value="block">Block</option>
<option value="inline">Inline</option>
<option value="selector">Selector</option>
</select>
</div>
<div class="rule__cell rule__cell--selector">
<input type="text" placeholder="e.g. h1" x-model="rule.selector" @input="updateOutput">
</div>
<div class="rule__cell rule__cell--classes">
<input type="text" placeholder="e.g. heading-primary" x-model="rule.classes" @input="updateOutput">
</div>
<button class="rule__delete" @click="removeRule(index)">X</button>
<button class="rule__sort">Sort</button>
</div>
</template>
<template x-if="rule.type === 'group'">
<div class="rule rule--group">
<div class="rule__cell rule__cell--group-title">
<input type="text" placeholder="e.g. Headings" x-model="rule.title" @input="updateOutput">
</div>
<template x-for="(groupRule, groupRuleIndex) in rule.groupRules" :key="groupRuleIndex">
<div class="rule__cell rule__cell--rules">
<div class="rule rule--single">
<div class="rule__cell rule__cell--title">
<input type="text" placeholder="title" x-model="groupRule.title" @input="updateOutput">
</div>
<div class="rule__cell rule__cell--type">
<select x-model="groupRule.category" @change="updateOutput">
<option value="block">Block</option>
<option value="inline">Inline</option>
<option value="selector">Selector</option>
</select>
</div>
<div class="rule__cell rule__cell--selector">
<input type="text" placeholder="selector" x-model="groupRule.selector" @input="updateOutput">
</div>
<div class="rule__cell rule__cell--classes">
<input type="text" placeholder="classes" x-model="groupRule.classes" @input="updateOutput">
</div>
<button class="rule__delete" @click="removeGroupRule(index, groupRuleIndex)">X</button>
</div>
</div>
</template>
<div class="add-rules">
<button class="add-rules__btn btn js-add-style-to-group" @click="addRuleToGroup(index)">Add Style To Group</button>
</div>
<button class="rule__delete" @click="removeRule(index)">X</button>
<button class="rule__sort">Sort</button>
</div>
</template>
</div>
</template>
<div class="add-rules">
<button @click="addRule('single')" class="add-rules__btn btn">Add Single Style</button>
<button @click="addRule('group')" class="add-rules__btn btn">Add Style Group</button>
</div>
<div class="output">
<p>Output:</p>
<textarea readonly x-text="output" class="output-ui__textarea js-output"></textarea>
</div>
</div>
</div>
import Alpine from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';
import Sortable, { SortableEvent } from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';
window.Alpine = Alpine;
interface BaseRule {
title: string;
category: 'block' | 'inline' | 'selector';
selector: string;
classes: string;
}
interface SingleRule extends BaseRule {
type: 'single';
}
interface GroupRule extends BaseRule {}
interface Group extends BaseRule {
type: 'group';
groupRules: GroupRule[];
}
type Rule = SingleRule | Group;
function ruleConfigurator() {
return {
rules: [] as Rule[],
output: '',
init() {
this.initSortable();
this.updateOutput();
},
initSortable() {
const rulesContainer = document.querySelector('.configurator') as HTMLElement;
Sortable.create(rulesContainer, {
handle: '.rule__sort',
onEnd: (event: SortableEvent) => {
console.log(event);
const movedRule = this.rules.splice(event.oldIndex!, 1)[0];
this.rules.splice(event.newIndex!, 0, movedRule);
this.updateOutput();
}
});
},
addRule(type: 'single' | 'group') {
if (type === 'single') {
this.rules.push({
type: 'single',
title: '',
category: 'block',
selector: '',
classes: ''
} as SingleRule);
} else {
this.rules.push({
type: 'group',
title: '',
category: 'block',
selector: '',
classes: '',
groupRules: [
{
title: '',
category: 'block',
selector: '',
classes: ''
}
]
} as Group);
}
this.updateOutput();
},
addRuleToGroup(ruleIndex: number) {
const rule = this.rules[ruleIndex];
if (rule.type === 'group') {
rule.groupRules.push({
title: '',
category: 'block',
selector: '',
classes: ''
});
this.updateOutput();
}
},
removeRule(index: number) {
this.rules.splice(index, 1);
this.updateOutput();
},
removeGroupRule(ruleIndex: number, groupRuleIndex: number) {
const rule = this.rules[ruleIndex];
if (rule.type === 'group') {
rule.groupRules.splice(groupRuleIndex, 1);
this.updateOutput();
}
},
updateOutput() {
this.output = JSON.stringify(this.rules, null, 2);
}
};
}
document.addEventListener('alpine:init', () => {
Alpine.data('ruleConfigurator', ruleConfigurator);
});
Alpine.start();
The issue I'm facing is with sorting. The code in the onEnd callback updates the rules object so that the JSON output shows the rules in the correct order, however the UI does not update — after dragging a rule to a new position the UI reverts back to how it was originally. If I comment out the stuff in the onEnd callback the UI sorting works correctly but the JSON object doesn't change hence is now in the wrong order.
I'd like the two things to be in sync so that the UI updates correctly when sorted and the JSON is also updated to reflect the new order.
Why this is happening and how can I fix it?