From 825c6d78a45319fcddcd6473fd9a76e3e325612f Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Sun, 30 Aug 2020 00:07:02 +0800 Subject: [PATCH] Initial cut of Bootstrap 4 template --- .../Templates/IFormTemplate.cs | 3 +- ChameleonForms.Example/Startup.cs | 4 + .../Views/Home/Index.cshtml | 5 +- .../Views/Shared/_Bootstrap4Layout.cshtml | 20 ++ .../Views/_ViewStart.cshtml | 4 + .../unobtrusive-bootstrap.js | 40 ++++ .../Bootstrap4/Bootstrap4FormTemplate.cs | 196 ++++++++++++++++++ .../ButtonHtmlAttributesExtensions.cs | 45 ++++ .../Bootstrap4/ButtonSize.cs | 30 +++ .../Bootstrap4/ButtonStyle.cs | 100 +++++++++ .../FieldConfigurationExtensions.cs | 24 +++ .../BeginAlert.cshtml | 8 + .../BeginNavigation.cshtml | 3 + .../BeginNestedSection.cshtml | 17 ++ .../BeginSection.cshtml | 13 ++ .../EndAlert.cshtml | 4 + .../EndField.cshtml | 6 + .../EndForm.cshtml | 3 + .../EndNavigation.cshtml | 3 + .../EndNestedSection.cshtml | 5 + .../EndSection.cshtml | 3 + .../Field.cshtml | 52 +++++ .../GetAppendedHtml.cshtml | 2 + .../GetHint.cshtml | 3 + .../GetLabelHtml.cshtml | 17 ++ .../GetPrependedHtml.cshtml | 2 + .../MessageParagraph.cshtml | 5 + .../Params/AlertParams.cs | 11 + .../Params/BeginSectionParams.cs | 12 ++ .../Params/FieldConfigurationParams.cs | 11 + .../Params/FieldParams.cs | 23 ++ .../Params/LabelParams.cs | 15 ++ .../Params/ListParams.cs | 12 ++ .../Params/MessageParagraphParams.cs | 10 + .../RadioOrCheckboxList.cshtml | 7 + .../RequiredDesignator.cshtml | 2 + .../Default/DefaultFormTemplate.cs | 2 +- .../TwitterBootstrap3FormTemplate.cs | 2 +- ChameleonForms.Tests/FormTests.cs | 4 +- .../Default/DefaultFormTemplateTests.cs | 4 +- .../Templates/TwitterBootstrap3/FormTests.cs | 4 +- ChameleonForms/Form.cs | 3 +- ChameleonForms/ServiceCollectionExtensions.cs | 7 +- 43 files changed, 728 insertions(+), 18 deletions(-) create mode 100644 ChameleonForms.Example/Views/Shared/_Bootstrap4Layout.cshtml create mode 100644 ChameleonForms.Example/wwwroot/lib/jquery-validation-unobtrusive-bootstrap/unobtrusive-bootstrap.js create mode 100644 ChameleonForms.Templates/Bootstrap4/Bootstrap4FormTemplate.cs create mode 100644 ChameleonForms.Templates/Bootstrap4/ButtonHtmlAttributesExtensions.cs create mode 100644 ChameleonForms.Templates/Bootstrap4/ButtonSize.cs create mode 100644 ChameleonForms.Templates/Bootstrap4/ButtonStyle.cs create mode 100644 ChameleonForms.Templates/Bootstrap4/FieldConfigurationExtensions.cs create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/BeginAlert.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/BeginNavigation.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/BeginNestedSection.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/BeginSection.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/EndAlert.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/EndField.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/EndForm.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/EndNavigation.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/EndNestedSection.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/EndSection.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/Field.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/GetAppendedHtml.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/GetHint.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/GetLabelHtml.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/GetPrependedHtml.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/MessageParagraph.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/Params/AlertParams.cs create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/Params/BeginSectionParams.cs create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/Params/FieldConfigurationParams.cs create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/Params/FieldParams.cs create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/Params/LabelParams.cs create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/Params/ListParams.cs create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/Params/MessageParagraphParams.cs create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/RadioOrCheckboxList.cshtml create mode 100644 ChameleonForms.Templates/ChameleonFormsBootstrap4Template/RequiredDesignator.cshtml diff --git a/ChameleonForms.Core/Templates/IFormTemplate.cs b/ChameleonForms.Core/Templates/IFormTemplate.cs index 757e0b9b..657b4bff 100644 --- a/ChameleonForms.Core/Templates/IFormTemplate.cs +++ b/ChameleonForms.Core/Templates/IFormTemplate.cs @@ -33,8 +33,9 @@ public interface IFormTemplate /// The form method /// Any HTML attributes the form should use; specified as an anonymous object /// The encoding type for the form + /// Whether or not the form has been submitted i.e. it's a post back request /// The starting HTML for a form - IHtmlContent BeginForm(string action, FormMethod method, HtmlAttributes htmlAttributes, EncType? enctype); + IHtmlContent BeginForm(string action, FormMethod method, HtmlAttributes htmlAttributes, EncType? enctype, bool formSubmitted); /// /// Creates the ending HTML for a form. diff --git a/ChameleonForms.Example/Startup.cs b/ChameleonForms.Example/Startup.cs index ace17b24..ecc8aff5 100644 --- a/ChameleonForms.Example/Startup.cs +++ b/ChameleonForms.Example/Startup.cs @@ -1,5 +1,6 @@ using ChameleonForms.Example.Controllers.Filters; using ChameleonForms.Templates; +using ChameleonForms.Templates.Bootstrap4; using ChameleonForms.Templates.Default; using ChameleonForms.Templates.TwitterBootstrap3; using Microsoft.AspNetCore.Builder; @@ -38,6 +39,9 @@ public void ConfigureServices(IServiceCollection services) if (template.StartsWith("default")) return new DefaultFormTemplate(); + if (template == "bootstrap4") + return new Bootstrap4FormTemplate(); + return new TwitterBootstrap3FormTemplate(); }); services.AddChameleonForms(b => b.WithoutTemplateTypeRegistration()); diff --git a/ChameleonForms.Example/Views/Home/Index.cshtml b/ChameleonForms.Example/Views/Home/Index.cshtml index 8816ba32..51e8f19b 100644 --- a/ChameleonForms.Example/Views/Home/Index.cshtml +++ b/ChameleonForms.Example/Views/Home/Index.cshtml @@ -11,8 +11,9 @@

ChameleonForms vs ASP.NET MVC OOTB

diff --git a/ChameleonForms.Example/Views/Shared/_Bootstrap4Layout.cshtml b/ChameleonForms.Example/Views/Shared/_Bootstrap4Layout.cshtml new file mode 100644 index 00000000..2687d885 --- /dev/null +++ b/ChameleonForms.Example/Views/Shared/_Bootstrap4Layout.cshtml @@ -0,0 +1,20 @@ + + + + @ViewBag.Title + + + + + +
+ @RenderBody() +
+ + + + + + + + \ No newline at end of file diff --git a/ChameleonForms.Example/Views/_ViewStart.cshtml b/ChameleonForms.Example/Views/_ViewStart.cshtml index eb527c26..e3ab56ac 100644 --- a/ChameleonForms.Example/Views/_ViewStart.cshtml +++ b/ChameleonForms.Example/Views/_ViewStart.cshtml @@ -19,6 +19,10 @@ { Layout = "~/Views/Shared/_Layout.cshtml"; } + else if (ViewContext.HttpContext.Request.Cookies["template"] == "bootstrap4") + { + Layout = "~/Views/Shared/_Bootstrap4Layout.cshtml"; + } else { Layout = "~/Views/Shared/_Bootstrap3Layout.cshtml"; diff --git a/ChameleonForms.Example/wwwroot/lib/jquery-validation-unobtrusive-bootstrap/unobtrusive-bootstrap.js b/ChameleonForms.Example/wwwroot/lib/jquery-validation-unobtrusive-bootstrap/unobtrusive-bootstrap.js new file mode 100644 index 00000000..748b3a1f --- /dev/null +++ b/ChameleonForms.Example/wwwroot/lib/jquery-validation-unobtrusive-bootstrap/unobtrusive-bootstrap.js @@ -0,0 +1,40 @@ +// https://github.com/brecons/jquery-validation-unobtrusive-bootstrap + +(function ($) { + if($.validator && $.validator.unobtrusive){ + var defaultOptions = { + validClass: 'is-valid', + errorClass: 'is-invalid', + highlight: function (element, errorClass, validClass) { + $(element) + .removeClass(validClass) + .addClass(errorClass); + }, + unhighlight: function (element, errorClass, validClass) { + $(element) + .removeClass(errorClass) + .addClass(validClass); + } + }; + + $.validator.setDefaults(defaultOptions); + + $.validator.unobtrusive.options = { + errorClass: defaultOptions.errorClass, + validClass: defaultOptions.validClass, + errorElement: 'div', + errorPlacement: function (error, element) { + error.addClass('invalid-feedback'); + + if (element.next().is(".input-group-append")) { + error.insertAfter(element.next()); + } else { + error.insertAfter(element); + } + } + }; + } + else { + console.warn('$.validator is not defined. Please load this library **after** loading jquery.validate.js and jquery.validate.unobtrusive.js'); + } +})(jQuery); diff --git a/ChameleonForms.Templates/Bootstrap4/Bootstrap4FormTemplate.cs b/ChameleonForms.Templates/Bootstrap4/Bootstrap4FormTemplate.cs new file mode 100644 index 00000000..5dbbec73 --- /dev/null +++ b/ChameleonForms.Templates/Bootstrap4/Bootstrap4FormTemplate.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ChameleonForms.Component; +using ChameleonForms.Component.Config; +using ChameleonForms.Enums; +using ChameleonForms.FieldGenerators; +using ChameleonForms.FieldGenerators.Handlers; +using ChameleonForms.Templates.ChameleonFormsBootstrap4Template; +using ChameleonForms.Templates.ChameleonFormsBootstrap4Template.Params; +using Humanizer; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; +using RazorRenderer; + +namespace ChameleonForms.Templates.Bootstrap4 +{ + /// + /// The default Chameleon Forms form template renderer. + /// + public class Bootstrap4FormTemplate : Default.DefaultFormTemplate + { + private static readonly IEnumerable StyledButtonClasses = Enum.GetNames(typeof(ButtonStyle)) + .Select(x => x.Humanize()) + .ToArray(); + + private static readonly FieldDisplayType[] NormalFieldTypes = new[] { FieldDisplayType.DropDown, FieldDisplayType.SingleLineText, FieldDisplayType.MultiLineText }; + + /// + public override void PrepareFieldConfiguration(IFieldGenerator fieldGenerator, IFieldGeneratorHandler fieldGeneratorHandler, IFieldConfiguration fieldConfiguration, FieldParent fieldParent) + { + if (fieldParent == FieldParent.Form) + return; + + fieldConfiguration.InlineLabelWrapsElement(); + + fieldConfiguration.AddValidationClass("invalid-feedback"); + + var displayType = fieldGeneratorHandler.GetDisplayType(fieldConfiguration); + if (NormalFieldTypes.Contains(displayType)) + { + fieldConfiguration.Bag.CanBeInputGroup = true; + fieldConfiguration.AddClass("form-control"); + } + + if (displayType == FieldDisplayType.Checkbox) + { + fieldConfiguration.Bag.IsCheckboxControl = true; + // Hide the parent label otherwise it looks weird + fieldConfiguration.Label("").WithoutLabelElement(); + } + + if (displayType == FieldDisplayType.List) + fieldConfiguration.Bag.IsRadioOrCheckboxList = true; + } + + /// + public override IHtmlContent BeginForm(string action, FormMethod method, HtmlAttributes htmlAttributes, EncType? enctype, bool formSubmitted) + { + if (formSubmitted) + htmlAttributes.AddClass("was-validated"); + return HtmlCreator.BuildFormTag(action, method, htmlAttributes, enctype); + } + + /// + public override IHtmlContent EndForm() + { + return new EndForm().Render(); + } + + /// + public override IHtmlContent BeginSection(IHtmlContent heading = null, IHtmlContent leadingHtml = null, HtmlAttributes htmlAttributes = null) + { + return new BeginSection().Render(new BeginSectionParams {Heading = heading, LeadingHtml = leadingHtml, HtmlAttributes = htmlAttributes ?? new HtmlAttributes() }); + } + + /// + public override IHtmlContent EndSection() + { + return new EndSection().Render(); + } + + /// + public override IHtmlContent BeginNestedSection(IHtmlContent heading = null, IHtmlContent leadingHtml = null, HtmlAttributes htmlAttributes = null) + { + return new BeginNestedSection().Render(new BeginSectionParams { Heading = heading, LeadingHtml = leadingHtml, HtmlAttributes = htmlAttributes ?? new HtmlAttributes() }); + } + + /// + public override IHtmlContent EndNestedSection() + { + return new EndNestedSection().Render(); + } + + /// + public override IHtmlContent Field(IHtmlContent labelHtml, IHtmlContent elementHtml, IHtmlContent validationHtml, ModelMetadata fieldMetadata, IReadonlyFieldConfiguration fieldConfiguration, bool isValid) + { + return new Field().Render(new FieldParams + { + RenderMode = FieldRenderMode.Field, LabelHtml = labelHtml, ElementHtml = elementHtml, + ValidationHtml = validationHtml, FieldMetadata = fieldMetadata, FieldConfiguration = fieldConfiguration, + IsValid = isValid, RequiredDesignator = RequiredDesignator(fieldMetadata, fieldConfiguration, isValid) + }); + } + + /// + public override IHtmlContent BeginField(IHtmlContent labelHtml, IHtmlContent elementHtml, IHtmlContent validationHtml, ModelMetadata fieldMetadata, IReadonlyFieldConfiguration fieldConfiguration, bool isValid) + { + return new Field().Render(new FieldParams + { + RenderMode = FieldRenderMode.BeginField, + LabelHtml = labelHtml, + ElementHtml = elementHtml, + ValidationHtml = validationHtml, + FieldMetadata = fieldMetadata, + FieldConfiguration = fieldConfiguration, + IsValid = isValid, + RequiredDesignator = RequiredDesignator(fieldMetadata, fieldConfiguration, isValid) + }); + } + + /// + protected override IHtmlContent RequiredDesignator(ModelMetadata fieldMetadata, IReadonlyFieldConfiguration fieldConfiguration, bool isValid) + { + return new RequiredDesignator().Render(); + } + + /// + public override IHtmlContent EndField() + { + return new EndField().Render(); + } + + /// + public override IHtmlContent BeginMessage(MessageType messageType, IHtmlContent heading) + { + string alertType; + switch (messageType) + { + case MessageType.Warning: + alertType = "warning"; + break; + case MessageType.Action: + alertType = "primary"; + break; + case MessageType.Failure: + alertType = "danger"; + break; + case MessageType.Success: + alertType = "success"; + break; + default: + alertType = "info"; + break; + } + + return new BeginAlert().Render(new AlertParams {AlertType = alertType, Heading = heading }); + } + + /// + public override IHtmlContent EndMessage() + { + return new EndAlert().Render(); + } + + /// + public override IHtmlContent BeginNavigation() + { + return new BeginNavigation().Render(); + } + + /// + public override IHtmlContent EndNavigation() + { + return new EndNavigation().Render(); + } + + /// + public override IHtmlContent Button(IHtmlContent content, string type, string id, string value, HtmlAttributes htmlAttributes) + { + htmlAttributes = htmlAttributes ?? new HtmlAttributes(); + htmlAttributes.AddClass("btn"); + if (!StyledButtonClasses.Any(c => htmlAttributes.Attributes["class"].Contains(c))) + htmlAttributes.AddClass("btn-light"); + + return base.Button(content, type, id, value, htmlAttributes); + } + + /// + public override IHtmlContent RadioOrCheckboxList(IEnumerable list, bool isCheckbox) + { + return new RadioOrCheckboxList().Render(new ListParams {Items = list, IsCheckbox = isCheckbox}); + } + } +} \ No newline at end of file diff --git a/ChameleonForms.Templates/Bootstrap4/ButtonHtmlAttributesExtensions.cs b/ChameleonForms.Templates/Bootstrap4/ButtonHtmlAttributesExtensions.cs new file mode 100644 index 00000000..70908b23 --- /dev/null +++ b/ChameleonForms.Templates/Bootstrap4/ButtonHtmlAttributesExtensions.cs @@ -0,0 +1,45 @@ +using ChameleonForms.Component; +using Humanizer; + +namespace ChameleonForms.Templates.Bootstrap4 +{ + /// + /// Extension methods on for the Bootstrap 4 template. + /// + public static class ButtonHtmlAttributesExtensions + { + /// + /// Adds the given emphasis to the button. + /// + /// + /// @n.Submit("Submit").WithStyle(ButtonStyle.Warning) + /// + /// The Html Attributes from a navigation button + /// The style of button + /// The Html Attribute object so other methods can be chained off of it + public static ButtonHtmlAttributes WithStyle(this ButtonHtmlAttributes attrs, ButtonStyle style) + { + // ReSharper disable once MustUseReturnValue + if (style != ButtonStyle.Default) + attrs.AddClass(style.Humanize()); + return attrs; + } + + /// + /// Changes the button to use the given size. + /// + /// + /// @n.Submit("Submit").WithSize(ButtonSize.Large) + /// + /// The Html Attributes from a navigation button + /// The size of button + /// The Html Attribute object so other methods can be chained off of it + public static ButtonHtmlAttributes WithSize(this ButtonHtmlAttributes attrs, ButtonSize size) + { + // ReSharper disable once MustUseReturnValue + if (size != ButtonSize.Default && size != ButtonSize.NoneSpecified) + attrs.AddClass($"btn-{size.Humanize()}"); + return attrs; + } + } +} diff --git a/ChameleonForms.Templates/Bootstrap4/ButtonSize.cs b/ChameleonForms.Templates/Bootstrap4/ButtonSize.cs new file mode 100644 index 00000000..41d0c984 --- /dev/null +++ b/ChameleonForms.Templates/Bootstrap4/ButtonSize.cs @@ -0,0 +1,30 @@ +using System.ComponentModel; + +namespace ChameleonForms.Templates.Bootstrap4 +{ + /// + /// Bootstrap 4 button sizes: https://getbootstrap.com/docs/4.5/components/buttons/#sizes + /// + public enum ButtonSize + { + /// + /// None specified. + /// + [Description("")] + NoneSpecified, + /// + /// Small button size. + /// + [Description("sm")] + Small, + /// + /// Default button size. + /// + Default, + /// + /// Large button size. + /// + [Description("lg")] + Large + } +} diff --git a/ChameleonForms.Templates/Bootstrap4/ButtonStyle.cs b/ChameleonForms.Templates/Bootstrap4/ButtonStyle.cs new file mode 100644 index 00000000..f68d48b2 --- /dev/null +++ b/ChameleonForms.Templates/Bootstrap4/ButtonStyle.cs @@ -0,0 +1,100 @@ +using System.ComponentModel; + +namespace ChameleonForms.Templates.Bootstrap4 +{ + /// + /// Bootstrap 4 button styles: https://getbootstrap.com/docs/4.5/components/buttons/#examples + /// + public enum ButtonStyle + { + /// + /// Default styling. + /// + Default, + /// + /// Primary styling. + /// + [Description("btn-primary")] + Primary, + /// + /// Secondary styling. + /// + [Description("btn-secondary")] + Secondary, + /// + /// Success styling. + /// + [Description("btn-success")] + Success, + /// + /// Information styling. + /// + [Description("btn-info")] + Info, + /// + /// Warning styling. + /// + [Description("btn-warning")] + Warning, + /// + /// Danger styling. + /// + [Description("btn-primary")] + Danger, + /// + /// Light styling. + /// + [Description("btn-light")] + Light, + /// + /// Dark styling. + /// + [Description("btn-dark")] + Dark, + /// + /// Primary outline styling. + /// + [Description("btn-outline-primary")] + PrimaryOutline, + /// + /// Secondary outline styling. + /// + [Description("btn-outline-secondary")] + SecondaryOutline, + /// + /// Success outline styling. + /// + [Description("btn-outline-success")] + SuccessOutline, + /// + /// Information outline styling. + /// + [Description("btn-outline-info")] + InfoOutline, + /// + /// Warning outline styling. + /// + [Description("btn-outline-warning")] + WarningOutline, + /// + /// Danger outline styling. + /// + [Description("btn-outline-danger")] + DangerOutline, + /// + /// Light outline styling. + /// + [Description("btn-outline-light")] + LightOutline, + /// + /// Dark outline styling. + /// + [Description("btn-outline-dark")] + DarkOutline, + /// + /// Link styling. + /// + [Description("btn-link")] + Link + } +} \ No newline at end of file diff --git a/ChameleonForms.Templates/Bootstrap4/FieldConfigurationExtensions.cs b/ChameleonForms.Templates/Bootstrap4/FieldConfigurationExtensions.cs new file mode 100644 index 00000000..35ddcbe4 --- /dev/null +++ b/ChameleonForms.Templates/Bootstrap4/FieldConfigurationExtensions.cs @@ -0,0 +1,24 @@ +using ChameleonForms.Component.Config; + +namespace ChameleonForms.Templates.Bootstrap4 +{ + /// + /// Extension methods on for the Bootstrap 4 template. + /// + public static class FieldConfigurationExtensions + { + /// + /// Outputs the field in an input group using prepended and appended HTML. + /// + /// + /// @n.Field(labelHtml, elementHtml, validationHtml, metadata, new FieldConfiguration().Prepend(beforeHtml).Append(afterHtml).AsInputGroup(), false) + /// + /// The configuration for a field + /// The field configuration object to allow for method chaining + public static IFieldConfiguration AsInputGroup(this IFieldConfiguration fc) + { + fc.Bag.DisplayAsInputGroup = true; + return fc; + } + } +} diff --git a/ChameleonForms.Templates/ChameleonFormsBootstrap4Template/BeginAlert.cshtml b/ChameleonForms.Templates/ChameleonFormsBootstrap4Template/BeginAlert.cshtml new file mode 100644 index 00000000..0509f0fc --- /dev/null +++ b/ChameleonForms.Templates/ChameleonFormsBootstrap4Template/BeginAlert.cshtml @@ -0,0 +1,8 @@ +@using Microsoft.AspNetCore.Html; +@inherits RazorRenderer.BasePage +@{@: