Search code examples
c#data-bindingcomponentsblazor

Blazor - Component wrapping and data binding


I've made a range input in a blazor component. And now i'm trying to make a logarithme range input.

To do this, I wanted to use my first range input, and simply make a wrapper between a fake value in the range input and the real value.

but I think I havent well understood the component binding, the index page is not notified of modifications.

Here is the range input component:

<div id="rc-@ID">
    @(new MarkupString($@"<style>
                #rc-{ID} {{
                    position: relative;
                    width: 100%;
                }}

                #rc-{ID} > input[type='range'] {{
                    padding: 0;
                    margin: 0;
                    display: inline-block;
                    vertical-align: top;
                    width: 100%;
                    --range-color: hsl(211deg 100% 50%);
                    background: var(--track-background);
                }}
                #rc-{ID} > input[type='range']::-moz-range-track {{
                    border-color: transparent; /* needed to switch FF to 'styleable' control */
                }}
                #rc-{ID} > input[name='low-range'] {{
                    position: absolute;
                }}
                #rc-{ID} > input[name='low-range']::-webkit-slider-thumb {{
                    position: relative;
                    z-index: 2;
                }}
                #rc-{ID} > input[name='low-range']::-moz-range-thumb {{
                        transform: scale(1); /* FF doesn't apply position it seems */
                        z-index: 1;
                    }}
                #rc-{ID} > input[name='high-range'] {{
                    position: relative;
                    --track-background: linear-gradient(to right, transparent {(int)(100 * (LowerValue - MinBound) / (MaxBound - MinBound) + 1 + 0.5f)}%, var(--range-color) 0, var(--range-color) {(int)(100 * (HigherValue - MinBound) / (MaxBound - MinBound) - 1 + 0.5f)}%, transparent 0 ) no-repeat 0 50% / 100% 100%;
                    background: linear-gradient(to right, gray {(int)(100 * (LowerValue - MinBound) / (MaxBound - MinBound) + 1+ 0.5f)}%, transparent 0, transparent {(int)(100 * (HigherValue - MinBound) / (MaxBound - MinBound) - 1 + 0.5f)}%, gray 0 ) no-repeat 0 50% / 100% 30%
                }}

                #rc-{ID} > input[type='range']::-webkit-slider-runnable-track {{
                    background: var(--track-background);
                }}

                #rc-{ID} > input[type='range']::-moz-range-track {{
                    background: var(--track-background);
                }}
            </style>"))
    <input class="custom-range" name="low-range" type="range" min="@MinBound" max="@MaxBound" step="@Step" @bind="@LowerValue" @bind:event="oninput" />
    <input class="custom-range" name="high-range" type="range" min="@MinBound" max="@MaxBound" step="@Step" @bind="@HigherValue" @bind:event="oninput" />
</div>

@code
{
    [Parameter] public float MinBound { get; set; } = 0;
    [Parameter] public float MaxBound { get; set; } = 1;
    [Parameter] public float Step { get; set; } = 0.01f;
    [Parameter]
    public float? ValueLow
    {
        get
        {
            var res = Math.Min(_valueLow, _valueHigh);
            if (res == MinBound)
                return null;
            return res;
        }
        set
        {
            if (!value.HasValue)
            {
                if (_valueLow.Equals(MinBound))
                    return;
                _valueLow = MinBound;
            }
            else
            {
                if (_valueLow.Equals(value.Value))
                    return;
                _valueLow = value.Value;
            }

            if (_valueLow > _valueHigh)
            {
                _valueLow = _valueHigh;
                _valueHigh = value.Value;
                ValueHighChanged.InvokeAsync(_valueHigh);
            }

            if (_valueLow == MinBound)
                ValueLowChanged.InvokeAsync(null);
            else
                ValueLowChanged.InvokeAsync(_valueLow);
        }
    }
    [Parameter]
    public float? ValueHigh
    {
        get
        {
            var res = Math.Max(_valueLow, _valueHigh);
            if (res == MaxBound)
                return null;
            return res;
        }
        set
        {
            if (!value.HasValue)
            {
                if (_valueHigh.Equals(MaxBound))
                    return;
                _valueHigh = MaxBound;
            }
            else
            {
                if (_valueHigh.Equals(value.Value))
                    return;

                _valueHigh = value.Value;
            }
            if (_valueLow > _valueHigh)
            {
                _valueHigh = _valueLow;
                _valueLow = value.Value;
                ValueLowChanged.InvokeAsync(_valueLow);
            }

            if (_valueHigh == MaxBound)
                ValueHighChanged.InvokeAsync(null);
            else
                ValueHighChanged.InvokeAsync(_valueHigh);
        }
    }


    [Parameter] public EventCallback<float?> ValueLowChanged { get; set; }
    [Parameter] public EventCallback<float?> ValueHighChanged { get; set; }

    float _valueLow = 0;
    float _valueHigh = 1;
    private float LowerValue
    {
        get => Math.Min(_valueLow, _valueHigh);
        set => ValueLow = value;
    }
    private float HigherValue
    {
        get => Math.Max(_valueLow, _valueHigh);
        set => ValueHigh = value;
    }


    string ID = Guid.NewGuid().ToString().Replace("-", "").Substring(15);
}

