Skip to content

Instantly share code, notes, and snippets.

@SteveSandersonMS
Created September 21, 2018 16:29
Show Gist options
  • Save SteveSandersonMS/6ac9458e3fc5b49c199777627fff3fc8 to your computer and use it in GitHub Desktop.
Save SteveSandersonMS/6ac9458e3fc5b49c199777627fff3fc8 to your computer and use it in GitHub Desktop.
Validation mockup A: explicit <ValidateXyz> components that take a Func<T>
@* Unfortunately this has to be named BlazorForm, not Form, because we can't differentiate based on casing *@
<form onsubmit=@HandleSubmit>
@ChildContent(_context)
</form>
@functions {
private FormContext _context = new FormContext();
[Parameter] protected RenderFragment<FormContext> ChildContent { get; set; }
[Parameter] protected Action<FormContext> OnSubmit { get; set; }
public class FormContext
{
private List<Func<bool>> _validations = new List<Func<bool>>();
public void AddValidation(Func<bool> validation)
{
_validations.Add(validation);
}
public bool Validate()
{
var isValid = true;
foreach (var validation in _validations)
{
isValid &= validation();
}
return isValid;
}
}
public class ValidationGroup
{
private List<Func<bool>> _lastResultAccessors = new List<Func<bool>>();
public bool Valid
{
get => _lastResultAccessors.All(x => x());
}
public void AddValidation(Func<bool> lastResultAccessor)
{
_lastResultAccessors.Add(lastResultAccessor);
}
}
void HandleSubmit()
{
if (_context.Validate())
{
OnSubmit?.Invoke(_context);
}
}
}
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
@* Would be good if we could put Content="form" here so all "context" below is replaced by "form" *@
<BlazorForm OnSubmit=@HandleSubmit>
@*
If we implemented the ability to share data with descendants as in #1 (like React's 'context') then we could do this,
i.e., could eliminate the Form and Value params:
<InputText bind=@email>
<ValidateRequired />
<ValidateEmail />
</InputText>
*@
<p>
@* Example of making use of validation group status *@
<input bind=@firstName style="background-color: @(firstNameValidations.Valid ? "" : "#ffcccc")" />
@* Specifying Value as a Func<T>, not T, is needed to be independent of render order *@
<ValidateRequired Form=context Value=@(() => firstName)>Type a first name</ValidateRequired>
</p>
<p>
@* Can work with arbitrary inputs, as we're validating a Func<T>, not a UI element *@
<input bind=@email />
<ValidateRequired Form=context Value=@(() => email) />
</p>
<button type="submit">Submit</button>
@** Can control where the validation messages get displayed, have custom rules, put them in groups *@
<ValidateCustom Group=@firstNameValidations Form=context Rule=@(() => !string.IsNullOrWhiteSpace(firstName))>First name is required</ValidateCustom>
<ValidateCustom Group=@firstNameValidations Form=context Rule=@(() => firstName != null && firstName.Length < 5)>First name is too long</ValidateCustom>
</BlazorForm>
<pre>@log</pre>
@functions {
BlazorForm.ValidationGroup firstNameValidations = new BlazorForm.ValidationGroup();
string log = "";
string firstName = "Bert";
string email = "[email protected]";
void HandleSubmit(BlazorForm.FormContext context)
{
log += "\nSubmitted for " + firstName;
StateHasChanged(); // Only needed for the log
}
}
@if (!isValid)
{
<span style="color:red">
@* Use child content as message if specified, otherwise use default *@
@if (ChildContent != null)
{
@ChildContent
}
else
{
<text>The valid is not valid</text>
}
</span>
}
@functions {
[Parameter] protected BlazorForm.FormContext Form { get; set; }
[Parameter] protected BlazorForm.ValidationGroup Group { get; set; }
[Parameter] protected Func<bool> Rule { get; set; }
[Parameter] protected RenderFragment ChildContent { get; set; }
private bool isValid = true;
protected override void OnInit()
{
Form.AddValidation(() =>
{
if (Rule() != isValid)
{
isValid = !isValid;
StateHasChanged();
}
return isValid;
});
Group?.AddValidation(() => isValid);
}
}
@* Example of specializing ValidateCustom with a pre-implemented rule *@
<ValidateCustom Form=@Form Group=@Group Rule=@Rule ChildContent=@(ChildContent ?? DefaultMessage) />
@functions {
[Parameter] protected BlazorForm.FormContext Form { get; set; }
[Parameter] protected BlazorForm.ValidationGroup Group { get; set; }
[Parameter] protected RenderFragment ChildContent { get; set; }
[Parameter] protected Func<string> Value { get; set; }
private void DefaultMessage(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
builder.AddContent(0, "A value is required");
}
private bool Rule()
=> !string.IsNullOrWhiteSpace(Value());
}
@SteveSandersonMS
Copy link
Author

