Skip to content

Commit

Permalink
feat: Get component view via reflection + fix headings (#33)
Browse files Browse the repository at this point in the history
* feat: Improved component views factory start

* feat: Get component views via reflection

* fix: Missing reference + tests
  • Loading branch information
jimwashbrook authored Jun 2, 2023
1 parent 01f60b9 commit 1e20b4c
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 38 deletions.
78 changes: 71 additions & 7 deletions src/Dfe.PlanTech.Web/Helpers/ComponentViewsFactory.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Reflection;

namespace Dfe.PlanTech.Web.Helpers;

public static class ComponentViewsFactory
public class ComponentViewsFactory
{
public static string GetViewForType(object model) => $"Components/{model.GetType().Name}";
}
private const string GENERATED_VIEW_NAMESPACE = "AspNetCoreGeneratedDocument";
private const string SHARED_PATH = "Views_Shared";

private readonly Type[] _viewTypes;
private readonly ILogger<ComponentViewsFactory> _logger;

public ComponentViewsFactory(ILogger<ComponentViewsFactory> logger)
{
_viewTypes = GetSharedViewTypes().ToArray();
_logger = logger;
}

/// <summary>
/// Tries to find matching shared view for the passed model, based on the model's name
/// </summary>
/// <param name="model">The model for a view</param>
/// <param name="viewPath">Found view path (if any matching view)</param>
/// <returns>Whether a view was successfully found or not</returns>
public bool TryGetViewForType(object model, out string? viewPath)
{
var componentTypeName = model.GetType().Name;

var matchingViewType = _viewTypes.FirstOrDefault(FileNameMatchesComponentTypeName(componentTypeName));

if (matchingViewType == null)
{
_logger.LogWarning("Could not find matching view for {model}", model);
viewPath = null;
return false;
}

viewPath = GetFolderPathForType(matchingViewType);

return true;
}

/// <summary>
/// Gets file path to view from the Type name
/// </summary>
/// <remarks>
/// Removes "Views_Shared_ from the name, then replaces all underscores with forward slashes
/// </remarks>
/// <param name="matchingViewType"></param>
/// <returns>Folder path to the view for this type</returns>
private static string GetFolderPathForType(Type matchingViewType)
=> matchingViewType.Name!.Replace("Views_Shared_", "").Replace("_", "/");

/// <summary>
/// Does the passed component type name match the type name?
/// </summary>
/// <param name="componentTypeName"></param>
/// <returns></returns>
private static Func<Type, bool> FileNameMatchesComponentTypeName(string componentTypeName)
=> type => type.Name.EndsWith($"_{componentTypeName}");

/// <summary>
/// Get all Types generated from Views that are in the "Shared" folder (or sub-folder)
/// </summary>
private static IEnumerable<Type> GetSharedViewTypes() => Assembly.GetExecutingAssembly()
.GetTypes()
.Where(IsSharedViewType);

/// <summary>
/// Is this type a View type, which is in the Shared folder path?
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private static bool IsSharedViewType(Type type) => type.Namespace == GENERATED_VIEW_NAMESPACE && type.Name.StartsWith(SHARED_PATH);
}
3 changes: 3 additions & 0 deletions src/Dfe.PlanTech.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Dfe.PlanTech.Infrastructure.Contentful.Content.Renderers;
using Dfe.PlanTech.Infrastructure.Contentful.Content.Renderers.Options;
using Dfe.PlanTech.Infrastructure.Contentful.Helpers;
using Dfe.PlanTech.Web.Helpers;
using GovUk.Frontend.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
Expand Down Expand Up @@ -32,6 +33,8 @@
Classes = "govuk-link",
});

builder.Services.AddScoped<ComponentViewsFactory>();

builder.Services.AddCQRSServices();

