21

I'm creating a blazor server side app and have problems to bind a value between two custom components.

I've looked through different example of how the bind or @bind is supposed to work but I cannot figure out what up to date information on this matter is.

Given a model class User:

public class User
    {
        [Mapping(ColumnName = "short_id")]
        public string ShortId { get; set; }

        public User()
        {

        }
    }

I want to build a form which displays all properties of this user and has them in an input so it can be edited and in the end saved to a database.

My Form (parent component) looks like this:

<div class="edit-user-form">
    <AnimatedUserInput TbText="@User.ShortId" Placeholder="MHTEE Id"/>
    <button class="btn btn-secondary" @onclick="Hide" style="{display:inline-block;}">Back</button>
    <button class="btn btn-primary" @onclick="SaveUser" style="{display:inline-block;}">Save</button>
</div>
@code {
    [Parameter] public vUser User { get; set; }

    [Parameter] public Action Hide { get; set; }

    bool _error = false;

    void SaveUser()
    {
        ValidateUserData();
    }

    void ValidateUserData()
    {
        _error = string.IsNullOrWhiteSpace(User.ShortId);
    }
}

Where AnimatedUserInput is a custom component that looks like this:

<div class="edit-area @FocusClass @WiggleClass">
    <input type="text" @bind="@TbText" />
    <span data-placeholder="@Placeholder"></span>
</div>
@code {
    [Parameter]
    public string TbText { get; set; }

    [Parameter]
    public string Placeholder { get; set; }
}

Now in the Input textbox I correctly see the ShortId of the User object in my parent component.

However if I change the text in the input and click on the Save button which triggers the ValidateUserData method and allows me to look at the current User object I see that no changes have been done in the actual User.ShortId property but only on the input.

Is there any way to bind it so that changes in the input will automatically be applied to the binded property?

I have several of these properies which need to be shown in the form which is why I dont want to hook a custom OnChanged Event for each of those properties.

Drew Fyre
  • 558
  • 1
  • 5
  • 9

5 Answers5

32

Ok so for anyone stumbling upon this. I tried a bit more and found a solution. So to my custom input component AnimatedUserInput I added a EventCallback which I call everytime the value on the input is updated:

@code {

    [Parameter]
    public string TbText
    {
        get => _tbText;
        set
        {
            if (_tbText == value) return;

            _tbText = value;
            TbTextChanged.InvokeAsync(value);
        }
    }

    [Parameter]
    public EventCallback<string> TbTextChanged { get; set; }

    [Parameter]
    public string Placeholder { get; set; }

    private string _tbText;
}

And the binding on my parent component looks like this:

<div class="edit-user-form">
    <AnimatedUserInput @bind-TbText="@User.ShortId" Placeholder="MHTEE Id"/>
    <AnimatedUserInput @bind-TbText="@User.FirstName" Placeholder="First name" />
    <AnimatedUserInput @bind-TbText="@User.LastName" Placeholder="Last name" />
    <AnimatedUserInput @bind-TbText="@User.UserName" Placeholder="Username" />
    <AnimatedUserInput @bind-TbText="@User.StaffType" Placeholder="Staff type" />
    <AnimatedUserInput @bind-TbText="@User.Token" Placeholder="Token" />
    <button class="btn btn-secondary" @onclick="Hide" style="{display:inline-block;}">Back</button>
    <button class="btn btn-primary" @onclick="SaveUser" style="{display:inline-block;}">Save</button>
</div>

@code {
    [Parameter] public vUser User { get; set; }
}

This way blazor can correctly bind the values together and updates them from both sides the way I would expect them to bind.

Drew Fyre
  • 558
  • 1
  • 5
  • 9
  • 2
    This was driving me mad, thank you for the tip! I was missing the local set on my string value to invoke the change. Was a difficult issue to search for as well. – chrisbyte Mar 13 '20 at 18:58
13

Some of the Data binding resources out there for Blazor fail to mention that when you do @bind-Value you actually get auto mapping to 3 parameters:

Bind Call

<CustomComponent TValue="DateTime?" @bind-Value="order.PurchaseDate"></CustomComponent>

Custom Component

