• jQuery Sortable update output on input

    455 Views Asked by At

    We're using the jQuery Sortable library for dynamic menu management similar to WordPress. And it has multi level management.

    <ol id="my-nav">
        <li data-id="unique-id-here" data-label="">
            <span>My Label</span>
            <input type="text" class="label-change">
            <ol><!-- PLACEHOLDER FOR SUBMENU --></ol>
        </li>
    </ol>
    
    <textarea id="output"></textarea>
    

    We want to update the Menu Label using the inherited input field. So we did something like below:

    $('body').on('keyup change', '.label-change', function () {
        var this_menu_label_field = $(this);
        var this_field_val        = this_menu_label_field.val();
        var this_menu_nav         = this_menu_label_field.parents('li');
    
        // Update the text string inside the <li>
        this_menu_nav.find('span').html(this_field_val);
    
        // Update the data-label attribute
        this_menu_nav.attr('data-label', this_field_val).sortable('refresh');
    
        var serialized_data = menu_container.sortable('serialize').get();
        $('#output').val(JSON.stringify(serialized_data));
    });
    

    The code is updating the string inside the <span> inside the respective <li> and also changing the data-label. But unfortunately it's updating only the first keystroke into the #output textarea.

    For example: if we type "Whatever", it might take "W" or "Wha". And no further keystrokes are updated into the #output. But at always the update inside the <span> and data-label is working fine.

    We need this kind of feature critically. But how can we achieve this?

    DEMO Fiddle

    https://jsfiddle.net/mayeenulislam/Lsrgu0qy/38/

    • Do nothing, see the value in the <textarea>
    • Do nothing, just type in the textbox
    • Now see the value in the <textarea> again
    2

    There are 2 best solutions below

    0
    Cave Johnson On BEST ANSWER

    I fiddled around with this and was able to make it work. Check my forked fiddle here: jsfiddle.net/qf7n89oe.

    The only change I made was to replace

    this_menu_nav.attr('data-label', this_field_val).sortable('refresh');
    

    with

    this_menu_nav.data('label', this_field_val).sortable('refresh');
    

    It seems using attr() causes some caching issues so I replaced it with data() which doesn't seem to have this problem.

    0
    Kalimah On

    jQuery Sortable stores data in data object. So it does not process data-* attributes. You can use Kodos Johnson answer if you would like to use jQuery data object. The alternative is to have your own definition of serialization function. See this example:

    /**
     * ---------------------------------------
     * OUR CODE STARTS HERE
     * ---------------------------------------
     */
    
    jQuery(document).ready(function($) {
      var menu_container = $('#my-nav');
      var menu_data_field = $('#output');
    
      var sortable = menu_container.sortable({
        delay: 500,
        group: 'serialization',
        onDrop: function($item, container, _super) {
          var data = sortable.sortable('serialize').get();
    
          var jsonString = JSON.stringify(data);
    
          menu_data_field.val(jsonString);
          _super($item, container);
        },
        serialize: function($parent, $children, parentIsContainer) {
          let result = $parent.map(function(e) {
            let attr = {};
            $.each(this.attributes, function() {
              const name = this.name.replace("data-", "");
              attr[name] = this.value;
            });
            return attr;
          });
    
          if (parentIsContainer)
            return [$children]
          else if ($children[0]) {
            result[0].children = $children
          }
    
          delete result.subContainers
          delete result.sortable
    
          return result.get();
        },
      });
    
      $('body').on('keyup', '.label-change', function() {
        var this_menu_label_field = $(this);
        var this_field_val = this_menu_label_field.val();
        var this_menu_nav = this_menu_label_field.parent('li');
    
        // Update the text string inside the <li>
        this_menu_nav.find('> span').html(this_field_val);
    
        // Update the data-label attribute
        this_menu_nav.attr('data-label', this_field_val).sortable('refresh');
    
        var serialized_data = menu_container.sortable('serialize').get();
        console.log(serialized_data);
        $('#output').val(JSON.stringify(serialized_data));
      });
    });
    body.dragging,
    body.dragging * {
      cursor: move !important;
    }
    
    input[type="text"] {
      border-color: red;
    }
    
    #my-nav {
      padding-left: 0;
      list-style-type: none;
      overflow: hidden;
    }
    
    #my-nav ol {
      padding-left: 0;
      margin-left: 20px;
      list-style-type: none;
    }
    
    #my-nav li {
      padding: 5px 10px;
      background-color: #f8f8f8;
      color: #333;
      border: 1px solid #999;
      border-radius: 3px;
      margin-bottom: 10px;
      box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
      cursor: move;
    }
    
    #my-nav li.placeholder {
      position: relative;
      border-style: dashed;
      background-color: #ededed;
      min-height: 34px;
    }
    
    #my-nav li.placeholder:before {
      position: absolute;
    }
    
    #my-nav li.dragged {
      position: absolute;
      top: 0;
      opacity: .5;
      z-index: 2000;
      box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
    }
    
    #my-nav li.dragged i.icon-move {
      color: var(--primary);
    }
    
    #my-nav li.highlight {
      background: gray;
      color: lightgray;
    }
    
    #my-nav li:first-of-type {
      margin-top: 10px;
    }
    
    #my-nav i.icon-move {
      cursor: pointer;
      color: #999;
      padding: 5px;
    }
    
    .dd {
      position: relative;
      display: block;
      margin: 0;
      padding: 0;
      max-width: 600px;
      list-style: none;
      font-size: 13px;
      line-height: 20px;
    }
    
    .dd-list {
      display: block;
      position: relative;
      margin: 0;
      padding: 0;
      list-style: none;
    }
    
    .dd-list .dd-list {
      padding-left: 30px;
    }
    
    .dd-item,
    .dd-empty,
    .dd-placeholder {
      display: block;
      position: relative;
      margin: 0;
      padding: 0;
      min-height: 20px;
      font-size: 13px;
      line-height: 20px;
    }
    
    .dd-handle {
      display: block;
      height: 30px;
      margin: 5px 0;
      padding: 5px 10px;
      color: #333;
      text-decoration: none;
      font-weight: bold;
      border: 1px solid #ccc;
      background: #fafafa;
      border-radius: 3px;
      box-sizing: border-box;
    }
    
    .dd-handle:hover {
      color: #2ea8e5;
      background: #fff;
    }
    
    .dd-item>button {
      position: relative;
      cursor: pointer;
      float: left;
      width: 25px;
      height: 20px;
      margin: 5px 0;
      padding: 0;
      text-indent: 100%;
      white-space: nowrap;
      overflow: hidden;
      border: 0;
      background: transparent;
      font-size: 12px;
      line-height: 1;
      text-align: center;
      font-weight: bold;
    }
    
    .dd-item>button:before {
      display: block;
      position: absolute;
      width: 100%;
      text-align: center;
      text-indent: 0;
    }
    
    .dd-item>button.dd-expand:before {
      content: '+';
    }
    
    .dd-item>button.dd-collapse:before {
      content: '-';
    }
    
    .dd-expand {
      display: none;
    }
    
    .dd-collapsed .dd-list,
    .dd-collapsed .dd-collapse {
      display: none;
    }
    
    .dd-collapsed .dd-expand {
      display: block;
    }
    
    .dd-empty,
    .dd-placeholder {
      margin: 5px 0;
      padding: 0;
      min-height: 30px;
      background: #f2fbff;
      border: 1px dashed #b6bcbf;
      box-sizing: border-box;
      -moz-box-sizing: border-box;
    }
    
    .dd-empty {
      border: 1px dashed #bbb;
      min-height: 100px;
      background-color: #e5e5e5;
      background-size: 60px 60px;
      background-position: 0 0, 30px 30px;
    }
    
    .dd-dragel {
      position: absolute;
      pointer-events: none;
      z-index: 9999;
    }
    
    .dd-dragel>.dd-item .dd-handle {
      margin-top: 0;
    }
    
    .dd-dragel .dd-handle {
      box-shadow: 2px 4px 6px 0 rgba(0, 0, 0, 0.1);
    }
    
    .dd-nochildren .dd-placeholder {
      display: none;
    }
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-sortable/0.9.13/jquery-sortable-min.js"></script>
    
    <ol id="my-nav">
      <li data-id="unique-id-here" data-label="My Label">
        <span>My Label</span>
        <input type="text" class="label-change form-control" placeholder="Type here to see the impact">
        <ol>
          <li data-id="unique-id-here" data-label="My Sub Label">
            <span>My Sub Label</span>
            <input type="text" class="label-change form-control" placeholder="Type here to see the impact">
            <ol>
              <!-- PLACEHOLDER FOR SUBMENU -->
            </ol>
          </li>
        </ol>
      </li>
    </ol>
    
    <h4>OUTPUT HERE</h4>
    <textarea id="output" class="form-control" placeholder="">[[{"label":"My Label","id":"unique-id-here","children":[[{"label":"My Sub Label","id":"unique-id-here","children":[[]]}]]}]]</textarea>