Search code examples
arrayslaravelvue.jssynchronizationdraggable

Laravel, VueJS - keeping 2 arrays in sync while dragging & dropping


I currently have code referencing vuejs packages to handle dragging & dropping and it IS WORKING.

Packages:

  • src="//cdn.jsdelivr.net/npm/sortablejs@1.8.4/Sortable.min.js"
  • src="//cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/2.19.2/vuedraggable.umd.min.js"
  • src="//unpkg.com/axios/dist/axios.min.js"

I have a series of checkbox items all bound to a single v-model named "choices."

Snippet of template code:

        <div class="form-group">  // One of the labels for the checkboxes...
            <div class="col-sm-2" style="text-align: center">
                <label class="control-label">@lang('messages.fields.category')</label>
                @include('v1.parts.tooltip', ['title' => trans('messages.admin.api.api_info_cat')])
            </div>

            // Other labels for checkboxes not shown
        </div>
        <div class="form-group">  // One of the checkboxes... others not shown
            <div class="col-sm-2">
                <input type="checkbox" class="form-control input-sm" v-model="choices"
                       id="{{ trans('messages.fields.category') }}"
                       value="{{ trans('messages.fields.category') }}"
                       name="category">
            </div>
            // Other  checkboxes not shown

    <div class="col-sm-4">
        <p>
            @lang('messages.admin.api.api_instr2')
        </p>

       // This is the portion to the right of screenshot, populated as boxes are checked

        <draggable tag="ul" v-model="choices" @start="drag=true" @end="drag=false" handle=".handle">
            <div class="list-group-item" v-for="choice in choices">
                @{{ choice }} <i class="handle fas fa-arrows-alt pull-right"></i>
            </div>
        </draggable>

    </div>

screenshot of the checkboxes and sortable list

The checkbox values are currently human-readable English (though currently using localization) words that, when checked, show up in a list that can then be dragged and dropped for manual sorting. As the checkboxes are checked and/or the list is sorted, the red text (screenshot) labeled "Event Text" updates.

I want to keep that functionality (using the human-readable items so that they're displayed) but I also want to have a set of corresponding values in an array that hold single-word names (aka variable names) for easier reference, handles for localization, and other processing in another area of my application.

So I want two arrays that stay in sync based on the actions that a user takes on the array that is getting dragged & dropped.

I hope what I'm trying to achieve makes sense. The alternative--parsing the currently-English words--is possible but will break once localization is introduced because then the displayed words would not be English.

Thanks in advance.


Solution

  • I've kept this open for 17d and have been working the issue. I'm posting the solution I've come up with to help anyone else who might run into this in the future AND/OR to prompt anyone with any better ideas that might be more efficient.

    I refactored the code to manage the display of the checkboxes in this snippet of template code:

                <div class="form-group">
                    <div v-for="item in checkbox_list" class="col-sm-2" style="text-align: center">
                        <label class="control-label">@{{ item.value }}</label>
                        <a v-if="item.instr" data-toggle="tooltip" :title="item.instr" data-placement="top">
                            <i class="fas fa-info-square purple"></i>
                        </a>
                    </div>
                </div>
                <div class="form-group">
                    <div v-for="item in checkbox_list" class="col-sm-2">
                        <input type="checkbox" class="form-control input-sm" v-model="choices"
                               :id="item.id"
                               :value="item.value"
                               :name="item.id">
                    </div>
                </div>
    

    My Vue code is as follows:

        new Vue({
            el: '#el',
            data: {
                admin_props: @json($admin_props),
                separator: '{{ $org_props[0]->value }}',
                header: '{{ $org_props[1]->value }}',
                choices: [],
                drag: false,
                checkbox_list: [
                    {sort: 0, checked: 0, value: "@lang('messages.fields.category')", id: "category", instr: '@lang('messages.admin.api.api_info_cat')'},
                    {sort: 0, checked: 0, value: "{!! trans_choice('messages.headers.et', 1) !!}", id: "et", instr: '@lang('messages.admin.api.api_info_et')'},
                    {sort: 0, checked: 0, value: "@lang('messages.admin.api.api_times')", id: "api_times"},
                    {sort: 0, checked: 0, value: "@lang('messages.headers.pdu_detail')", id: "pdu_detail"},
                    {sort: 0, checked: 0, value: "@lang('messages.fields.memprice')", id: "memprice"},
                    {sort: 0, checked: 0, value: "@lang('messages.fields.nonprice')", id: "nonprice"},
                ],
            },
    
            mounted: function () {
                this.choices = this.admin_props[1].value.split(' ' + this.separator + ' ',);
                this.varArray_update();
            },
    
            watch: {
                choices: function () {
                    this.admin_props[1].value = this.choices.join(' ' + this.separator + ' ');
                    this.api_update('header', this.admin_props[1].value);
                    this.varArray_update();
                }
            },
            methods: {
                api_update: function (name, value) {
    
                    axios.post('/panel/update', {
                        name: name,
                        value: value,
                    })
                        .catch(error => console.log(error.response));
                },
    
                varArray_update: function() {
                    var index = 0;
                    this.checkbox_list.forEach(e => {
                        e.checked = 0;
                        e.sort = 0;
                    });
    
                    this.choices.forEach(element => {
                        index += 1;
                        console.log(index, element);
                        var x = this.checkbox_list.filter(function(ele) {
                            if(ele.value == element) {
                                ele.checked = 1;
                                ele.sort = index;
                            }
                        });
                    });
                    var out = [];
                    var array_copy = this.checkbox_list;
                    array_copy = array_copy.filter(function(v){ return v.checked == 1; });
                    array_copy.sort((a, b) => (a.sort > b.sort) ? 1 : -1);
                    array_copy.forEach(function(v) { out.push(v.id); });
                    var id_hdr = out.join(this.separator);
                    this.api_update('var_array', id_hdr);
                },
            }
        });
    

    The choices array is auto-managed by the Vue package for sortable and when it's updated via the drag/drop UI, the sort order and such is getting maintained (as expected).

    Because I needed to replicate that with corresponding variable names associated with the display names, I set up the checkbox_list object to store the name/value pairs and added on a checked state, sort order and instr (instruction text only used during the initial setup of the display)

    I have a watch setup on choices such that it updates both the header (used for display) and the checkbox_list array. I then manipulate a copy of that array to get down to an array of variable names that is sorted according to the corresponding display values in choices.

    If there is a more efficient way to do this, I would love to hear about it.