Skip to content

Initial cut of Bootstrap 4 template #169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ChameleonForms.Core/Templates/IFormTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ public interface IFormTemplate
/// <param name="method">The form method</param>
/// <param name="htmlAttributes">Any HTML attributes the form should use; specified as an anonymous object</param>
/// <param name="enctype">The encoding type for the form</param>
/// <param name="formSubmitted">Whether or not the form has been submitted i.e. it's a post back request</param>
/// <returns>The starting HTML for a form</returns>
IHtmlContent BeginForm(string action, FormMethod method, HtmlAttributes htmlAttributes, EncType? enctype);
IHtmlContent BeginForm(string action, FormMethod method, HtmlAttributes htmlAttributes, EncType? enctype, bool formSubmitted);

/// <summary>
/// Creates the ending HTML for a form.
Expand Down
4 changes: 4 additions & 0 deletions ChameleonForms.Example/Startup.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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());
Expand Down
5 changes: 3 additions & 2 deletions ChameleonForms.Example/Views/Home/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
<ul>
<li><a href="?template=default">Default</a></li>
<li><a href="?template=defaultnojquery">Default - without jQuery</a></li>
<li><a href="?template=bootstrap">Twitter Bootstrap</a></li>
<li><a href="?template=bootstrapnojquery">Twitter Bootstrap - without jQuery</a></li>
<li><a href="?template=bootstrap3">Twitter Bootstrap 3</a></li>
<li><a href="?template=bootstrap3nojquery">Twitter Bootstrap 3 - without jQuery</a></li>
<li><a href="?template=bootstrap4">Bootstrap 4</a></li>
</ul>

<h2>ChameleonForms vs ASP.NET MVC <abbr title="out-of-the-box">OOTB</abbr></h2>
Expand Down
20 changes: 20 additions & 0 deletions ChameleonForms.Example/Views/Shared/_Bootstrap4Layout.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
</head>
<body>
<div class="container">
@RenderBody()
</div>
<script type="text/javascript" src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
<script type="text/javascript" src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script type="text/javascript" src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
<script type="text/javascript" src="~/lib/jquery-validation-unobtrusive-bootstrap/unobtrusive-bootstrap.js"></script>
<script type="text/javascript" src="~/lib/chameleonforms/unobtrusive-date-validation.chameleonforms.js"></script>
</body>
</html>
4 changes: 4 additions & 0 deletions ChameleonForms.Example/Views/_ViewStart.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
196 changes: 196 additions & 0 deletions ChameleonForms.Templates/Bootstrap4/Bootstrap4FormTemplate.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The default Chameleon Forms form template renderer.
/// </summary>
public class Bootstrap4FormTemplate : Default.DefaultFormTemplate
{
private static readonly IEnumerable<string> StyledButtonClasses = Enum.GetNames(typeof(ButtonStyle))
.Select(x => x.Humanize())
.ToArray();

private static readonly FieldDisplayType[] NormalFieldTypes = new[] { FieldDisplayType.DropDown, FieldDisplayType.SingleLineText, FieldDisplayType.MultiLineText };

/// <inheritdoc />
public override void PrepareFieldConfiguration<TModel, T>(IFieldGenerator<TModel, T> fieldGenerator, IFieldGeneratorHandler<TModel, T> 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;
}

/// <inheritdoc />
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);
}

/// <inheritdoc />
public override IHtmlContent EndForm()
{
return new EndForm().Render();
}

/// <inheritdoc />
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() });
}

/// <inheritdoc />
public override IHtmlContent EndSection()
{
return new EndSection().Render();
}

/// <inheritdoc />
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() });
}

/// <inheritdoc />
public override IHtmlContent EndNestedSection()
{
return new EndNestedSection().Render();
}

/// <inheritdoc />
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)
});
}

/// <inheritdoc />
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)
});
}

/// <inheritdoc />
protected override IHtmlContent RequiredDesignator(ModelMetadata fieldMetadata, IReadonlyFieldConfiguration fieldConfiguration, bool isValid)
{
return new RequiredDesignator().Render();
}

/// <inheritdoc />
public override IHtmlContent EndField()
{
return new EndField().Render();
}

/// <inheritdoc />
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 });
}

/// <inheritdoc />
public override IHtmlContent EndMessage()
{
return new EndAlert().Render();
}

/// <inheritdoc />
public override IHtmlContent BeginNavigation()
{
return new BeginNavigation().Render();
}

/// <inheritdoc />
public override IHtmlContent EndNavigation()
{
return new EndNavigation().Render();
}

/// <inheritdoc />
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);
}

/// <inheritdoc />
public override IHtmlContent RadioOrCheckboxList(IEnumerable<IHtmlContent> list, bool isCheckbox)
{
return new RadioOrCheckboxList().Render(new ListParams {Items = list, IsCheckbox = isCheckbox});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using ChameleonForms.Component;
using Humanizer;

namespace ChameleonForms.Templates.Bootstrap4
{
/// <summary>
/// Extension methods on <see cref="HtmlAttributes"/> for the Bootstrap 4 template.
/// </summary>
public static class ButtonHtmlAttributesExtensions
{
/// <summary>
/// Adds the given emphasis to the button.
/// </summary>
/// <example>
/// @n.Submit("Submit").WithStyle(ButtonStyle.Warning)
/// </example>
/// <param name="attrs">The Html Attributes from a navigation button</param>
/// <param name="style">The style of button</param>
/// <returns>The Html Attribute object so other methods can be chained off of it</returns>
public static ButtonHtmlAttributes WithStyle(this ButtonHtmlAttributes attrs, ButtonStyle style)
{
// ReSharper disable once MustUseReturnValue
if (style != ButtonStyle.Default)
attrs.AddClass(style.Humanize());
return attrs;
}

/// <summary>
/// Changes the button to use the given size.
/// </summary>
/// <example>
/// @n.Submit("Submit").WithSize(ButtonSize.Large)
/// </example>
/// <param name="attrs">The Html Attributes from a navigation button</param>
/// <param name="size">The size of button</param>
/// <returns>The Html Attribute object so other methods can be chained off of it</returns>
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;
}
}
}
30 changes: 30 additions & 0 deletions ChameleonForms.Templates/Bootstrap4/ButtonSize.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.ComponentModel;

namespace ChameleonForms.Templates.Bootstrap4
{
/// <summary>
/// Bootstrap 4 button sizes: https://getbootstrap.com/docs/4.5/components/buttons/#sizes
/// </summary>
public enum ButtonSize
{
/// <summary>
/// None specified.
/// </summary>
[Description("")]
NoneSpecified,
/// <summary>
/// Small button size.
/// </summary>
[Description("sm")]
Small,
/// <summary>
/// Default button size.
/// </summary>
Default,
/// <summary>
/// Large button size.
/// </summary>
[Description("lg")]
Large
}
}
Loading