SteveSandersonMS commented Sep 21, 2018

This is kind of a halfway point between being template centric and model centric. It has the benefit of letting you store the actual data being edited on any object(s) you want, such as the DTOs returned by your server giving the existing model data, and updates that data in place.

Other major approaches would include:

  • Fully UI (template) centric
    • In this model, the UI elements are the underlying source of truth
    • We'd have <InputText ref="firstName" />", etc., then the developer would access the value as firstName.Value.
    • You might also nest validation components inside the <InputText>. They wouldn't necessarily render any output; they'd just register themselves as validators on the <InputText>. Alternatively you might do them as inline attributes on the <InputText> like Validations=@(new[] { Validation.Required, Validation.MaxLength(3) }).
    • You might have <ValidationDisplay ForInput=@firstName /> to control where messages appear
    • The developer would manually copy all the initial state from their DTOs onto the UI elements, then on successful submit, copy all the values back from the UI into their DTOs, like WebForms
    • Pros: simple, very quick to understand. Every UI works independently.
  • Fully model centric
    • In this model, some data structure representing the form is the underlying source of truth
    • We'd have a procedural way to specify a form object, its fields, and their validators.
    • We might have a programmatic/templated way to "generate" the UI based on this data structure
    • Pro: it's possible to execute the validators independently of your UI (though this is often the wrong abstraction, and people waste a lot of time on it)
    • Con: usually a leaky abstraction

I've mocked up each of these at earlier stages (well, mostly the fully UI centric one), but don't have the end result in an easily consumable format. It's easy enough to recreate later.

@SteveSandersonMS
Copy link
Author

Also in the code samples above, we would eventually remove all the Form=context that is repeated everywhere. This could be implicit once we have the "share data with descendant components" feature from #1.

@Kaffeegangster
Copy link

Both approaches would be would be nice. We have some dynamic entities in the database. A extensible model centric approach would ease generating forms for such entities. I have wrapped https://github.com/mozilla-services/react-jsonschema-form in a blazor component for such use cases. Something similar or better in native blazor would be nice. I have tried to do a cascaded dropdown list with react-jsonschema-form. It worked by changing forms data and ui schema on the fly but things start getting ugly with more complex scenaries.

@Andrzej-W
Copy link

Do you have any plans for generic Blazor components (like generic classes in C#)? To implement universal ValidateRequired component we need
[Parameter] protected Func<T> Value { get; set; }
instead of
[Parameter] protected Func<string> Value { get; set; }
and smarter Rule function. Otherwise we will have to create ValidateStringRequired, ValidateIntRequired, etc.

Also we should take into account that average HTML form in LOB application is quite complex. There are a lot of "noise" if we want to create a nice responsive layout which is also compliant with accessibility rules. With this syntax:

<InputText bind=@email>
            <ValidateRequired />
            <ValidateEmail />
</InputText>

our forms will be even more complex and hard to maintain. Probably the biggest drawback of this solution is that we have to specify validation rules second time. In Blazor we have the same programming language on the server and on the client and we should be able to specify validation rules in DTO objects.

@hishamco
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment