Skip to content

Commit 448617d

Browse files
committed
Added implementation of manual field html in tag helpers, added missing prepend options to <field> and fixed bug where nested fields weren't being output from the parent field, but rather from the section
1 parent 0937375 commit 448617d

17 files changed

+464
-44
lines changed

ChameleonForms.Core/Component/Config/FieldConfiguration.cs

+2
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,8 @@ public void WriteTo(TextWriter writer, HtmlEncoder encoder)
918918
if (field != null)
919919
{
920920
field.WriteTo(writer, encoder);
921+
if (field is IDisposable disposable)
922+
disposable.Dispose();
921923
}
922924
}
923925

ChameleonForms.Example/Views/ExampleForms/Form2.cshtml

+19-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
@using ChameleonForms.Templates.TwitterBootstrap3
2+
@using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
3+
@inject ICompositeMetadataDetailsProvider MetadataDetailsProvider
24
@model ViewModelExample
35

46
@{
@@ -15,33 +17,41 @@
1517
@* Various incarnations of nested sections and fields and partials mixed in *@
1618
<form-section heading="Wassup" leading-html="InstructionalText" add-class="xyz" id="aaaaa" attr-style="border-top:red;" fluent-config='h => h.Attr(data_x => "y")'>
1719
<p>Yo</p>
20+
1821
<field for="ListId" add-class="asdf asf ew" label="awefawefEWAF #$%&" attr-data-test="wassup" append="aaa" as="RadioList" />
22+
23+
<field manual model-metadata="new DefaultModelMetadataProvider(MetadataDetailsProvider).GetMetadataForType(typeof(int))" is-valid="true" append="After Element" prepend="Before Element">
24+
<manual-element><strong>Element</strong></manual-element>
25+
<manual-label><strong>Label</strong></manual-label>
26+
<manual-validation><strong>validation</strong></manual-validation>
27+
</field>
28+
1929
<field for="SomeEnum" exclude="new Enum[]{SomeEnum.SomeOtherValue}" />
2030
<field for="Decimal" label="asdf" append="asdffff" step="1.2m">
21-
<field for="NullableInt"/>
22-
<partial name="_Child"/>
31+
<field for="NullableInt" />
32+
<partial name="_Child" />
2333
</field>
2434
<form-section heading-html="InstructionalText">
2535
<field for="TextAreaField" />
26-
<field for="Child.ChildField"/>
27-
<partial name="_Child"/>
36+
<field for="Child.ChildField" />
37+
<partial name="_Child" />
2838
</form-section>
29-
<partial name="_Child"/>
30-
<form-partial name="_Child2" for="Child"/>
39+
<partial name="_Child" />
40+
<form-partial name="_Child2" for="Child" />
3141
</form-section>
3242
<form-message heading="Heading" type="Action">
3343
<field-label for="UrlAsString" label="Overridden" />
3444
<field-element for="UrlAsString" add-class="addedclass" />
3545
<field-validation for="UrlAsString" fluent-config='fc => fc.AddValidationClass("asdf")' />
3646
</form-message>
37-
<form-partial name="_Child2" for="Child"/>
47+
<form-partial name="_Child2" for="Child" />
3848
@* Mix and match helper syntax with tag helpers and vice versa *@
3949
@Html.GetChameleonForm().FieldElementFor(m => m.ListId)
40-
<field-element for="Boolean"/>
50+
<field-element for="Boolean" />
4151
@using (var s = Html.GetChameleonForm().BeginSection("Section from helper syntax"))
4252
{
4353
@s.FieldFor(m => m.SomeEnum)
44-
<field for="Boolean"/>
54+
<field for="Boolean" />
4555
}
4656
@* Navigation *@
4757
<form-navigation>

ChameleonForms.Tests/PublicApiTests.ChameleonFormsPublicApi_ShouldNotChange_WithoutApproval.approved.txt

+47-1
Original file line numberDiff line numberDiff line change
@@ -480,11 +480,13 @@ namespace ChameleonForms.Component
480480
public interface ISection
481481
{
482482
ChameleonForms.Component.ISection<TPartialModel> CreatePartialSection<TPartialModel>(ChameleonForms.IForm<TPartialModel> partialModelForm);
483+
ChameleonForms.Component.Config.IFieldConfiguration Field(Microsoft.AspNetCore.Html.IHtmlContent labelHtml, Microsoft.AspNetCore.Html.IHtmlContent elementHtml, Microsoft.AspNetCore.Html.IHtmlContent validationHtml = null, Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata metadata = null, bool isValid = true, ChameleonForms.Component.Config.IFieldConfiguration fieldConfiguration = null);
483484
}
484485
public interface ISection<TModel> : ChameleonForms.Component.IFormComponent<TModel>, System.IDisposable
485486
{
486487
ChameleonForms.Component.ISection<TModel> CreatePartialSection(Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<TModel> partialHelper);
487488
ChameleonForms.Component.ISection<TPartialModel> CreatePartialSection<TPartialModel>(ChameleonForms.IForm<TPartialModel> partialModelForm);
489+
ChameleonForms.Component.Config.IFieldConfiguration Field(Microsoft.AspNetCore.Html.IHtmlContent labelHtml, Microsoft.AspNetCore.Html.IHtmlContent elementHtml, Microsoft.AspNetCore.Html.IHtmlContent validationHtml = null, Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata metadata = null, bool isValid = true, ChameleonForms.Component.Config.IFieldConfiguration fieldConfiguration = null);
488490
}
489491
public static class MessageExtensions
490492
{
@@ -545,7 +547,7 @@ namespace ChameleonForms.Component
545547
public ChameleonForms.Component.ISection<TPartialModel> CreatePartialSection<TPartialModel>(ChameleonForms.IForm<TPartialModel> partialModelForm) { }
546548
public override void Dispose() { }
547549
public override Microsoft.AspNetCore.Html.IHtmlContent End() { }
548-
public ChameleonForms.Component.Config.IFieldConfiguration Field(Microsoft.AspNetCore.Html.IHtmlContent labelHtml, Microsoft.AspNetCore.Html.IHtmlContent elementHtml, Microsoft.AspNetCore.Html.IHtmlContent validationHtml = null, Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata metadata = null, bool isValid = true) { }
550+
public ChameleonForms.Component.Config.IFieldConfiguration Field(Microsoft.AspNetCore.Html.IHtmlContent labelHtml, Microsoft.AspNetCore.Html.IHtmlContent elementHtml, Microsoft.AspNetCore.Html.IHtmlContent validationHtml = null, Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata metadata = null, bool isValid = true, ChameleonForms.Component.Config.IFieldConfiguration fieldConfiguration = null) { }
549551
}
550552
}
551553
namespace ChameleonForms.Config
@@ -773,6 +775,13 @@ namespace ChameleonForms.TagHelpers
773775
false})]
774776
public System.Func<object, Microsoft.AspNetCore.Html.IHtmlContent> OverrideFieldHtml { get; set; }
775777
public Microsoft.AspNetCore.Html.IHtmlContent OverrideFieldHtmlContent { get; set; }
778+
public string Prepend { get; set; }
779+
[System.Runtime.CompilerServices.Dynamic(new bool[] {
780+
false,
781+
true,
782+
false})]
783+
public System.Func<object, Microsoft.AspNetCore.Html.IHtmlContent> PrependHtml { get; set; }
784+
public Microsoft.AspNetCore.Html.IHtmlContent PrependHtmlContent { get; set; }
776785
public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { }
777786
}
778787
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElement("field")]
@@ -921,6 +930,43 @@ namespace ChameleonForms.TagHelpers
921930
public string Id { get; set; }
922931
public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { }
923932
}
933+
public class ManualFieldContext
934+
{
935+
public ManualFieldContext() { }
936+
public Microsoft.AspNetCore.Html.IHtmlContent Element { get; set; }
937+
public ChameleonForms.Component.Config.IFieldConfiguration FieldConfiguration { get; set; }
938+
public bool? IsValid { get; set; }
939+
public Microsoft.AspNetCore.Html.IHtmlContent Label { get; set; }
940+
public Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata ModelMetadata { get; set; }
941+
public Microsoft.AspNetCore.Html.IHtmlContent Validation { get; set; }
942+
}
943+
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElement("manual-element", ParentTag="field")]
944+
public class ManualFieldElementTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
945+
{
946+
public ManualFieldElementTagHelper() { }
947+
public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { }
948+
}
949+
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElement("manual-label", ParentTag="field")]
950+
public class ManualFieldLabelTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
951+
{
952+
public ManualFieldLabelTagHelper() { }
953+
public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { }
954+
}
955+
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElement("field", Attributes="manual")]
956+
public class ManualFieldTagHelper : ChameleonForms.TagHelpers.ModelAwareTagHelper
957+
{
958+
public ManualFieldTagHelper() { }
959+
public bool? IsValid { get; set; }
960+
public Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata ModelMetadata { get; set; }
961+
public override int Order { get; }
962+
public override System.Threading.Tasks.Task ProcessWhileAwareOfModelTypeAsync<TModel>(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { }
963+
}
964+
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElement("manual-validation", ParentTag="field")]
965+
public class ManualFieldValidationTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
966+
{
967+
public ManualFieldValidationTagHelper() { }
968+
public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { }
969+
}
924970
public class MessageParagraphTagHelper : ChameleonForms.TagHelpers.ModelAwareTagHelper
925971
{
926972
public MessageParagraphTagHelper() { }

ChameleonForms/Component/Field.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
2+
using System.IO;
23
using System.Linq.Expressions;
4+
using System.Text.Encodings.Web;
35
using ChameleonForms.Component.Config;
46
using ChameleonForms.FieldGenerators;
57
using Microsoft.AspNetCore.Html;
@@ -42,7 +44,8 @@ public class Field<TModel> : FormComponent<TModel>
4244
public Field(IForm<TModel> form, bool isParent, IFieldGenerator fieldGenerator, IFieldConfiguration config)
4345
: base(form, !isParent)
4446
{
45-
form.HtmlHelper.ViewData[Constants.ViewDataFieldKey] = this;
47+
if (isParent)
48+
form.HtmlHelper.ViewData[Constants.ViewDataFieldKey] = this;
4649

4750
_fieldGenerator = fieldGenerator;
4851
_config = config;

ChameleonForms/Component/Section.cs

+29-11
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ public interface ISection
2020
/// <typeparam name="TPartialModel">The model type of the partial view</typeparam>
2121
/// <returns>A section with the same characteristics as the current section, but using the given partial form</returns>
2222
ISection<TPartialModel> CreatePartialSection<TPartialModel>(IForm<TPartialModel> partialModelForm);
23+
24+
/// <summary>
25+
/// Outputs a field with passed in HTML.
26+
/// </summary>
27+
/// <param name="labelHtml">The HTML for the label part of the field</param>
28+
/// <param name="elementHtml">The HTML for the field element part of the field</param>
29+
/// <param name="validationHtml">The HTML for the validation markup part of the field</param>
30+
/// <param name="metadata">Any field metadata</param>
31+
/// <param name="isValid">Whether or not the field is valid</param>
32+
/// <param name="fieldConfiguration">Optional field configuration</param>
33+
/// <returns>A field configuration that can be used to output the field as well as configure it fluently</returns>
34+
IFieldConfiguration Field(IHtmlContent labelHtml, IHtmlContent elementHtml, IHtmlContent validationHtml = null,
35+
ModelMetadata metadata = null, bool isValid = true, IFieldConfiguration fieldConfiguration = null);
2336
}
2437

2538
/// <summary>
@@ -41,6 +54,19 @@ public interface ISection<TModel> : IFormComponent<TModel>, IDisposable
4154
/// <param name="partialHelper">The HTML helper from the partial view</param>
4255
/// <returns>A section with the same characteristics as the current section, but using the given partial form</returns>
4356
ISection<TModel> CreatePartialSection(IHtmlHelper<TModel> partialHelper);
57+
58+
/// <summary>
59+
/// Outputs a field with passed in HTML.
60+
/// </summary>
61+
/// <param name="labelHtml">The HTML for the label part of the field</param>
62+
/// <param name="elementHtml">The HTML for the field element part of the field</param>
63+
/// <param name="validationHtml">The HTML for the validation markup part of the field</param>
64+
/// <param name="metadata">Any field metadata</param>
65+
/// <param name="isValid">Whether or not the field is valid</param>
66+
/// <param name="fieldConfiguration">Optional field configuration</param>
67+
/// <returns>A field configuration that can be used to output the field as well as configure it fluently</returns>
68+
IFieldConfiguration Field(IHtmlContent labelHtml, IHtmlContent elementHtml, IHtmlContent validationHtml = null,
69+
ModelMetadata metadata = null, bool isValid = true, IFieldConfiguration fieldConfiguration = null);
4470
}
4571