@using System.Linq.Expressions;
@typeparam TValue
//...
@code {
[Parameter]
public virtual TValue Value { get; set; }
[Parameter]
public EventCallback<TValue> ValueChanged { get; set; }
[Parameter]
public Expression<Func<TValue>> ValueExpression { get; set; }
//...
}

This is important to know if you want a component that simplifies a user interface, as you might need that Value Expression for form validation...

<ValidationMessage For="ValueExpression" ></ValidationMessage>

In a similiar sense, I use this ValueExpression accesor method to also run my label, as of course you might want a model data annotation, just like you might want to know if the field has a max length, or is required...

@( DisplayName.For(ValueExpression) )

Display Name For Code:

public static class StringHelpers
{
    public static string CamelCaseToPhrase(this string self)
    {
        return Regex.Replace(self, "([A-Z])", " $1").Trim();
    }
}

public static class DisplayName
{
    public static string For(string input)
    {
        return input.CamelCaseToPhrase();
    }

    public static string For<T>(Expression<Func<T>> accessor)
    {
        var expression = (MemberExpression)accessor.Body;
        var value = expression.Member.GetCustomAttribute(typeof(DisplayAttribute)) as DisplayAttribute;
        return value?.Name ?? expression.Member.Name?.CamelCaseToPhrase() ?? "";
    }

}

I share this here because google lands me here when I search for this, and most of the other resources out there do a bad job of giving this full picture, so hopefully this helps others too. Especially if you want to access Data Annotations, that pattern was also included.

Greg
  • 2,410
  • 21
  • 26
3

Thank you for the examples you guys posted here. Using them, I did a custom input checkbox two-way binding component based on the Toggle Switch example from W3schools' How TO-Toggle Switch like this one:

enter image description here

It triggers the event when it changes its status.

The Component

<div class="@Class">
    <label class="switch">
        <input id="@Id" type="checkbox" @bind="IsOn" />
        <span class="slider round"></span>
    </label>
</div>

@code {
    [Parameter] public EventCallback<bool> OnToggle { get; set; }
    [Parameter] public string Id { get; set; }
    [Parameter] public string Class { get; set; }
    [Parameter] public bool SetAsChecked { get; set; }

    private bool _isSettingUp = true;
    private bool _isOn;

    protected override Task OnInitializedAsync()
    {
        IsOn = SetAsChecked;
        return base.OnInitializedAsync();
    }

    private bool IsOn
    {
        get => _isOn;
        set
        {
            _isOn = value;
            if (!_isSettingUp)
            {
                OnToggle.InvokeAsync(value);
            }
            _isSettingUp = false;
        }
    }
}

The _isSettingUp flag is the way I found not to trigger the EventCallback when the check is set on at first. Maybe someone finds a better way to deal with that.

The Parent

(here is where you use your component)

<ToggleSwitch Id="mySwitch" OnToggle="((isChecked)=>DoSomething(isChecked))" SetAsChecked="isChecked" />

@code {
    private bool isChecked;

    protected override void OnInitialized()
    {
        isChecked = true;
    }

    protected void DoSomething(bool isChecked)
    {
        Console.WriteLine($">>>> {isChecked} <<<<");
    }
}

The CSS

The CSS is adapted from the W3Scholl example here. It should be placed in a .razor.css file named as the component (in the same folder) if you want to take advantage of Blazor CSS Isolation. Then you can use the Class parameter of the component to adapt it to different needs.

.switch {
    position: relative;
    display: inline-block;
    width: 60px;
    margin-top: 7px;
}

    .switch input {
        opacity: 0;
        width: 0;
        height: 0;
    }

.slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    padding-top: 2px;
    background-color: #737272;
    -webkit-transition: .7s;
    transition: .7s;
    width: 62px;
    height: 29px;
}

    .slider:before {
        position: absolute;
        content: "\2716";
        text-align: center;
        color: dimgrey;
        height: 25px;
        width: 25px;
        left: 2px;
        bottom: 2px;
        padding-top: 1px;
        background-color: white;
        -webkit-transition: .8s;
        transition: .8s;
    }

input:checked + .slider {
    background-color: limegreen;
}

    input:checked + .slider:before {
        -webkit-transform: translateX(33px);
        -ms-transform: translateX(33px);
        transform: translateX(33px);
        content: "\2714";
        text-align: center;
        color: dodgerblue;
    }

.slider.round {
    border-radius: 20px;
}

    .slider.round:before {
        border-radius: 50%;
    }

By the way, you can put any HTML Symbol in the switch. I did pick these from this cool page that has a search and the CSS code, which is what we need if we add the symbol in the CSS content, as here.

Hope this is of use to someone.

marcaldo
  • 59
  • 4
1

This is how i achieved two way binding

Parent Component

<LabelledField Class=""
   Label="Label value"
   DateValue="@Model.Value"
   OnDateChange="@((e) => Model.Value = e )"
   Type="InputType.date" />

Child Component

<div class="@Class">
<div class="ft-07 mb-03 field-label @LabelClass">@Label</div>
@switch (Type)
{
    case InputType.constant:
        <div class="label-field-value @ValueClass">@Value</div>
        break;
    case InputType.text:
        <input class="form-control input @ValueClass"
               type="text"
               @bind="Value" />

        break;
    case InputType.date:
        <input class="form-control input @ValueClass"
               type="date"
               @bind="DateValue" />

        break;
    default:
        break;
}
@code{
private string _tbText;
private DateTime? _tbDate;

[Parameter]
public string Class { get; set; }

[Parameter]
public string Label { get; set; }

[Parameter]
public string LabelClass { get; set; }

[Parameter]
public string Value
{
    get => _tbText;
    set
    {
        if (_tbText == value) return;

        _tbText = value;
        OnTextChange.InvokeAsync(value);
    }
}

[Parameter]
public DateTime? DateValue
{
    get => _tbDate;
    set
    {
        if (_tbDate == value) return;

        _tbDate = value;
        OnDateChange.InvokeAsync(_tbDate);
    }
}

[Parameter]
public string ValueClass { get; set; }

[Parameter]
public InputType Type { get; set; } = InputType.constant;

[Parameter]
public EventCallback<string> OnTextChange { get; set; }

[Parameter]
public EventCallback<DateTime?> OnDateChange { get; set; }
}
prvn
  • 406
  • 3
  • 7
  • 24
0

Might be useful to someone. Uses InputBase Class

Example Component EditText.razor

@inherits InputText

<div class="@cssClass">    
@if (viewEdit)
{
    <div @onclick="EditInput">
        @Value
    </div> 
    <label class="hover">

    </label>
    <label class="@view">
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle-fill" viewBox="0 0 16 16">
            <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
        </svg>
    </label>
}
else
{
    <input @ref="textInput" @bind:event="oninput" 
@bind="CurrentValueAsString" @onkeydown="ClickKey" @onfocusout="FocusOut">
}
</div>
@code {

[Parameter]
public string cssClass { get; set; }

public bool viewEdit { get; set; } = true;
ElementReference textInput;
// If you want to run JS
 //Guid guid { get; set; }
//protected override void OnInitialized()
//{
//    guid = Guid.NewGuid();
//}
//protected override async Task OnAfterRenderAsync(bool firstRender)
//{
//    if (firstRender)
//    {
//        await JSRuntime.InvokeVoidAsync("customMask",guid.ToString(), AdditionalAttributes["data-mask"].ToString(), AdditionalAttributes["data-placeholder"].ToString());
//    }
//}
async Task EditInput()
{
    viewEdit = !viewEdit;
    if (!viewEdit)
    {
        await Task.Run(async () => await textInput.FocusAsync());            
    }
}

async Task FocusOut()
{
    viewEdit = true;
    viewIcon();
}

async Task ClickKey(KeyboardEventArgs e)
{
    if (e.Code == "Enter")
    {
        viewEdit = true;
        viewIcon();
    }        
}

bool viewClass = false;

public string view => viewClass ? "active" : "hidden";

async Task viewIcon()
{
    viewClass = true;        
    await Task.Delay(2000);        
    viewClass = false;
    StateHasChanged();
}

protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage)
{
    result = value;
    validationErrorMessage = null;
    return true;
}

}

Usage

// With JS
<EditText data-mask="Date" data-placeholder="_" @bind-Value="myhtml" cssClass="form" />

<EditText cssClass="form" @bind-Value="@myhtml"></EditText>
blakcat
  • 634
  • 1
  • 7
  • 16