And here is my Range input log component:

<RangeControl @bind-ValueLow="Low"
              @bind-ValueHigh="High"
              MaxBound="max"
              MinBound="min"
              Step="1" />
<div class="d-flex">
    <strong>Log values : </strong>
    <span>@Low</span>
    <span class="ml-2">@High</span>
</div>

@code
{
    private float min = 1.0f;
    private float max = 100.0f;

    [Parameter] public float MinBound { get; set; } = 10;
    [Parameter] public float MaxBound { get; set; } = 10000;
    [Parameter] public float Step { get; set; } = 1;


    private float r => MinBound == 0 ? MaxBound : (MaxBound / MinBound);

    private float? _valueLow;
    [Parameter]
    public float? ValueLow
    {
        get => _valueLow;
        set
        {
            if (value == _valueLow) return;
            _valueLow = value;
            ValueLowChanged.InvokeAsync(ValueLow);
        }
    }

    private float? _valueHigh;
    [Parameter]
    public float? ValueHigh
    {
        get => _valueHigh;
        set
        {
            if (value == _valueHigh) return;
            _valueHigh = value;
            ValueHighChanged.InvokeAsync(ValueHigh);
        }
    }



    private float? Low
    {
        get
        {
            if (ValueLow.HasValue)
                return (float)((min = max) * Math.Log(ValueLow.Value) / Math.Log(r));
            return null;
        }
        set
        {
            if (value.HasValue)
                ValueLow = (float)Math.Exp(value.Value * Math.Log(r) / (max - min));
            else
                ValueLow = null;
        }
    }
    private float? High
    {
        get
        {
            if (ValueHigh.HasValue)
                return (float)((min = max) * Math.Log(ValueHigh.Value) / Math.Log(r));
            return null;
        }
        set
        {
            if (value.HasValue)
                ValueHigh = (float)Math.Exp(value.Value * Math.Log(r) / (max - min));
            else
                ValueHigh = null;
        }
    }

    [Parameter] public EventCallback<float?> ValueLowChanged { get; set; }
    [Parameter] public EventCallback<float?> ValueHighChanged { get; set; }
}

And the index page :

@page "/"

<h1>Hello, world!</h1>

<RangeControl @bind-ValueHigh="ValueHigh" @bind-ValueLow="ValueLow" MinBound="10" MaxBound="10000" Step="1"></RangeControl>
<br />
<RangeControlLog @bind-ValueHigh="ValueHigh" @bind-ValueLow="ValueLow" MinBound="10" MaxBound="10000" Step="1"></RangeControlLog>

<div class="d-flex">
    <strong>Real values : </strong>
    <span>@ValueLow</span>
    <span class="ml-2">@ValueHigh</span>
</div>

@code {
    float? ValueHigh = null;
    float? ValueLow = null;
}

Solution

  • You cannot have nested @bind- i.e. have a wrapper that uses @bind- of the wrapped component and also expose a property to be used with @bind-.

    You need to pass Foo and FooChanged to the component being wrapped.

    This means that in your RangeControlLog, you need to pass to RangeControl ValueLow and ValueLowChanged instead of using @bind-ValueLow

    <RangeControl ValueLow="Low"
                  ValueHigh="High"
                  ValueLowChanged="ValueLowChanged"
                  ValueHighChanged="ValueHighChanged"
                  MaxBound="max"
                  MinBound="min"
                  Step="1" />
    

    To learn more, you can take a look at the docs about chained binding and also take a look at this question I have made to understand better about ValueChanged and how it works.

    But for short, when you use @bind-Foo="Bar" it transforms it into Foo="Bar", FooChanged="@(foo => Bar = foo;)" which are kind of a default value for updating the properties. But it doesn't work when you have multiple @bind-, so you need to pass that directly.

    For me, @bind- looks like a syntax sugar for binding properties and when you have the parameters Foo and FooChanged, you can use @bind-Foo.