mirror of
https://gitee.com/ant-design-blazor/ant-design-blazor.git
synced 2024-12-02 03:57:38 +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
|
internal class PropertyReflector
|
||||||
{
|
{
|
||||||
|
public PropertyInfo PropertyInfo { get; set; }
|
||||||
public RequiredAttribute RequiredAttribute { get; set; }
|
public RequiredAttribute RequiredAttribute { get; set; }
|
||||||
|
|
||||||
public ValidationAttribute[] ValidationAttributes { get; set; }
|
public ValidationAttribute[] ValidationAttributes { get; set; }
|
||||||
@ -31,7 +32,7 @@ namespace AntDesign.Core.Reflection
|
|||||||
public PropertyReflector(MemberInfo propertyInfo, PropertyReflector parentReflector = null)
|
public PropertyReflector(MemberInfo propertyInfo, PropertyReflector parentReflector = null)
|
||||||
{
|
{
|
||||||
ParentReflector = parentReflector;
|
ParentReflector = parentReflector;
|
||||||
|
PropertyInfo = propertyInfo as PropertyInfo;
|
||||||
ValidationAttributes = propertyInfo?.GetCustomAttributes<ValidationAttribute>(true).ToArray();
|
ValidationAttributes = propertyInfo?.GetCustomAttributes<ValidationAttribute>(true).ToArray();
|
||||||
if (parentReflector?.ValidationAttributes?.Length > 0)
|
if (parentReflector?.ValidationAttributes?.Length > 0)
|
||||||
{
|
{
|
||||||
|
@ -27,10 +27,8 @@
|
|||||||
<AntDesign.Col @attributes="GetWrapperColAttributes()" Class=@($"{_prefixCls}-control")>
|
<AntDesign.Col @attributes="GetWrapperColAttributes()" Class=@($"{_prefixCls}-control")>
|
||||||
<div class=@($"{_prefixCls}-control-input")>
|
<div class=@($"{_prefixCls}-control-input")>
|
||||||
<div class=@($"{_prefixCls}-control-input-content")>
|
<div class=@($"{_prefixCls}-control-input-content")>
|
||||||
<CascadingValue Value="null" Name="Form" TValue="IForm" IsFixed="@true">
|
<CascadingValue Value="this" Name="FormItem" TValue="IFormItem" IsFixed="@true">
|
||||||
<CascadingValue Value="this" Name="FormItem" TValue="IFormItem" IsFixed="@true">
|
@ChildContent
|
||||||
@ChildContent
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
</CascadingValue>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,10 +3,13 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using AntDesign.Core.Helpers.MemberPath;
|
||||||
using AntDesign.Core.Reflection;
|
using AntDesign.Core.Reflection;
|
||||||
using AntDesign.Form.Locale;
|
using AntDesign.Form.Locale;
|
||||||
using AntDesign.Forms;
|
using AntDesign.Forms;
|
||||||
@ -184,7 +187,7 @@ namespace AntDesign
|
|||||||
|
|
||||||
private AntLabelAlignType? FormLabelAlign => LabelAlign ?? Form?.LabelAlign;
|
private AntLabelAlignType? FormLabelAlign => LabelAlign ?? Form?.LabelAlign;
|
||||||
|
|
||||||
private string DisplayName => Label ?? _propertyReflector?.DisplayName ?? _propertyReflector?.PropertyName;
|
private string DisplayName => Label ?? _propertyReflector?.DisplayName;
|
||||||
|
|
||||||
private string _name;
|
private string _name;
|
||||||
private Action _nameChanged;
|
private Action _nameChanged;
|
||||||
@ -200,7 +203,7 @@ namespace AntDesign
|
|||||||
private FormValidateStatus? _originalValidateStatus;
|
private FormValidateStatus? _originalValidateStatus;
|
||||||
private Action _vaildateStatusChanged;
|
private Action _vaildateStatusChanged;
|
||||||
|
|
||||||
private Action<string[]> _onValidated;
|
private Action<string[]> _onValidated = _ => { };
|
||||||
|
|
||||||
private IEnumerable<FormValidationRule> _rules;
|
private IEnumerable<FormValidationRule> _rules;
|
||||||
|
|
||||||
@ -241,6 +244,8 @@ namespace AntDesign
|
|||||||
}
|
}
|
||||||
|
|
||||||
SetInternalIsRequired();
|
SetInternalIsRequired();
|
||||||
|
|
||||||
|
SetEventHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetClass()
|
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()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(Help))
|
if (!string.IsNullOrWhiteSpace(Help))
|
||||||
@ -386,7 +411,7 @@ namespace AntDesign
|
|||||||
|
|
||||||
private void UpdateValidateMessage()
|
private void UpdateValidateMessage()
|
||||||
{
|
{
|
||||||
if (_control == null)
|
if (_fieldIdentifier.Model == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -423,23 +448,6 @@ namespace AntDesign
|
|||||||
_fieldIdentifier = control.FieldIdentifier;
|
_fieldIdentifier = control.FieldIdentifier;
|
||||||
_control = control;
|
_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)
|
if (control.PopertyReflector is not null)
|
||||||
{
|
{
|
||||||
_propertyReflector = control.PopertyReflector;
|
_propertyReflector = control.PopertyReflector;
|
||||||
@ -461,8 +469,57 @@ namespace AntDesign
|
|||||||
StateHasChanged();
|
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()
|
ValidationResult[] IFormItem.ValidateFieldWithRules()
|
||||||
{
|
{
|
||||||
|
if (_propertyReflector is null)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(Name))
|
||||||
|
{
|
||||||
|
BuildPropertyWithName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_propertyReflector is null)
|
if (_propertyReflector is null)
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
@ -493,7 +550,7 @@ namespace AntDesign
|
|||||||
Rule = rule,
|
Rule = rule,
|
||||||
Value = propertyValue,
|
Value = propertyValue,
|
||||||
FieldName = _fieldIdentifier.FieldName,
|
FieldName = _fieldIdentifier.FieldName,
|
||||||
DisplayName = DisplayName,
|
DisplayName = DisplayName ?? _propertyReflector.PropertyName,
|
||||||
FieldType = _valueUnderlyingType,
|
FieldType = _valueUnderlyingType,
|
||||||
ValidateMessages = validateMessages,
|
ValidateMessages = validateMessages,
|
||||||
Model = Form.Model
|
Model = Form.Model
|
||||||
|
@ -43,9 +43,9 @@
|
|||||||
{
|
{
|
||||||
public class Address
|
public class Address
|
||||||
{
|
{
|
||||||
[Display(Name = "省份")]
|
[Display(Name = "Pr.")]
|
||||||
public string Province { get; set; }
|
public string Province { get; set; }
|
||||||
[Display(Name = "街道")]
|
[Display(Name = "St.")]
|
||||||
public string Street { get; set; }
|
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`:有两个控件,在 `FormItem` 内使用两个 `<FormItem NoStyle />` 分别绑定对应控件(一个FormItem下只能出现一个使用了@bing-Value的控件),对FormItem使用NoStyle,则FormItem的Grid布局会被忽略,就算主动使用了LabelCol或WrapperCol也不会产生效果。
|
||||||
|
|
||||||
这个场景还展示了复杂类型的表单验证,Address 属性是一个包含了两个属性的类型,当级联表单项绑定了它的属性时,会继承这个属性的所有 Attributes,但由于表单未绑定这个属性,外部表单无法获取 Required 和 Label。
|
这个场景还展示了复杂类型的表单验证,Address 属性是一个包含了两个属性的类型,当级联表单项绑定了它的属性时,会继承这个属性的所有 Attributes,但由于表单未绑定这个属性,外部表单无法获取 Required 和 Label。
|
||||||
|
|
||||||
- `BirthDate`:有两个内联控件,错误信息展示各自控件下,使用两个 `<FormItem />` 分别绑定对应控件,并修改 `style` 使其内联布局。
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
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