4672
/// <summary>
@@ -75,18 +101,10 @@ public Section(IForm<TModel> form, IHtmlContent heading, bool nested, IHtmlConte
75101
Initialise();
76102
}
77103

78-
/// <summary>
79-
/// Outputs a field with passed in HTML.
80-
/// </summary>
81-
/// <param name="labelHtml">The HTML for the label part of the field</param>
82-
/// <param name="elementHtml">The HTML for the field element part of the field</param>
83-
/// <param name="validationHtml">The HTML for the validation markup part of the field</param>
84-
/// <param name="metadata">Any field metadata</param>
85-
/// <param name="isValid">Whether or not the field is valid</param>
86-
/// <returns>A field configuration that can be used to output the field as well as configure it fluently</returns>
87-
public IFieldConfiguration Field(IHtmlContent labelHtml, IHtmlContent elementHtml, IHtmlContent validationHtml = null, ModelMetadata metadata = null, bool isValid = true)
104+
/// <inheritdoc />
105+
public IFieldConfiguration Field(IHtmlContent labelHtml, IHtmlContent elementHtml, IHtmlContent validationHtml = null, ModelMetadata metadata = null, bool isValid = true, IFieldConfiguration fieldConfiguration = null)
88106
{
89-
var fc = new FieldConfiguration();
107+
var fc = fieldConfiguration ?? new FieldConfiguration();
90108
fc.SetField(() => Form.Template.Field(labelHtml, elementHtml, validationHtml, metadata, fc, isValid));
91109
return fc;
92110
}

ChameleonForms/HtmlHelperExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ public static Field<TModel> GetChameleonFormsField<TModel>(this IHtmlHelper<TMod
246246
if (field is Field<TModel> castedField)
247247
return castedField;
248248

249-
throw new InvalidOperationException($"Attempted to retrieve a ChameleonForms form field instance as Field<{typeof(TModel).Name}>, but instead found a {field.GetType().Name}.");
249+
throw new InvalidOperationException($"Attempted to retrieve a ChameleonForms form field instance as Field<{typeof(TModel).Name}>, but instead found a {field.GetType().FullName}.");
250250
}
251251

252252
/// <summary>

ChameleonForms/PartialViewSection.cs

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
using ChameleonForms.Component;
1+
using System;
2+
using ChameleonForms.Component;
3+
using ChameleonForms.Component.Config;
4+
using Microsoft.AspNetCore.Html;
5+
using Microsoft.AspNetCore.Mvc.ModelBinding;
26
using Microsoft.AspNetCore.Mvc.Rendering;
37

48
namespace ChameleonForms
59
{
6-
internal class PartialViewSection<TPartialModel> : ISection<TPartialModel>
10+
internal class PartialViewSection<TPartialModel> : ISection, ISection<TPartialModel>
711
{
812
private readonly object _parentSection;
913

@@ -28,6 +32,12 @@ public ISection<TPartialModel> CreatePartialSection(IHtmlHelper<TPartialModel> p
2832
: new PartialViewSection<TPartialModel>(Form.CreatePartialForm(partialHelper));
2933
}
3034

35+
public IFieldConfiguration Field(IHtmlContent labelHtml, IHtmlContent elementHtml, IHtmlContent validationHtml = null,
36+
ModelMetadata metadata = null, bool isValid = true, IFieldConfiguration fieldConfiguration = null)
37+
{
38+
return (_parentSection as ISection)?.Field(labelHtml, elementHtml, validationHtml, metadata, isValid, fieldConfiguration);
39+
}
40+
3141
public void Dispose()
3242
{
3343
Form.HtmlHelper.ViewData[Constants.ViewDataSectionKey] = _parentSection;

ChameleonForms/ServiceCollectionExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ namespace ChameleonForms
2121
// Update all dependencies to latest versions
2222
// Tidy up cshtml files
2323
// Add ability to switch unobtrusive validation on/off and html5 validation on/off (<form novalidate="novalidate">) - global default with per-form override? reference ValidationHtmlAttributeProvider in documentation
24-
// blog posts: razorgenerator, mvctestcontext, modelbindertestbase, ilmerge, client validation in aspnetcore, end-to-end test in-memory, disposablehtmlhelper, testing metadatadetailsprovider
24+
// blog posts: razorgenerator, mvctestcontext, modelbindertestbase, ilmerge, client validation in aspnetcore, end-to-end test in-memory, disposablehtmlhelper, testing metadatadetailsprovider, docfx, tag helpers including nested children
2525
// Generate nuget from .csproj rather than nuspec like https://github.com/LazZiya/ExpressLocalization/blob/master/LazZiya.ExpressLocalization/LazZiya.ExpressLocalization.csproj
2626
// [Range] client validation support
2727
// Add support for non jquery unobtrusive validation

ChameleonForms/TagHelpers/FieldConfigurationTagHelper.cs

+25
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ public class FieldConfigurationTagHelper : TagHelper
2626
/// </summary>
2727
public IHtmlContent AppendHtmlContent { get; set; }
2828

29+
30+
/// <summary>
31+
/// Prepended HTML as a <see cref="String"/>.
32+
/// </summary>
33+
public string Prepend { get; set; }
34+
35+
/// <summary>
36+
/// Prepended HTML as templated razor delegate.
37+
/// </summary>
38+
public Func<dynamic, IHtmlContent> PrependHtml { get; set; }
39+
40+
/// <summary>
41+
/// Prepended HTML as a <see cref="IHtmlContent"/>.
42+
/// </summary>
43+
public IHtmlContent PrependHtmlContent { get; set; }
44+
2945
/// <summary>
3046
/// Hint as a <see cref="String"/>.
3147
/// </summary>
@@ -87,6 +103,15 @@ public override Task ProcessAsync(TagHelperContext context, TagHelperOutput outp
87103
if (AppendHtmlContent != null)
88104
fc.Append(AppendHtmlContent);
89105

106+
if (Prepend != null)
107+
fc.Prepend(Prepend.ToHtml());
108+
109+
if (PrependHtml != null)
110+
fc.Prepend(PrependHtml);
111+
112+
if (PrependHtmlContent != null)
113+
fc.Prepend(PrependHtmlContent);
114+
90115
if (Hint != null)
91116
fc.WithHint(Hint);
92117

0 commit comments

Comments
 (0)