Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Add buttonTagHelper and submitTagHelper for formaction #5089

Closed
wants to merge 1 commit into from
Closed
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
185 changes: 185 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/ButtonTagHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Routing;

namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;button&gt; elements.
/// </summary>
[HtmlTargetElement("button", Attributes = ActionAttributeName)]
[HtmlTargetElement("button", Attributes = ControllerAttributeName)]
[HtmlTargetElement("button", Attributes = AreaAttributeName)]
[HtmlTargetElement("button", Attributes = RouteAttributeName)]
[HtmlTargetElement("button", Attributes = RouteValuesDictionaryName)]
[HtmlTargetElement("button", Attributes = RouteValuesPrefix + "*")]
public class ButtonTagHelper : TagHelper
{
private const string ActionAttributeName = "asp-action";
private const string ControllerAttributeName = "asp-controller";
private const string AreaAttributeName = "asp-area";
private const string RouteAttributeName = "asp-route";
private const string RouteValuesDictionaryName = "asp-all-route-data";
private const string RouteValuesPrefix = "asp-route-";
private const string FormAction = "formaction";
private IDictionary<string, string> _routeValues;

/// <summary>
/// Creates a new <see cref="ButtonTagHelper"/>.
/// </summary>
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
public ButtonTagHelper(IUrlHelperFactory urlHelperFactory)
{
UrlHelperFactory = urlHelperFactory;
}

/// <inheritdoc />
public override int Order => -1000;

/// <summary>
/// Gets or sets the <see cref="Rendering.ViewContext"/> for the current request.
/// </summary>
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }

protected IUrlHelperFactory UrlHelperFactory { get; }

/// <summary>
/// The name of the action method.
/// </summary>
[HtmlAttributeName(ActionAttributeName)]
public string Action { get; set; }

/// <summary>
/// The name of the controller.
/// </summary>
[HtmlAttributeName(ControllerAttributeName)]
public string Controller { get; set; }

/// <summary>
/// The name of the area.
/// </summary>
[HtmlAttributeName(AreaAttributeName)]
public string Area { get; set; }

/// <summary>
/// Name of the route.
/// </summary>
/// <remarks>
/// Must be <c>null</c> if <see cref="Action"/> or <see cref="Controller"/> is non-<c>null</c>.
/// </remarks>
[HtmlAttributeName(RouteAttributeName)]
public string Route { get; set; }

/// <summary>
/// Additional parameters for the route.
/// </summary>
[HtmlAttributeName(RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix)]
public IDictionary<string, string> RouteValues
{
get
{
if (_routeValues == null)
{
_routeValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}

return _routeValues;
}
set
{
_routeValues = value;
}
}

/// <inheritdoc />
/// <remarks>Does nothing if user provides an <c>formaction</c> attribute.</remarks>
/// <exception cref="InvalidOperationException">
/// Thrown if <c>formaction</c> attribute is provided and <see cref="Action"/>, <see cref="Controller"/>,
/// or <see cref="Route"/> are non-<c>null</c> or if the user provided <c>asp-route-*</c> attributes.
/// Also thrown if <see cref="Route"/> and one or both of <see cref="Action"/> and <see cref="Controller"/>
/// are non-<c>null</c>
/// </exception>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

if (output == null)
{
throw new ArgumentNullException(nameof(output));
}

// If "formaction" is already set, it means the user is attempting to use a normal button.
if (output.Attributes.ContainsName(FormAction))
{
if (Action != null || Controller != null || Area != null || Route != null || RouteValues.Count != 0)
{
// User specified a formaction and one of the bound attributes; can't determine the formaction attribute.
throw new InvalidOperationException(
Resources.FormatButtonTagHelper_CannotOverrideFormAction(
"<button>",
ActionAttributeName,
ControllerAttributeName,
AreaAttributeName,
RouteAttributeName,
RouteValuesPrefix,
FormAction));
}
}
else
{
RouteValueDictionary routeValues = null;
if (_routeValues != null && _routeValues.Count > 0)
{
routeValues = new RouteValueDictionary(_routeValues);
}

if (Area != null)
{
if (routeValues == null)
{
routeValues = new RouteValueDictionary();
}

// Unconditionally replace any value from asp-route-area.
routeValues["area"] = Area;
}

if (Route == null)
{
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
var url = urlHelper.Action(Action, Controller, routeValues);
output.Attributes.SetAttribute(FormAction, url);
}
else if (Action != null || Controller != null)
{
// Route and Action or Controller were specified. Can't determine the formaction attribute.
throw new InvalidOperationException(
Resources.FormatButtonTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified(
"<button>",
RouteAttributeName,
ActionAttributeName,
ControllerAttributeName,
FormAction));
}
else
{
var urlHelper = UrlHelperFactory.GetUrlHelper(ViewContext);
var url = urlHelper.RouteUrl(Route, routeValues);
output.Attributes.SetAttribute(FormAction, url);
}
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,16 @@
<data name="PropertyOfTypeCannotBeNull" xml:space="preserve">
<value>The '{0}' property of '{1}' must not be null.</value>
</data>
<data name="ButtonTagHelper_CannotOverrideFormAction" xml:space="preserve">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest renaming existing resources e.g. AnchorTagHelper_CannotOverrideHref and reusing them here. Resources are internal so there's no compat worry. And maintenance will be easier if we keep the numbers and repetitions down.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙈 Never mind. "a" versus "an" and the different ordering of the FormTagHelper_CannotOverrideAction parameters makes this difficult.

<value>Cannot override the '{6}' attribute for {0}. A {0} with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute.</value>
</data>
<data name="ButtonTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified" xml:space="preserve">
<value>Cannot determine a '{4}' attribute for {0}. A {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.</value>
</data>
<data name="SubmitTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified" xml:space="preserve">
<value>Cannot determine a '{4}' attribute for {0}. An {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.</value>
</data>
<data name="SubmitTagHelper_CannotOverrideFormAction" xml:space="preserve">
<value>Cannot override the '{6}' attribute for {0}. An {0} with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute.</value>
</data>
</root>
Loading