0

I am trying to create an inline editor which will support editing on click and saving on enter/blur. The environment includes PHP v5.6, jQuery v3.1, Laravel v5.2 and MySQL.

The question is, while saving with enter works, the saving on blur does not.

Laravel DB:listen to saving on enter:

2016-08-10 12:01:45: select * from `user` where `user`.`id` = '15' limit 1
2016-08-10 12:01:45: update `user` set `name` = '22222', `updated_at` = '2016-08-10 12:01:45' where `id` = '15'

Laravel DB:listen to saving on blur, note that there is no "update" query at all:

2016-08-10 11:21:53: select * from `user` where `user`.`id` = '15' limit 1

It seems the blur does not detect that the <input> text has changed. How should I solve it?

inline-edit.js:

var edit = $(".inline-edit");

edit.click(function() {
    $('.ajax').html($('.ajax input').val());
    $('.ajax').removeClass('ajax');
    $(this).addClass('ajax');
    $OLDVAL = $.trim($(this).text());
    $(this).html('<input id="inline-editbox" type="text" value="' + $OLDVAL + '">');
    // focus and move cursor to the end of the editbox
    var inline_editbox = $('#inline-editbox');
    inline_editbox.focus();
    var editbox_value = inline_editbox.val();
    inline_editbox.val('');
    inline_editbox.val(editbox_value);
    // 
    inline_editbox.blur(function(event) {
        var formData = {
            tag: $(this).attr('id'),
            value: $('.ajax input').val(),
        };
        $.ajax({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content')
            },
            type: "PATCH",
            data: formData,
            dataType: 'json',
            success: function(data) {
                $('.ajax').html($('.ajax input').val());
                $('.ajax').removeClass('ajax');
            },
            error: function(xhr, status, error) {
                console.warn(xhr.responseText);
                alert(error);
            }
        });
    });
});

edit.keydown(function(event) {
    if (event.keyCode == 13) {
        var formData = {
            tag: $(this).attr('id'),
            value: $('.ajax input').val(),
        };
        $.ajax({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content')
            },
            type: "PATCH",
            data: formData,
            dataType: 'json',
            success: function(data) {
                $('.ajax').html($('.ajax input').val());
                $('.ajax').removeClass('ajax');
            },
            error: function(xhr, status, error) {
                console.warn(xhr.responseText);
                alert(error);
            }
        });
    }
});

Part of Laravel route.php:

Route::patch ('user/{id}',      ['as'=>'user.patch',  'uses'=>'UserController@update_ajax']);

Part of Laravel UserController.php:

function update_ajax($id, UserCreateFormRequest $request)
{
    $user = User::findOrFail($id);
    $tag  = $request->get('tag');
    if( $tag == 'name') {
        $user->update([
            'name'        =>    $request->get('value')
        ]);
    }

    if( $tag == 'email') {
        $user->update([
            'email'       =>    $request->get('value')
        ]);
    }

    if( $tag == 'isRegistered') {
        $user->update([
            'isRegistered'=>    $request->get('value')
        ]);
    }

    return response()->json([
        'status' => 'success',
        'msg' => 'Data created successfully',
    ]);
}
Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • You should start by comparing the HTTP requests that are generated for the "enter" and "blur" situations (using the network tab in your browser's developer tools). If the HTTP requests look correct in both cases, the the problem is on the server side. If the HTTP requests look different, then the client side is the problem. In any case, the first step you should make is to remove the duplication from your JS code. Your two Ajax calls are absolutely identical, replace them with a function. – Tomalak Aug 10 '16 at 16:31
  • Fixed, thank you. The reason is, formData.tag in blur is "inline-editbox", which is id, and is not the server want. By contrast, formData.tag in keyDown is inline-edit field id, and is ok. So, in blur, change `$(this).attr('id')` to `$(this).parent().attr('id')`, and it works. – zhang_career Aug 11 '16 at 04:20
  • I had an answer ready yesterday, but I wanted to wait and see if you can find out the mistake on your own. I've just posted that answer, I think you can learn a few things from it. – Tomalak Aug 11 '16 at 09:11

1 Answers1

0

Your main problem seems to be that you listen to the keydown event on the wrong element (on the .inline-editbox instead of on the <input>).

However, there also is a lot of duplication and unnecessary steps in your code, so I have rewritten it to be clearer.

The following:

  • has a separate function to do the Ajax call (which makes the Ajax calls and the rest of the code easier to understand and, more importantly, easier to test)
  • uses jQuery's XHR promises to enable separation of application logic and Ajax logic
  • makes use of jQuery custom events via .trigger() so that the data upload to the server can be centralized in one spot
  • restores the original value when the server update fails (you can implement other error behavior)
  • does not send an update the server when the value has not changed
  • handles HTML special characters (quotes, angle brackets) properly (your version has a bug in this regard)

Full code:

function patchUser(params) {
    return $.ajax({
        url: 'user/...',
        headers: {
            'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content')
        },
        type: "PATCH",
        data: JSON.stringify(params),
        contentType: "application/json"
    }).fail(function(xhr, status, error) {
        console.warn(xhr.responseText);
        alert(error);
    });
}

$(".inline-edit").click(function () {
    var $inlineEdit = $(this),
        curentValue = $.trim($inlineEdit.text());

    $inlineEdit.empty().addClass('ajax');
    $('<input>')
        .val(curentValue)
        .appendTo($inlineEdit)
        .on("blur", function () {
            $(this).trigger("contentchange");
        })
        .on("keydown", function (e) {
            if (e.keyCode == 13) $(this).trigger("contentchange");
        })
        .on("contentchange", function () {
            var newValue = $(this).val();
            if (newValue === curentValue) {
                $inlineEdit
                    .text(curentValue)
                    .removeClass('ajax');
            } else {
                patchUser({
                    tag: $inlineEdit.attr('id'),
                    value: newValue
                }).done(function (data) {
                    $inlineEdit.text(data);
                }).fail(function () {
                    $inlineEdit.text(curentValue);
                }).always(function () {
                    $inlineEdit.removeClass('ajax');
                });
            }
        })
        .focus();
});

Live sample

Click to run a demonstration. I replaced patchUser() with a mock-up here.

function patchUser(params) {
    var result = $.Deferred();
    result.resolve("Value saved: (" + params.value + ")");
    return result.promise();
}

$(".inline-edit").click(function () {
    var $inlineEdit = $(this),
        curentValue = $.trim($inlineEdit.text());

    $inlineEdit.empty().addClass('ajax');
    $('<input>')
        .val(curentValue)
        .appendTo($inlineEdit)
        .on("blur", function () {
            $(this).trigger("contentchange");
        })
        .on("keydown", function (e) {
            if (e.keyCode == 13) $(this).trigger("contentchange");
        })
        .on("contentchange", function () {
            var newValue = $(this).val();
            if (newValue === curentValue) {
                $inlineEdit
                    .text(curentValue)
                    .removeClass('ajax');
            } else {
                patchUser({
                    tag: $inlineEdit.attr('id'),
                    value: newValue
                }).done(function (data) {
                    $inlineEdit.text(data);
                }).fail(function () {
                    $inlineEdit.text(curentValue);
                }).always(function () {
                    $inlineEdit.removeClass('ajax');
                });
            }
        })
        .focus();
});
.inline-edit > input {
  width: 100%;
}
.ajax {
  border: 1px solid green;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="inline-edit" id="name">User Name</div>
<div class="inline-edit" id="email">User Email</div>
<div class="inline-edit" id="isRegistered">User IsRegistered</div>
Community
  • 1
  • 1
Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • Thank you for teaching me so much. Based on your code, I wrap `$(".inline-edit").click(function () {}` with `$(document).ready(function() {}`, so click-to-edit is enabled. Then, I meet another problem: after patchUser called, until refresh the web page, the text does not show the currect content but show `[object Object]`. I am trying to fix it. :) – zhang_career Aug 12 '16 at 16:49
  • Set a breakpoint on the line with `$inlineEdit.text(data);`, update a text input, inspect `data`. There is your object. – Tomalak Aug 12 '16 at 16:53
  • Fixed. It is mismatch between server response and `data` in `$inlineEdit.text(data);`. I change the server response from object to `newValue`. And what I see is what I have edited. May I ask a question, the server response of `newValue` is from database query (I think so. The laravel code is `return response($user->name);`), which is time-consuming and transmit-doubling. Is this nessary? – zhang_career Aug 12 '16 at 18:01
  • It depends. If you do additional data sanitation on the server (and you should - never blindly trust data from a client) then the server might save something different than what the client transmitted. Telling the client what you actually saved makes sure the user sees what the database contains. – Tomalak Aug 12 '16 at 18:31