KnockoutJS: RangeError: Maximum call stack size exceeded;-Collection of common programming errors

When registering a product, the user can customize the URL of it! As the user goes by typing the Tipo de produto, Nome or Link, the website will show you how will the URL for this product

Full url: http://i.stack.imgur.com/jZg7G.png

Note that the field “Tipo de produto” also modifies the URL!!

For this, I created a helper in KnockoutJS

Code

KnockoutJS

ko.bindingHandlers.url =
    update: (element, valueAccessor, allBindingsAccessor, viewModel) ->
        link = ko.utils.unwrapObservable(valueAccessor())
        if link
            link = link.toLowerCase().trim().replaceAll(" ", "-")
            link = encodeURI(link)
        else
            link = ""
        valueAccessor()(link)
        $(element).nextAll(".link-exibicao").text(link).effect("highlight", { color: "#FDBB30" }, 800 )

The only purpose of this helper is to generate a valid URL and display it in the span .link-exibicao

ViewModel

public class ProdutoViewModel
{
    [AdditionalMetadata("data-bind", "event: { change: function(data) { Link(data.Nome());  }}")]
    public string Nome { get; set; }

    [DataType(DataType.Url)]
    [AdditionalMetadata("Prefixo", "Produto/")]
    public string Link { get; set; }

    [Display(Name = "Descrição")]
    [DataType(DataType.MultilineText)]
    public string Descricao { get; set; }

    public int? Ordem { get; set; }
}

AdditionalMetadata will add an attribute with that name and value. For example, the property Name will generate the HTML:


Url.cshtml

The next step would be to add the markup data-bind="url: Link" in all fields of type URL:

@model string
@{

    var values = ViewData.ModelMetadata.AdditionalValues;
    object objDatabind;
    string data_bind = "";
    if (values.TryGetValue("data-bind", out objDatabind))
    {
        data_bind = objDatabind.ToString();
    }

    var nomeCampo = Html.IdForModel();

    var objPrefixo = values["Prefixo"];
    string prefixo = objPrefixo.ToString();
    string separador = "/";
    if (!string.IsNullOrWhiteSpace(prefixo))
    {
        if (prefixo.EndsWith("/") || prefixo.EndsWith("#"))
        {
            separador = prefixo[prefixo.Length - 1].ToString();
            prefixo = prefixo.Substring(0, prefixo.Length - 1);
        }   
    }
}

@Html.TextBoxFor(p => Model, new { data_bind = "value: " + nomeCampo + ", url: " + nomeCampo + (string.IsNullOrWhiteSpace(data_bind) ? "" : ", " + data_bind) })
@Request.Url.Host/@prefixo@separador

ProdutoViewModel.cshtml

Finally, and most simple step would be to build the form =):


    Tipo de produto


    



    @Html.LabelFor(p => p.Nome)


    @Html.EditorFor(p => p.Nome)
    @Html.ValidationMessageFor(p => p.Nome)



    @Html.LabelFor(p => p.Link)


    @Html.EditorFor(p => p.Link)
    @Html.ValidationMessageFor(p => p.Link)



    @Html.LabelFor(p => p.Descricao)


    @Html.EditorFor(p => p.Descricao)
    @Html.ValidationMessageFor(p => p.Descricao)



    @Html.LabelFor(p => p.Ordem)


    @Html.EditorFor(p => p.Ordem)
    @Html.ValidationMessageFor(p => p.Ordem)

Problem

Whenever typed simple words like: “my product name” everything works perfectly!
But words like meu prodúto côm açênto the error below is displayed!

Uncaught Error: Unable to parse bindings.
Message: RangeError: Maximum call stack size exceeded;
Bindings value: value: Link, url: Link
  1. Your bindingHandler is causing recursive updates, as you are accessing the value:

    link = ko.utils.unwrapObservable(valueAccessor())

    and later setting it:

    valueAccessor()(link)

    If link ends up being identical to its current value, then the chain would stop (observables don’t notify on identical (===) values).

    When you pass: meu prodúto côm açênto

    It becomes: meu-prod%C3%BAto%20c%C3%B4m%20a%C3%A7%C3%AAnto

    When setting the observable it re-triggers the same binding. So, it calls encodeURI again and now it is double-encoded like:

    meu-prod%25C3%25BAto%2520c%25C3%25B4m%2520a%25C3%25A7%25C3%25AAnto

    The observable is set again and since this value is new it triggers it again (and again and again) until you get the call stack error.

    Some options to handle this would be to not write back to the observable and just use the binding to encode the URL.

    Otherwise a good choice would be to use a writeable computed observable to intercept writes to the value and manipulate it in the model.