var app = builder.Build();
Expand Down
20 changes: 10 additions & 10 deletions src/Dfe.PlanTech.Web/TagHelpers/HeadingTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class HeaderTagHelper : TagHelper
{
private readonly ILogger<HeaderTagHelper> _logger;

public Header? Header { get; init; }
public Header? Model { get; set; }

public HeaderTagHelper(ILogger<HeaderTagHelper> logger)
{
Expand All @@ -18,15 +18,15 @@ public HeaderTagHelper(ILogger<HeaderTagHelper> logger)

public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (Header == null)
if (Model == null)
{
_logger.LogWarning($"Missing {nameof(Header)}");
_logger.LogWarning($"Missing {nameof(Model)}");
return;
}

if (Header.Tag == Domain.Content.Enums.HeaderTag.Unknown)
if (Model.Tag == Domain.Content.Enums.HeaderTag.Unknown)
{
_logger.LogWarning($"Could not find {nameof(Header.Tag)} for {nameof(Header)}");
_logger.LogWarning($"Could not find {nameof(Model.Tag)} for {nameof(Model)}");
}

var html = GetHtml();
Expand All @@ -38,7 +38,7 @@ public string GetHtml()
{
var stringBuilder = new StringBuilder();
AppendOpenTag(stringBuilder);
stringBuilder.Append(Header!.Text);
stringBuilder.Append(Model!.Text);
AppendCloseTag(stringBuilder);

return stringBuilder.ToString();
Expand All @@ -47,7 +47,7 @@ public string GetHtml()
private StringBuilder AppendCloseTag(StringBuilder stringBuilder)
{
stringBuilder.Append("</");
stringBuilder.Append(Header!.Tag.ToString());
stringBuilder.Append(Model!.Tag.ToString());
stringBuilder.Append('>');

return stringBuilder;
Expand All @@ -56,10 +56,10 @@ private StringBuilder AppendCloseTag(StringBuilder stringBuilder)
private StringBuilder AppendOpenTag(StringBuilder stringBuilder)
{
stringBuilder.Append('<');
stringBuilder.Append(Header!.Tag.ToString());
stringBuilder.Append(Model!.Tag.ToString());
stringBuilder.Append(" class=\"");
stringBuilder.Append(Header.GetClassForSize());
stringBuilder.Append('>');
stringBuilder.Append(Model.GetClassForSize());
stringBuilder.Append("\">");

return stringBuilder;
}
Expand Down
5 changes: 1 addition & 4 deletions src/Dfe.PlanTech.Web/Views/Shared/Components/Header.cshtml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
@using Dfe.PlanTech.Web.Helpers;
@using Dfe.PlanTech.Domain.Content.Enums;

@model Dfe.PlanTech.Domain.Content.Models.Header;

<header heading=@Model></header>
<header model=@Model></header>
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
@using Dfe.PlanTech.Domain.Content.Models;
@using Dfe.PlanTech.Domain.Content.Models.Buttons;
@using Dfe.PlanTech.Web.Helpers;

@inject ComponentViewsFactory ComponentViewsFactory;

@model Dfe.PlanTech.Domain.Content.Models.ContentComponent;

@{
await Html.RenderPartialAsync(ComponentViewsFactory.GetViewForType(Model), Model);
if(ComponentViewsFactory.TryGetViewForType(Model, out string? path) && !string.IsNullOrEmpty(path)){
await Html.RenderPartialAsync(path, Model);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Dfe.PlanTech.Domain.Content.Models;
using Dfe.PlanTech.Web.Helpers;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

namespace Dfe.PlanTech.Web.UnitTests.Helpers;
Expand All @@ -11,9 +12,11 @@ public void Should_ReturnStringWithTypeName_When_PassedObject()
{
var header = new Header();

var viewName = ComponentViewsFactory.GetViewForType(header);
var factory = new ComponentViewsFactory(new NullLogger<ComponentViewsFactory>());
var success = factory.TryGetViewForType(header, out string? viewPath);

Assert.Contains(header.GetType().Name, viewName);
Assert.Contains("Components", viewName);
Assert.True(success);
Assert.Contains(header.GetType().Name, viewPath);
Assert.Contains("Components", viewPath);
}
}
22 changes: 11 additions & 11 deletions tests/Dfe.PlanTech.Web.UnitTests/TagHelpers/HeaderTagHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public void Should_RenderH1Tag_When_H1()
var loggerMock = new Mock<ILogger<HeaderTagHelper>>();
var tagHelper = new HeaderTagHelper(loggerMock.Object)
{
Header = header
Model = header
};

var html = tagHelper.GetHtml();
Expand All @@ -46,7 +46,7 @@ public void Should_RenderH2Tag_When_H2()
var loggerMock = new Mock<ILogger<HeaderTagHelper>>();
var tagHelper = new HeaderTagHelper(loggerMock.Object)
{
Header = header
Model = header
};

var html = tagHelper.GetHtml();
Expand All @@ -69,7 +69,7 @@ public void Should_RenderH3Tag_When_H3()
var loggerMock = new Mock<ILogger<HeaderTagHelper>>();
var tagHelper = new HeaderTagHelper(loggerMock.Object)
{
Header = header
Model = header
};

var html = tagHelper.GetHtml();
Expand All @@ -92,14 +92,14 @@ public void Should_RenderRightClass_When_SmallSize()
var loggerMock = new Mock<ILogger<HeaderTagHelper>>();
var tagHelper = new HeaderTagHelper(loggerMock.Object)
{
Header = header
Model = header
};

var html = tagHelper.GetHtml();

var expectedClass = header.GetClassForSize();

Assert.Contains($" class=\"{expectedClass}>", html);
Assert.Contains($" class=\"{expectedClass}\">", html);
}

[Fact]
Expand All @@ -115,14 +115,14 @@ public void Should_RenderRightClass_When_MediumSize()
var loggerMock = new Mock<ILogger<HeaderTagHelper>>();
var tagHelper = new HeaderTagHelper(loggerMock.Object)
{
Header = header
Model = header
};

var html = tagHelper.GetHtml();

var expectedClass = header.GetClassForSize();

Assert.Contains($" class=\"{expectedClass}>", html);
Assert.Contains($" class=\"{expectedClass}\">", html);
}

[Fact]
Expand All @@ -138,14 +138,14 @@ public void Should_RenderRightClass_When_LargeSize()
var loggerMock = new Mock<ILogger<HeaderTagHelper>>();
var tagHelper = new HeaderTagHelper(loggerMock.Object)
{
Header = header
Model = header
};

var html = tagHelper.GetHtml();

var expectedClass = header.GetClassForSize();

Assert.Contains($" class=\"{expectedClass}>", html);
Assert.Contains($" class=\"{expectedClass}\">", html);
}

[Fact]
Expand All @@ -161,13 +161,13 @@ public void Should_RenderRightClass_When_ExtraLarge()
var loggerMock = new Mock<ILogger<HeaderTagHelper>>();
var tagHelper = new HeaderTagHelper(loggerMock.Object)
{
Header = header
Model = header
};

var html = tagHelper.GetHtml();

var expectedClass = header.GetClassForSize();

Assert.Contains($" class=\"{expectedClass}>", html);
Assert.Contains($" class=\"{expectedClass}\">", html);
}
}

0 comments on commit 1e20b4c

Please sign in to comment.