Problem with Controls and Knockout binding handlers

Hello,
I’m trying to build an OTP Input component and I’m having some troubles calling the Command binded to the control.

My control code

public Command OnChange
{
    get { return (Command)GetValue(OnChangeProperty); }
    set { SetValue(OnChangeProperty, value); }
}
public static readonly DotvvmProperty OnChangeProperty
    = DotvvmProperty.Register<Command, OTPInput>(c => c.OnChange, null);

protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context) {
   ...
   var onChangeProperty = GetCommandBinding(OnChangeProperty);
   if(onChangeProperty != null)
   {
       string propertyName = nameof(OnChange);
       string value = KnockoutHelper.GenerateClientPostBackScript(propertyName, onChangeProperty, this, true, null);
       writer.AddAttribute("data-password-otp-changed", value);
   }
   ...
}

Markup control code:

@viewModel System.Object, mscorlib
@baseType GestRestVVM.Controls.OTP.OTPInput, GestRestVVM

<form autocomplete="off" name="unlock_data_container">
    <div class="input-group justify-content-end flex-nowrap">
        <input type="password" class="form-control form-control--otp js-otp-input" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" required>
        <input type="password" class="form-control form-control--otp js-otp-input" inputmode="numeric" pattern="[0-9]*" required>
        <input type="password" class="form-control form-control--otp js-otp-input" inputmode="numeric" pattern="[0-9]*" required>
        <input type="password" class="form-control form-control--otp js-otp-input" inputmode="numeric" pattern="[0-9]*" required>
    </div>
</form>

Knockout javascript

ko.bindingHandlers["pin-input-plugin"] = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        const $el = $(element);
        const callback = $el.attr('data-password-otp-changed');
        const $inputs = $el.find('input');

        otpHandler = otp($inputs);
        otpHandler.init(function (code) {
            var property = valueAccessor();

            if (ko.isObservable(property)) {
                if (code !== property()) {
                    property(code);


                    if (callback != undefined) {
                        new Function(callback).call($el[0]);
                    }
                }
            }
        });

        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            otpHandler.destroy();
        });

    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext)
    {
        const propertyValue = ko.unwrap(valueAccessor());
        const $inputs = $(element).find('input');

        if (propertyValue && propertyValue.length === $inputs.length) {
            otpHandler.setValue(propertyValue);
        }
    }
}

When callback is called, I get the following console error:
“postback Uncaught error returned from promise! TypeError: Cannot read properties of undefined (reading ‘$data’)”

When I had DataContext in my control markup code, the function was called but the inputs were transformed into normal inputs like if the otpHandler was destroyed (maybe because of the postback after calling property(code)). So I tried moving the DataContext to the control tag:

<input:OTP DataContext="{value: _root.OTPInput}" Pin="{value: _root.OTPInput}" OnChange="{command: _root.OnChange()}" />

After that I had no more issues with the otpHandler stoping working but the Command function stopped being called. How should I configure it correctly?
I tried to follow Bootstrap Datepicker from contrib repo.

Best Regards,

Hello,
Sorry, I could not figure out why your component doesn’t work. However, I think you can significantly simplify the knockout-related code, which might at least make it easier to debug it:

  • Instead of KnockoutHelper.GenerateClientPostBackScript, use the GenerateClientPostBackLambda method and add the generated lambda function to the knockout binding directly. You’ll then be able to simply call the function, without new Function, accessing attributes, etc
  • Since you need to pass multiple values to the knockout handler, you can use KnockoutBindingGroup to generate syntax like { value: ..., otpChanged: ... }:
    var gr = new KnockoutBindingGroup {
       { "value", ValueBinding.GetKnockoutBindingExpression() },
       { "otpChanged", KnockoutHelper.GenerateClientPostBackLambda(...) }
    }
    writer.AddKnockoutDataBind("pin-input-plugin", gr);
    
  • You can then invoke the command by calling valueAccessor().otpChanged(). Element and knockout context are handled automatically.
1 Like

I’ll try using KockoutBindingGroup then, thank you!