0

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();

CodePen

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?

2 Answers 2

0

After a long struggle of trial, error and further searching I came to the conclusion that modifying the rules after sorting was causing the DOM issues. All the data was correct, but the DOM wouldn't properly show it. I came to this issue which suggested using Alpine.raw:

 initSortable() {
      const rulesContainer = document.querySelector('.configurator') as HTMLElement;
      Sortable.create(rulesContainer, {
        handle: '.rule__sort',
        onEnd: (event: SortableEvent) => {
          console.log(event);
          let rules = Alpine.raw(this.rules);
          const movedRule = rules.splice(event.oldIndex!, 1)[0];
          rules.splice(event.newIndex!, 0, movedRule);
          this.rules = rules;
          this.updateOutput();
        }
      });
    },

This is not in the docs, but it basically seems to remove the proxy wrapper from the data, which allows you to modify it without modifying the proxy instance which seems to be what's causing the issue.

If you were to not unwrap (let rules = this.rules), and you modify rules, it would also still modify this.rules.

Note: I did get some other reordering issues:

  • I was able to move the data below the buttons
  • Adding new rows after reorder jumped the order, potentially due to the key being index based (perhaps add a random ID based on datetime)
Sign up to request clarification or add additional context in comments.

Comments

-1

this.rules.splice does not mutate the original array, so maybe that is why it is not updating. See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice

Instead try this: this.rules = this.rules.splice

1 Comment

That seems to wipe all of the added rules in both the UI and the output JSON. I don't think it was the problem since it did modify the JSON object being output into the textarea correctly.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.