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 +@{@: