mirror of
https://gitee.com/ant-design-blazor/ant-design-blazor.git
synced 2024-12-01 19:48:17 +08:00
feat(module: form): support nest property validation and add table form demo (#4102)
* fix(module: form): support table editor validation * fix error * fix DisplayName
This commit is contained in:
parent
3faebe2456
commit
a81be3de54
@ -10,6 +10,7 @@ namespace AntDesign.Core.Reflection
|
||||
{
|
||||
internal class PropertyReflector
|
||||
{
|
||||
public PropertyInfo PropertyInfo { get; set; }
|
||||
public RequiredAttribute RequiredAttribute { get; set; }
|
||||
|
||||
public ValidationAttribute[] ValidationAttributes { get; set; }
|
||||
@ -31,7 +32,7 @@ namespace AntDesign.Core.Reflection
|
||||
public PropertyReflector(MemberInfo propertyInfo, PropertyReflector parentReflector = null)
|
||||
{
|
||||
ParentReflector = parentReflector;
|
||||
|
||||
PropertyInfo = propertyInfo as PropertyInfo;
|
||||
ValidationAttributes = propertyInfo?.GetCustomAttributes<ValidationAttribute>(true).ToArray();
|
||||
if (parentReflector?.ValidationAttributes?.Length > 0)
|
||||
{
|
||||
|
@ -27,10 +27,8 @@
|
||||
<AntDesign.Col @attributes="GetWrapperColAttributes()" Class=@($"{_prefixCls}-control")>
|
||||
<div class=@($"{_prefixCls}-control-input")>
|
||||
<div class=@($"{_prefixCls}-control-input-content")>
|
||||
<CascadingValue Value="null" Name="Form" TValue="IForm" IsFixed="@true">
|
||||
<CascadingValue Value="this" Name="FormItem" TValue="IFormItem" IsFixed="@true">
|
||||
@ChildContent
|
||||
</CascadingValue>
|
||||
<CascadingValue Value="this" Name="FormItem" TValue="IFormItem" IsFixed="@true">
|
||||
@ChildContent
|
||||
</CascadingValue>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,10 +3,13 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using AntDesign.Core.Helpers.MemberPath;
|
||||
using AntDesign.Core.Reflection;
|
||||
using AntDesign.Form.Locale;
|
||||
using AntDesign.Forms;
|
||||
@ -184,7 +187,7 @@ namespace AntDesign
|
||||
|
||||
private AntLabelAlignType? FormLabelAlign => LabelAlign ?? Form?.LabelAlign;
|
||||
|
||||
private string DisplayName => Label ?? _propertyReflector?.DisplayName ?? _propertyReflector?.PropertyName;
|
||||
private string DisplayName => Label ?? _propertyReflector?.DisplayName;
|
||||
|
||||
private string _name;
|
||||
private Action _nameChanged;
|
||||
@ -200,7 +203,7 @@ namespace AntDesign
|
||||
private FormValidateStatus? _originalValidateStatus;
|
||||
private Action _vaildateStatusChanged;
|
||||
|
||||
private Action<string[]> _onValidated;
|
||||
private Action<string[]> _onValidated = _ => { };
|
||||
|
||||
private IEnumerable<FormValidationRule> _rules;
|
||||
|
||||
@ -241,6 +244,8 @@ namespace AntDesign
|
||||
}
|
||||
|
||||
SetInternalIsRequired();
|
||||
|
||||
SetEventHandlers();
|
||||
}
|
||||
|
||||
private void SetClass()
|
||||
@ -280,6 +285,26 @@ namespace AntDesign
|
||||
}
|
||||
}
|
||||
|
||||
private void SetEventHandlers()
|
||||
{
|
||||
if (Form?.ValidateOnChange == true)
|
||||
{
|
||||
_validationStateChangedHandler = (s, e) =>
|
||||
{
|
||||
UpdateValidateMessage();
|
||||
};
|
||||
CurrentEditContext.OnValidationStateChanged += _validationStateChangedHandler;
|
||||
}
|
||||
else
|
||||
{
|
||||
_validationRequestedHandler = (s, e) =>
|
||||
{
|
||||
UpdateValidateMessage();
|
||||
};
|
||||
CurrentEditContext.OnValidationRequested += _validationRequestedHandler;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Help))
|
||||
@ -386,7 +411,7 @@ namespace AntDesign
|
||||
|
||||
private void UpdateValidateMessage()
|
||||
{
|
||||
if (_control == null)
|
||||
if (_fieldIdentifier.Model == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -423,23 +448,6 @@ namespace AntDesign
|
||||
_fieldIdentifier = control.FieldIdentifier;
|
||||
_control = control;
|
||||
|
||||
if (Form?.ValidateOnChange == true)
|
||||
{
|
||||
_validationStateChangedHandler = (s, e) =>
|
||||
{
|
||||
UpdateValidateMessage();
|
||||
};
|
||||
CurrentEditContext.OnValidationStateChanged += _validationStateChangedHandler;
|
||||
}
|
||||
else
|
||||
{
|
||||
_validationRequestedHandler = (s, e) =>
|
||||
{
|
||||
UpdateValidateMessage();
|
||||
};
|
||||
CurrentEditContext.OnValidationRequested += _validationRequestedHandler;
|
||||
}
|
||||
|
||||
if (control.PopertyReflector is not null)
|
||||
{
|
||||
_propertyReflector = control.PopertyReflector;
|
||||
@ -461,8 +469,57 @@ namespace AntDesign
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void BuildPropertyWithName()
|
||||
{
|
||||
var type = Form.Model.GetType();
|
||||
var dataIndex = Name;
|
||||
if (typeof(IDictionary).IsAssignableFrom(type))
|
||||
{
|
||||
dataIndex = $"['{dataIndex}']";
|
||||
}
|
||||
|
||||
LambdaExpression exp = PathHelper.GetLambda<object>(dataIndex, type);
|
||||
|
||||
if (exp.Body is UnaryExpression unary)
|
||||
{
|
||||
if (unary.Operand is MemberExpression member)
|
||||
{
|
||||
var perpertyInfo = member.Member as PropertyInfo;
|
||||
_propertyReflector = new PropertyReflector(perpertyInfo);
|
||||
}
|
||||
}
|
||||
else if (exp.Body is MemberExpression member)
|
||||
{
|
||||
var perpertyInfo = member.Member as PropertyInfo;
|
||||
_propertyReflector = new PropertyReflector(perpertyInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
var getValueDelegate = PathHelper.GetDelegate<object>(dataIndex, type);
|
||||
_propertyReflector = new PropertyReflector
|
||||
{
|
||||
GetValueDelegate = getValueDelegate.Invoke,
|
||||
PropertyName = Name,
|
||||
DisplayName = Name,
|
||||
ValidationAttributes = []
|
||||
};
|
||||
}
|
||||
_fieldValueGetter = _propertyReflector?.GetValueDelegate;
|
||||
_valueUnderlyingType = THelper.GetUnderlyingType(_propertyReflector.PropertyInfo.PropertyType);
|
||||
_fieldIdentifier = new FieldIdentifier(Form.Model, Name);
|
||||
SetRules();
|
||||
}
|
||||
|
||||
ValidationResult[] IFormItem.ValidateFieldWithRules()
|
||||
{
|
||||
if (_propertyReflector is null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
BuildPropertyWithName();
|
||||
}
|
||||
}
|
||||
|
||||
if (_propertyReflector is null)
|
||||
{
|
||||
return [];
|
||||
@ -493,7 +550,7 @@ namespace AntDesign
|
||||
Rule = rule,
|
||||
Value = propertyValue,
|
||||
FieldName = _fieldIdentifier.FieldName,
|
||||
DisplayName = DisplayName,
|
||||
DisplayName = DisplayName ?? _propertyReflector.PropertyName,
|
||||
FieldType = _valueUnderlyingType,
|
||||
ValidateMessages = validateMessages,
|
||||
Model = Form.Model
|
||||
|
@ -43,9 +43,9 @@
|
||||
{
|
||||
public class Address
|
||||
{
|
||||
[Display(Name = "省份")]
|
||||
[Display(Name = "Pr.")]
|
||||
public string Province { get; set; }
|
||||
[Display(Name = "街道")]
|
||||
[Display(Name = "St.")]
|
||||
public string Street { get; set; }
|
||||
}
|
||||
|
||||
|
@ -1,26 +0,0 @@
|
||||
@using System.ComponentModel.DataAnnotations;
|
||||
|
||||
<Form Model="@model"
|
||||
LabelColSpan="8"
|
||||
WrapperColSpan="16">
|
||||
<FormItem>
|
||||
<LabelTemplate>
|
||||
<label class="ant-form-item-required" for="username">
|
||||
<Tooltip Title="Enter your username">Username</Tooltip>
|
||||
</label>
|
||||
</LabelTemplate>
|
||||
<ChildContent>
|
||||
<Input Id="username" @bind-Value="@context.Username" />
|
||||
</ChildContent>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
@code{
|
||||
public class Model
|
||||
{
|
||||
[Required]
|
||||
public string Username { get; set; }
|
||||
}
|
||||
|
||||
private Model model = new Model();
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
@using System.ComponentModel.DataAnnotations;
|
||||
|
||||
<Form Model="@model"
|
||||
ValidateMode="FormValidateMode.Complex"
|
||||
OnFinish="OnFinish"
|
||||
OnFinishFailed="OnFinishFailed"
|
||||
LabelColSpan="8"
|
||||
WrapperColSpan="16">
|
||||
<FormItem Label="Class Name">
|
||||
<Input @bind-Value="@context.ClassName" />
|
||||
</FormItem>
|
||||
<FormItem Label="Students" Name="@nameof(context.Students)" Rules="[new(){ Min=1 }]">
|
||||
<Button OnClick="addRow" Type="primary" Style="margin-bottom:16px" Size="small">
|
||||
Add a row
|
||||
</Button>
|
||||
<Table DataSource="context.Students" TItem="Student" Context="row" Size="TableSize.Small" HidePagination Bordered>
|
||||
<PropertyColumn Width="30%" Property="c=>c.Name">
|
||||
<FormItem Required>
|
||||
<Input @bind-Value="@row.Name" />
|
||||
</FormItem>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="c=>c.Address">
|
||||
<FormItem Required>
|
||||
<Input @bind-Value="@row.Address" />
|
||||
</FormItem>
|
||||
</PropertyColumn>
|
||||
</Table>
|
||||
</FormItem>
|
||||
<FormItem WrapperColOffset="8" WrapperColSpan="16">
|
||||
<Button Type="@ButtonType.Primary" HtmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
<style>
|
||||
.ant-form-item-has-error .ant-table {
|
||||
border: 1px solid red;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code
|
||||
{
|
||||
private Model model = new Model();
|
||||
|
||||
public class Model
|
||||
{
|
||||
[Required]
|
||||
public string ClassName { get; set; }
|
||||
|
||||
public List<Student> Students { get; set; } = [];
|
||||
}
|
||||
|
||||
public record Student()
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Age { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Address { get; set; }
|
||||
}
|
||||
|
||||
void addRow()
|
||||
{
|
||||
model.Students.Add(new());
|
||||
}
|
||||
|
||||
private void OnFinish(EditContext editContext)
|
||||
{
|
||||
Console.WriteLine($"Success:{JsonSerializer.Serialize(model)}");
|
||||
}
|
||||
|
||||
private void OnFinishFailed(EditContext editContext)
|
||||
{
|
||||
Console.WriteLine($"Failed:{JsonSerializer.Serialize(model)}");
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ title:
|
||||
- `Address`:有两个控件,在 `FormItem` 内使用两个 `<FormItem NoStyle />` 分别绑定对应控件(一个FormItem下只能出现一个使用了@bing-Value的控件),对FormItem使用NoStyle,则FormItem的Grid布局会被忽略,就算主动使用了LabelCol或WrapperCol也不会产生效果。
|
||||
|
||||
这个场景还展示了复杂类型的表单验证,Address 属性是一个包含了两个属性的类型,当级联表单项绑定了它的属性时,会继承这个属性的所有 Attributes,但由于表单未绑定这个属性,外部表单无法获取 Required 和 Label。
|
||||
|
||||
- `BirthDate`:有两个内联控件,错误信息展示各自控件下,使用两个 `<FormItem />` 分别绑定对应控件,并修改 `style` 使其内联布局。
|
||||
|
||||
更复杂的封装复用方式可以参考下面的 `自定义表单控件` 。
|
||||
@ -28,7 +29,8 @@ Three typical scenarios are shown here.
|
||||
- `Username`: there is a description text or other component behind the input box, within `FormItem` only the component that uses @bind-Value will be bound to the FormItem, other components can be added at will.
|
||||
- `Address`: there are two controls, use two `<FormItem NoStyle />` within the `FormItem` to bind the corresponding controls separately (only one control with @bind-Value can appear under a FormItem), use NoStyle for the FormItem, then The Grid layout of the FormItem will be ignored, even if LabelCol or WrapperCol is actively used.
|
||||
|
||||
This scenario also demonstrates the validation of a complex type of form, Address is a class structure containing two properties and by attaching ValidateComplexType the form can be validated against all its properties. Details can be found in the Blazor documentation: [Nested Models, Collection Types and Complex Types](https://docs.microsoft.com/zh-cn/aspnet/core/blazor/forms-validation?WT.mc_id=DT-MVP-5003987)
|
||||
The scenario also shows complex types of form validation. The Address property is a type that contains two properties. When a cascading form entry binds its property, it inherits all attributes of that property, but because the form does not bind this property, the external form cannot obtain Required and Label.
|
||||
|
||||
- `BirthDate`: there are two inline controls with error messages displayed under each control, using two `<FormItem />`s to bind the corresponding controls separately, and modifying the `style` to make the layout inline.
|
||||
|
||||
For a more complex way of wrapping and reusing controls see `Custom Form Controls` below.
|
@ -1,14 +0,0 @@
|
||||
---
|
||||
order: 6.2
|
||||
title:
|
||||
zh-CN: 自定义标签
|
||||
en-US: Customized Label
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
标签可以通过LabelTemplate来自定义。
|
||||
|
||||
## en-US
|
||||
|
||||
Label can be customized by using LabelTemplate.
|
14
site/AntDesign.Docs/Demos/Components/Form/demo/nest-table.md
Normal file
14
site/AntDesign.Docs/Demos/Components/Form/demo/nest-table.md
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
order: 6.2
|
||||
title:
|
||||
zh-CN: 表格编辑与验证
|
||||
en-US: Table Edit & Validate
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
支持使用表格作为表单组件,并验证单元格编辑。
|
||||
|
||||
## en-US
|
||||
|
||||
Support using table as form component and validate cell edit.
|
Loading…
Reference in New Issue
Block a user