-
-
Save SteveSandersonMS/6ac9458e3fc5b49c199777627fff3fc8 to your computer and use it in GitHub Desktop.
@* 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()); | |
} |
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.
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.
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.
I did sort of component-based validation in my blog post http://www.hishambinateya.com/part1-validation-controls-using-blazor-basic-validation-controls
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:
<InputText ref="firstName" />"
, etc., then the developer would access the value asfirstName.Value
.<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>
likeValidations=@(new[] { Validation.Required, Validation.MaxLength(3) })
.<ValidationDisplay ForInput=@firstName />
to control where messages appearI'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.