370 lines
17 KiB
C#
370 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel.DataAnnotations;
|
|
using System.Linq;
|
|
using System.Linq.Expressions;
|
|
using Microsoft.AspNet.Mvc.Rendering.Expressions;
|
|
using Microsoft.AspNet.Routing;
|
|
|
|
namespace Microsoft.AspNet.Mvc.Rendering
|
|
{
|
|
public static class AngularExtensions
|
|
{
|
|
public static HtmlString ngPasswordFor<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
|
|
{
|
|
return html.ngPasswordFor(expression, null);
|
|
}
|
|
|
|
public static HtmlString ngPasswordFor<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
|
|
{
|
|
return html.ngPasswordFor(expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
|
|
}
|
|
|
|
public static HtmlString ngPasswordFor<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
|
|
{
|
|
return html.ngTextBoxFor(expression, MergeAttributes(
|
|
new RouteValueDictionary { { "type", "password" } },
|
|
htmlAttributes));
|
|
}
|
|
|
|
public static HtmlString ngTextBoxFor<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
|
|
{
|
|
return html.ngTextBoxFor(expression, new RouteValueDictionary());
|
|
}
|
|
|
|
public static HtmlString ngTextBoxFor<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
|
|
{
|
|
return html.ngTextBoxFor(expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
|
|
}
|
|
|
|
public static HtmlString ngTextBoxFor<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
|
|
{
|
|
var helper = html as AngularHtmlHelper<TModel>;
|
|
if (helper == null)
|
|
{
|
|
throw new InvalidOperationException("You need to configure the services container to return AngularHtmlHelper<T> for IHtmlHelper<T>.");
|
|
}
|
|
|
|
var expressionText = ExpressionHelper.GetExpressionText(expression);
|
|
var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, helper.ViewData, helper.ModelMetadataProvider);
|
|
var ngAttributes = new Dictionary<string, object>();
|
|
|
|
ngAttributes["type"] = "text";
|
|
|
|
// Angular binding to client-side model (scope). This is required for Angular validation to work.
|
|
var valueFieldName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(expressionText);
|
|
ngAttributes["name"] = valueFieldName;
|
|
ngAttributes["ng-model"] = valueFieldName;
|
|
|
|
// Set input type
|
|
if (string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.EmailAddress), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ngAttributes["type"] = "email";
|
|
}
|
|
else if (metadata.ModelType == typeof(Uri)
|
|
|| string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.Url), StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.ImageUrl), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ngAttributes["type"] = "url";
|
|
}
|
|
else if (IsNumberType(metadata.ModelType))
|
|
{
|
|
ngAttributes["type"] = "number";
|
|
if (IsIntegerType(metadata.ModelType))
|
|
{
|
|
ngAttributes["step"] = "1";
|
|
}
|
|
else
|
|
{
|
|
ngAttributes["step"] = "any";
|
|
}
|
|
}
|
|
else if (metadata.ModelType == typeof(DateTime))
|
|
{
|
|
if (string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.Date), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ngAttributes["type"] = "date";
|
|
}
|
|
else if (string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.DateTime), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ngAttributes["type"] = "datetime";
|
|
}
|
|
}
|
|
|
|
// Add attributes for Angular validation
|
|
//var clientValidators = metadata.GetValidators(html.ViewContext.Controller.ControllerContext)
|
|
// .SelectMany(v => v.GetClientValidationRules());
|
|
var clientValidators = helper.GetClientValidators(null, metadata);
|
|
|
|
foreach (var validator in clientValidators)
|
|
{
|
|
if (string.Equals(validator.ValidationType, "length"))
|
|
{
|
|
if (validator.ValidationParameters.ContainsKey("min"))
|
|
{
|
|
ngAttributes["ng-minlength"] = validator.ValidationParameters["min"];
|
|
}
|
|
if (validator.ValidationParameters.ContainsKey("max"))
|
|
{
|
|
ngAttributes["ng-maxlength"] = validator.ValidationParameters["max"];
|
|
}
|
|
}
|
|
else if (string.Equals(validator.ValidationType, "required"))
|
|
{
|
|
ngAttributes["required"] = null;
|
|
}
|
|
else if (string.Equals(validator.ValidationType, "range"))
|
|
{
|
|
if (validator.ValidationParameters.ContainsKey("min"))
|
|
{
|
|
ngAttributes["min"] = validator.ValidationParameters["min"];
|
|
}
|
|
if (validator.ValidationParameters.ContainsKey("max"))
|
|
{
|
|
ngAttributes["max"] = validator.ValidationParameters["max"];
|
|
}
|
|
}
|
|
else if (string.Equals(validator.ValidationType, "equalto"))
|
|
{
|
|
// CompareAttribute validator
|
|
var fieldToCompare = validator.ValidationParameters["other"]; // e.g. *.NewPassword
|
|
var other = validator.ValidationParameters["other"].ToString();
|
|
if (other.StartsWith("*."))
|
|
{
|
|
// The built-in CompareAttributeAdapter prepends *. to the property name so we strip it off here
|
|
other = other.Substring("*.".Length);
|
|
}
|
|
ngAttributes["app-equal-to"] = other;
|
|
// TODO: Actually write the Angular directive to use this
|
|
}
|
|
// TODO: Regex, Phone(regex)
|
|
}
|
|
|
|
// Render!
|
|
if (metadata.Model != null)
|
|
{
|
|
ngAttributes.Add("value", metadata.Model.ToString());
|
|
}
|
|
|
|
var tag = new TagBuilder("input");
|
|
tag.MergeAttributes(MergeAttributes(ngAttributes, htmlAttributes));
|
|
return tag.ToHtmlString(TagRenderMode.SelfClosing);
|
|
}
|
|
|
|
//private static bool IsNumberType(Type type)
|
|
//{
|
|
// switch (Type.GetTypeCode(type))
|
|
// {
|
|
// case TypeCode.Int16:
|
|
// case TypeCode.Int32:
|
|
// case TypeCode.Int64:
|
|
// case TypeCode.UInt16:
|
|
// case TypeCode.UInt32:
|
|
// case TypeCode.UInt64:
|
|
// case TypeCode.Decimal:
|
|
// case TypeCode.Double:
|
|
// case TypeCode.Single:
|
|
// return true;
|
|
// }
|
|
// return false;
|
|
//}
|
|
|
|
private static bool IsNumberType(Type type)
|
|
{
|
|
if (type == typeof(Int16) ||
|
|
type == typeof(Int32) ||
|
|
type == typeof(Int64) ||
|
|
type == typeof(UInt16) ||
|
|
type == typeof(UInt32) ||
|
|
type == typeof(UInt64) ||
|
|
type == typeof(Decimal) ||
|
|
type == typeof(Double) ||
|
|
type == typeof(Single))
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//private static bool IsIntegerType(Type type)
|
|
//{
|
|
// switch (Type.GetTypeCode(type))
|
|
// {
|
|
// case TypeCode.Int16:
|
|
// case TypeCode.Int32:
|
|
// case TypeCode.Int64:
|
|
// case TypeCode.UInt16:
|
|
// case TypeCode.UInt32:
|
|
// case TypeCode.UInt64:
|
|
// return true;
|
|
// }
|
|
// return false;
|
|
//}
|
|
|
|
private static bool IsIntegerType(Type type)
|
|
{
|
|
if (type == typeof(Int16) ||
|
|
type == typeof(Int32) ||
|
|
type == typeof(Int64) ||
|
|
type == typeof(UInt16) ||
|
|
type == typeof(UInt32) ||
|
|
type == typeof(UInt64))
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static HtmlString ngDropDownListFor<TModel, TProperty, TDisplayProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> propertyExpression, Expression<Func<TModel, TDisplayProperty>> displayExpression, string source, string nullOption, object htmlAttributes)
|
|
{
|
|
return ngDropDownListFor(html, propertyExpression, displayExpression, source, nullOption, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
|
|
}
|
|
|
|
public static HtmlString ngDropDownListFor<TModel, TProperty, TDisplayProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> propertyExpression, Expression<Func<TModel, TDisplayProperty>> displayExpression, string source, string nullOption, IDictionary<string, object> htmlAttributes)
|
|
{
|
|
var helper = html as AngularHtmlHelper<TModel>;
|
|
if (helper == null)
|
|
{
|
|
throw new InvalidOperationException("You need to configure the services container to return AngularHtmlHelper<T> for IHtmlHelper<T>.");
|
|
}
|
|
|
|
var propertyExpressionText = ExpressionHelper.GetExpressionText(propertyExpression);
|
|
var displayExpressionText = ExpressionHelper.GetExpressionText(displayExpression);
|
|
var metadata = ExpressionMetadataProvider.FromLambdaExpression(propertyExpression, helper.ViewData, helper.ModelMetadataProvider);
|
|
var tag = new TagBuilder("select");
|
|
|
|
var valueFieldName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(propertyExpressionText);
|
|
var displayFieldName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(displayExpressionText);
|
|
|
|
var displayFieldNameParts = displayFieldName.Split('.');
|
|
displayFieldName = displayFieldNameParts[displayFieldNameParts.Length - 1];
|
|
|
|
tag.Attributes["id"] = html.Id(propertyExpressionText);
|
|
tag.Attributes["name"] = valueFieldName;
|
|
tag.Attributes["ng-model"] = valueFieldName;
|
|
|
|
var ngOptionsFormat = "a.{0} as a.{1} for a in {2}";
|
|
var ngOptions = string.Format(ngOptionsFormat, valueFieldName, displayFieldName, source);
|
|
tag.Attributes["ng-options"] = ngOptions;
|
|
|
|
if (nullOption != null)
|
|
{
|
|
var nullOptionTag = new TagBuilder("option");
|
|
nullOptionTag.Attributes["value"] = string.Empty;
|
|
nullOptionTag.SetInnerText(nullOption);
|
|
tag.InnerHtml = nullOptionTag.ToString();
|
|
}
|
|
|
|
var clientValidators = helper.GetClientValidators(null, metadata);
|
|
var isRequired = clientValidators.SingleOrDefault(cv => string.Equals(cv.ValidationType, "required", StringComparison.OrdinalIgnoreCase)) != null;
|
|
if (isRequired)
|
|
{
|
|
tag.Attributes["required"] = string.Empty;
|
|
}
|
|
|
|
tag.MergeAttributes(htmlAttributes, replaceExisting: true);
|
|
|
|
return tag.ToHtmlString(TagRenderMode.Normal);
|
|
}
|
|
|
|
public static HtmlString ngValidationMessageFor<TModel, TProperty>(this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string formName)
|
|
{
|
|
return ngValidationMessageFor(htmlHelper, expression, formName, ((IDictionary<string, object>)new RouteValueDictionary()));
|
|
}
|
|
|
|
public static HtmlString ngValidationMessageFor<TModel, TProperty>(this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string formName, object htmlAttributes)
|
|
{
|
|
return ngValidationMessageFor(htmlHelper, expression, formName, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
|
|
}
|
|
|
|
public static HtmlString ngValidationMessageFor<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, string formName, IDictionary<string, object> htmlAttributes)
|
|
{
|
|
var helper = html as AngularHtmlHelper<TModel>;
|
|
if (helper == null)
|
|
{
|
|
throw new InvalidOperationException("You need to configure the services container to return AngularHtmlHelper<T> for IHtmlHelper<T>.");
|
|
}
|
|
|
|
var expressionText = ExpressionHelper.GetExpressionText(expression);
|
|
var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, html.ViewData, helper.ModelMetadataProvider);
|
|
var modelName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(expressionText);
|
|
|
|
//var clientValidators = metadata.GetValidators(html.ViewContext.Controller.ControllerContext)
|
|
// .SelectMany(v => v.GetClientValidationRules());
|
|
var clientValidators = helper.GetClientValidators(null, metadata);
|
|
var tags = new List<TagBuilder>();
|
|
|
|
// Get validation messages from data type
|
|
// TODO: How to get validation messages from model metadata? All methods/properties required seem protected internal :(
|
|
|
|
foreach (var validator in clientValidators)
|
|
{
|
|
var tag = new TagBuilder("span");
|
|
tag.Attributes["ng-cloak"] = string.Empty;
|
|
|
|
if (string.Equals(validator.ValidationType, "required"))
|
|
{
|
|
tag.Attributes["ng-show"] = string.Format("({0}.submitAttempted || {0}.{1}.$dirty || {0}.{1}.visited) && {0}.{1}.$error.{2}", formName, modelName, "required");
|
|
tag.SetInnerText(validator.ErrorMessage);
|
|
}
|
|
else if (string.Equals(validator.ValidationType, "length"))
|
|
{
|
|
tag.Attributes["ng-show"] = string.Format("({0}.submitAttempted || {0}.{1}.$dirty || {0}.{1}.visited) && ({0}.{1}.$error.minlength || {0}.{1}.$error.maxlength)",
|
|
formName, modelName);
|
|
tag.SetInnerText(validator.ErrorMessage);
|
|
}
|
|
else if (string.Equals(validator.ValidationType, "range"))
|
|
{
|
|
tag.Attributes["ng-show"] = string.Format("({0}.submitAttempted || {0}.{1}.$dirty || {0}.{1}.visited) && ({0}.{1}.$error.min || {0}.{1}.$error.max)",
|
|
formName, modelName);
|
|
tag.SetInnerText(validator.ErrorMessage);
|
|
}
|
|
// TODO: Regex, equalto, remote
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
|
|
tag.MergeAttributes(htmlAttributes);
|
|
tags.Add(tag);
|
|
}
|
|
|
|
return html.Raw(String.Concat(tags.Select(t => t.ToString())));
|
|
}
|
|
|
|
public static string ngValidationClassFor<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, string formName, string className)
|
|
{
|
|
var helper = html as AngularHtmlHelper<TModel>;
|
|
if (helper == null)
|
|
{
|
|
throw new InvalidOperationException("You need to configure the services container to return AngularHtmlHelper<T> for IHtmlHelper<T>.");
|
|
}
|
|
|
|
var expressionText = ExpressionHelper.GetExpressionText(expression);
|
|
var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, html.ViewData, helper.ModelMetadataProvider);
|
|
var modelName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(expressionText);
|
|
var ngClassFormat = "{{ '{0}' : ({1}.submitAttempted || {1}.{2}.$dirty || {1}.{2}.visited) && {1}.{2}.$invalid }}";
|
|
|
|
return string.Format(ngClassFormat, className, formName, modelName);
|
|
}
|
|
|
|
private static IDictionary<string, object> MergeAttributes(IDictionary<string, object> source, IDictionary<string, object> target)
|
|
{
|
|
if (target == null)
|
|
{
|
|
return source;
|
|
}
|
|
|
|
// Keys in target win over keys in source
|
|
foreach (var pair in source)
|
|
{
|
|
if (!target.ContainsKey(pair.Key))
|
|
{
|
|
target[pair.Key] = pair.Value;
|
|
}
|
|
}
|
|
|
|
return target;
|
|
}
|
|
}
|
|
} |