Magento – Validation Error with jQuery-UI Autocomplete and KnockoutJs – Magento 2

autocompletecheckoutjqueryknockoutjsmagento2

The Problem:

I've implemented jQuery-ui Autocomplete with Knockout Js in Checkout Page but some input fields that are binded to my Component throw a validation error ("Required Field") when i update the input's value programmatically.

error

The Context:

I've asked already a question for the way of implementing the Autocomplete functionality but taking into account that the new problems are more specific i've posted this question. This is the link for the other one: Jquery-Ui Autocomplete with KnockoutJs Magento 2

The result 'till now is this:
styles

What I've Done:

In Vendor/Module/Plugin/Checkout/Block/Checkout/LayoutProcessorPlugin.php

public function afterProcess(
    \Magento\Checkout\Block\Checkout\LayoutProcessor $subject,
    array  $jsLayout
) {

    $departmentField = [
        'component' => 'Vendor_Module/js/shippingAutocomplete',
        'config' => [
            // customScope is used to group elements within a single form (e.g. they can be validated separately)
            'customScope' => 'shippingAddress',
            'template' => 'ui/form/field',
            'elementTmpl' => 'Vendor_Module/autocomplete/shippingAddress/regionInput',
            'id' => 'region',
            'tooltip' => [
                'description' => 'Select a Department',
            ],
        ],
        'dataScope' => 'shippingAddress.region',
        'label' => 'Department',
        'provider' => 'checkoutProvider',
        'sortOrder' => 51,
        'validation' => [
            'required-entry' => true
        ],
        'options' => [],
        'visible' => true,
    ];

    $jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']
    ['shippingAddress']['children']['shipping-address-fieldset']['children']['region'] = $departmentField;

    $departmentIdField = [
        'component' => 'Vendor_Module/js/shippingAutocomplete',
        'config' => [
            // customScope is used to group elements within a single form (e.g. they can be validated separately)
            'customScope' => 'shippingAddress',
            'template' => 'ui/form/field',
            'elementTmpl' => 'Vendor_Module/autocomplete/regionIdInput',
            'id' => 'region_id',
        ],
        'dataScope' => 'shippingAddress.region_id',
        'provider' => 'checkoutProvider',
        'sortOrder' => 50,
        'validation' => [
            'required-entry' => true
        ],
        'options' => [],
        'visible' => true,
    ];

    $jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']
    ['shippingAddress']['children']['shipping-address-fieldset']['children']['region_id'] = $departmentIdField;

    $cityField = [
        'component' => 'Vendor_Module/js/shippingAutocomplete',
        'config' => [
            // customScope is used to group elements within a single form (e.g. they can be validated separately)
            'customScope' => 'shippingAddress',
            'template' => 'ui/form/field',
            'elementTmpl' => 'Vendor_Module/autocomplete/shippingAddress/cityInput',
            'id' => 'city',
            'tooltip' => [
                'description' => 'Select a City.',
            ],
        ],
        'dataScope' => 'shippingAddress.city',
        'label' => 'City',
        'provider' => 'checkoutProvider',
        'sortOrder' => 53,
        'validation' => [
            'required-entry' => true
        ],
        'options' => [],
        'visible' => true,
    ];

    $jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']
    ['shippingAddress']['children']['shipping-address-fieldset']['children']['city'] = $cityField;

    $cityIdField = [
        'component' => 'Vendor_Module/js/shippingAutocomplete',
        'config' => [
            // customScope is used to group elements within a single form (e.g. they can be validated separately)
            'customScope' => 'shippingAddress',
            'template' => 'ui/form/field',
            'elementTmpl' => 'Vendor_Module/autocomplete/cityIdInput',
            'id' => 'city_id',
        ],
        'dataScope' => 'shippingAddress.city_id',
        'provider' => 'checkoutProvider',
        'sortOrder' => 52,
        'validation' => [
            'required-entry' => true
        ],
        'options' => [],
        'visible' => true,
    ];

    $jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']
    ['shippingAddress']['children']['shipping-address-fieldset']['children']['city_id'] = $cityIdField;

    $postcodeField = [
        'component' => 'Vendor_Module/js/shippingAutocomplete',
        'config' => [
            // customScope is used to group elements within a single form (e.g. they can be validated separately)
            'customScope' => 'shippingAddress',
            'template' => 'ui/form/field',
            'elementTmpl' => 'Vendor_Module/autocomplete/postcodeInput',
            'id' => 'postcode',
        ],
        'dataScope' => 'shippingAddress.postcode',
        'label' => 'Postal Code',
        'provider' => 'checkoutProvider',
        'sortOrder' => 54,
        'validation' => [
            'required-entry' => true
        ],
        'options' => [],
        'visible' => true,
    ];

    return $jsLayout;
}

My Vendor/Module/view/frontend/web/js/shippingAutocomplete.js looks like this:

define([
'Magento_Ui/js/form/element/abstract',
'mage/url',
'ko',
'jquery',
'jquery/ui'
], function (Abstract, url, ko, $) {
'use strict';

ko.bindingHandlers.shippingAutoComplete = {

    init: function (element, valueAccessor) {

        console.log("Init - Custom Binding Shipping");
        console.log(element);
        console.log(valueAccessor);

        // valueAccessor = { selected: mySelectedOptionObservable, options: myArrayOfLabelValuePairs }
        var settings = valueAccessor();

        var selectedOption = settings.selected;
        var options = settings.options;
        var updateElementValueWithLabel = function (event, ui) {
            // Stop the default behavior
            event.preventDefault();

            // Update the value of the html element with the label
            // of the activated option in the list (ui.item)
            $(element).val(ui.item.label);

            if (ui.item.type == 'city') {
                var cityId = $('[name="city_id"]');
                cityId.val(ui.item.value);
                var postcode = $('[name="region_id"]').val()+cityId.val();
                $('[name="postcode"]').val(postcode);
            } else if (ui.item.type == 'department') {
                $('[name="region_id"]').val(ui.item.value);
            }

            // Update our SelectedOption observable
            if(typeof ui.item !== "undefined") {
                // ui.item - id|label|...
                selectedOption(ui.item);
                //selectedValue(ui.item.value);
            }
        };

        $(element).autocomplete({
            source: options,
            select: function (event, ui) {
                updateElementValueWithLabel(event, ui);
            }
        });

    }
};

return Abstract.extend({

    selectedDepartment: ko.observable(''),
    selectedCity: ko.observable(''),
    postCode: ko.observable(''),
    getDepartments: function( request, response ) {
        $.ajax({
            url: url.build('list/check/departments/'),
            data: JSON.stringify({
                q: request.term
            }),
            contentType: "application/json",
            type: "POST",
            dataType: 'json',
            error : function () {
                alert("An error have occurred.");
            },
            success : function (data) {
                //Data is a string of the form: '[{"label": label, "value": value}]'
                var items = JSON.parse(data);
                response( items );
            }
        });
    },
    getCities: function( request, response ) {
        var departmentValue = $('[name="region"]').val();
        $.ajax({
            url: url.build('list/check/cities/'),
            data: JSON.stringify({
                q: request.term,
                filter: departmentValue
            }),
            contentType: "application/json",
            type: "POST",
            dataType: 'json',
            error : function () {
                alert("An error have occurred.");
            },
            success : function (data) {
                //Data is a string of the form: '[{"label": label, "value": value}]'
                var items = JSON.parse(data);
                response( items );
            }
        });
    }
});
});

The inputs are as follows:

In Vendor/Module/view/frontend/web/template/autocomplete/shippingAddress/regionInput.html

<input class="admin__control-text" type="text"
   data-bind="
    shippingAutoComplete: {
        selected: selectedDepartment,
        options: getDepartments
    },
    event: {change: userChanges},
    value: value,
    hasFocus: focused,
    valueUpdate: valueUpdate,
    attr: {
        name: inputName,
        placeholder: placeholder,
        'aria-describedby': noticeId,
        id: uid,
        disabled: disabled
}"/>

In Vendor/Module/view/frontend/web/template/autocomplete/shippingAddress/cityInput.html

<input class="admin__control-text" type="text"
   data-bind="
    shippingAutoComplete: {
        selected: selectedCity,
        options: getCities
    },
    event: {change: userChanges},
    value: value,
    hasFocus: focused,
    valueUpdate: valueUpdate,
    attr: {
        name: inputName,
        placeholder: placeholder,
        'aria-describedby': noticeId,
        id: uid,
        disabled: disabled
}"/>

In Vendor/Module/view/frontend/web/template/autocomplete/regionIdInput.html

<input class="admin__control-text"
   data-bind="
    value: value,
    hasFocus: focused,
    attr: {
        name: inputName,
        placeholder: placeholder,
        'aria-describedby': noticeId,
        id: uid
}"/>

In Vendor/Module/view/frontend/web/template/autocomplete/cityIdInput.html

<input class="admin__control-text"
   data-bind="
    value: value,
    hasFocus: focused,
    attr: {
        name: inputName,
        placeholder: placeholder,
        'aria-describedby': noticeId,
        id: uid
}"/>

In Vendor/Module/view/frontend/web/template/autocomplete/postcodeInput.html

<input class="admin__control-text" type="text"
   data-bind="
    event: {change: userChanges},
    value: value,
    hasFocus: focused,
    valueUpdate: valueUpdate,
    attr: {
        name: inputName,
        placeholder: placeholder,
        'aria-describedby': noticeId,
        id: uid,
        disabled: disabled
}"/>

Best Answer

Change directy the value of the knockout observable object from your custom input field.

Pass the observale in your data-bind attribute:

 shippingAutoComplete: {
        selected: selectedCity,
        options: getCities
        value: value // this is your obersable you wanna change
    },

Access it via

  var settings = valueAccessor();
  var inputValue= settings.value;

Change value

inputValue("yourValue");