Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.
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
156 changes: 156 additions & 0 deletions src/Microsoft.AspNet.Mvc.TagHelpers/AnchorTagHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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 System.Linq;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;

namespace Microsoft.AspNet.Mvc.TagHelpers
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;a&gt; elements.
/// </summary>
[TagName("a")]
public class AnchorTagHelper : TagHelper
{
private const string RouteAttributePrefix = "route-";
private const string Href = "href";

[Activate]
private IHtmlGenerator Generator { get; set; }

/// <summary>
/// The name of the action method.
/// </summary>
/// <remarks>Must be <c>null</c> if <see cref="Route"/> is non-<c>null</c>.</remarks>
public string Action { get; set; }

/// <summary>
/// The name of the controller.
/// </summary>
/// <remarks>Must be <c>null</c> if <see cref="Route"/> is non-<c>null</c>.</remarks>
public string Controller { get; set; }

/// <summary>
/// The protocol for the URL, such as &quot;http&quot; or &quot;https&quot;.
/// </summary>
public string Protocol { get; set; }

/// <summary>
/// The host name.
/// </summary>
public string Host { get; set; }

/// <summary>
/// The URL fragment name.
/// </summary>
public string Fragment { 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>
public string Route { get; set; }

/// <inheritdoc />
/// <remarks>Does nothing if user provides an "href" attribute. Throws an
/// <see cref="InvalidOperationException"/> if "href" attribute is provided and <see cref="Action"/>,
/// <see cref="Controller"/>, or <see cref="Route"/> are non-<c>null</c>.</remarks>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var routePrefixedAttributes = output.FindPrefixedAttributes(RouteAttributePrefix);

// If there's an "href" on the tag it means it's being used as a normal anchor.
if (output.Attributes.ContainsKey(Href))
Copy link
Contributor

Choose a reason for hiding this comment

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

agreed general rule is that MVC tag helpers should simply not overwrite what user (or an earlier tag helper in line) specified. so, everywhere else, the IHtmlGenerator returns a TagBuilder with attributes that might not be copied to the TagHelperOutput because they are already there -- and no error results. there's nothing special here and it should follow the general rule. that is, get rid of this entire if block

Copy link
Contributor

Choose a reason for hiding this comment

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

on second thought, given Process() should do something when all the bound attributes are null, maybe there is something special here.
nevermind

{
if (Action != null ||
Controller != null ||
Route != null ||
Protocol != null ||
Host != null ||
Fragment != null ||
routePrefixedAttributes.Any())
{
// User specified an href and one of the bound attributes; can't determine the href attribute.
throw new InvalidOperationException(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we shouldn't throw here.
example - The a tag is populated by javascript based on the id tag.
so instead do nothing?

CC @DamianEdwards

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 this is another case where, like "controller" and "route-value" in the <form/> helper, we should be consistent. I agree we shouldn't second-guess the users's intent when they force the tag helper to no-op.

Copy link
Author

Choose a reason for hiding this comment

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

Talked in person.

Resources.FormatAnchorTagHelper_CannotOverrideSpecifiedHref(
"<a>",
nameof(Action).ToLowerInvariant(),
nameof(Controller).ToLowerInvariant(),
nameof(Route).ToLowerInvariant(),
nameof(Protocol).ToLowerInvariant(),
nameof(Host).ToLowerInvariant(),
nameof(Fragment).ToLowerInvariant(),
RouteAttributePrefix,
Href));
}
}
else
{
TagBuilder tagBuilder;
var routeValues = GetRouteValues(output, routePrefixedAttributes);

if (Route == null)
{
tagBuilder = Generator.GenerateActionLink(linkText: string.Empty,
actionName: Action,
controllerName: Controller,
protocol: Protocol,
hostname: Host,
fragment: Fragment,
routeValues: routeValues,
htmlAttributes: null);
}
else if (Action != null || Controller != null)
{
// Route and Action or Controller were specified. Can't determine the href attribute.
throw new InvalidOperationException(
Copy link
Contributor

Choose a reason for hiding this comment

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

agree this is an odd mix but throwing seems harsh. if the user cares, they should see their attribute values are ignored. or, restore the values and they'll see attributes intended for the tag helper just carry through into the final HTML.

Copy link
Author

Choose a reason for hiding this comment

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

Talked in person.

Resources.FormatAnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified(
"<a>",
nameof(Route).ToLowerInvariant(),
nameof(Action).ToLowerInvariant(),
nameof(Controller).ToLowerInvariant(),
Href));
}
else
{
tagBuilder = Generator.GenerateRouteLink(linkText: string.Empty,
routeName: Route,
protocol: Protocol,
hostName: Host,
fragment: Fragment,
routeValues: routeValues,
htmlAttributes: null);
}

if (tagBuilder != null)
{
output.MergeAttributes(tagBuilder);
}
}
}

// TODO: We will not need this method once https://github.com/aspnet/Razor/issues/89 is completed.
private static Dictionary<string, object> GetRouteValues(
TagHelperOutput output, IEnumerable<KeyValuePair<string, string>> routePrefixedAttributes)
{
Dictionary<string, object> routeValues = null;
if (routePrefixedAttributes.Any())
{
// Prefixed values should be treated as bound attributes, remove them from the output.
output.RemoveRange(routePrefixedAttributes);

// Generator.GenerateForm does not accept a Dictionary<string, string> for route values.
routeValues = routePrefixedAttributes.ToDictionary(
attribute => attribute.Key.Substring(RouteAttributePrefix.Length),
attribute => (object)attribute.Value);
}

return routeValues;
}
}
}

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

6 changes: 6 additions & 0 deletions src/Microsoft.AspNet.Mvc.TagHelpers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified" xml:space="preserve">
<value>Cannot determine an {4} for {0}. An {0} with a specified {1} must not have an {2} or {3} attribute.</value>
</data>
<data name="AnchorTagHelper_CannotOverrideSpecifiedHref" xml:space="preserve">
<value>Cannot determine an {8} for {0}. An {0} with a specified {8} must not have attributes starting with {7} or an {1}, {2}, {3}, {4}, {5} or {6} attribute.</value>
</data>
<data name="FormTagHelper_CannotDetermineAction" xml:space="preserve">
<value>Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.</value>
</data>
Expand Down
Loading