skartknet / Iconic

The ultimate icon picker for Umbraco backoffice
MIT License
6 stars 8 forks source link

Iconic V12 No longer allows separate front-end/back-office templates? #51

Open hfloyd opened 1 month ago

hfloyd commented 1 month ago

According to the Documentation, there should be two fields to configure the HTML template, but on V12, I see only a single Template field:

image

I would prefer to configure my own HTML manually in the front-end template, thus the Value should just be the necessary class names, but I found that I needed to include the full HTML in the "Template" field for the back-office icon rendering to work.

I did notice that the raw data stored is like this: {"icon":"fa-instagram","packageId":"fffb855f-140e-4838-bc2f-d08289ca8063"}

Which leads me to believe that it would be possible, using a different PropertyValueConverter to get info about the package, as well as the specific icon class, but it would mean I'd need to manually add the additional classes for FontAwesome ("fab", etc.) since there is no way to just store that information via the configuration.

skartknet commented 1 month ago

Hi @hfloyd ,

The reason we removed the frontend template was to align with a headless approach, where the icon can be used potentially in other platforms.

Instead of restoring the ability for Iconic to return the frontend template, it would be good to modify the returned data to add more useful information. You talk about modifying the PropertyValueConverter and yes, that's what I think we could do, but I don't understand why you're saying that you need to add the FA classes in there. Couldn't you do that in your frontend?

hfloyd commented 1 month ago

Hi Mario! What I mean is that for later versions of FA, there are actually 3 separate groups of icons, and you need to add the indicator for the icon group, for example: <i class="**fas** fa-yin-yang"></i> and <i class="**fab** fa-youtube"></i> and <i class="**far** fa-thumbs-up"></i>

For my project, yesterday I created a PropertyValueConverter based on yours which returns an object with a bit more info. Here I pull out the classes via RegEx, which is expedient, but probably not ideal. Perhaps if there could be a config property for "other required classes" which could just be combined with the actual selected value?

Here is my code, for reference:

using System;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Html;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Our.Iconic.Core;
using Our.Iconic.Core.Models;
using Our.Iconic.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
using Umbraco.Cms.Core.Services;

public class CustomIconicValueConverter : PropertyValueConverterBase, IDeliveryApiPropertyValueConverter
{
    private readonly ConfiguredPackagesCollection _configuredPackages;

    public CustomIconicValueConverter(IDataTypeService dataTypeService, ConfiguredPackagesCollection configuredPackages)
    {
        _configuredPackages = configuredPackages;
    }
    public override bool IsConverter(IPublishedPropertyType propertyType)
         => propertyType.EditorAlias.Equals("our.iconic");

    public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
        => typeof(IconicModel);

    public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
    {
        if (source == null) return null;

        SelectedIcon icon;
        if (source is JObject jObject)
        {
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
            icon = jObject.ToObject<SelectedIcon>();
#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
        }
        else
        {
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
#pragma warning disable CS8604 // Possible null reference argument.
            icon = JsonConvert.DeserializeObject<SelectedIcon>(source.ToString());
#pragma warning restore CS8604 // Possible null reference argument.
#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
        }

        return icon;
    }

    public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
    {
        var model = new IconicModel();

        if (inter == null) return model;

        var icon = (SelectedIcon)inter;

        var packages = _configuredPackages.GetConfiguredPackages(propertyType);

        if (icon != null && packages.ContainsKey(icon.PackageId))
        {
            var pckg = packages[icon.PackageId];
            var htmlString = pckg?.Template?.Replace("{icon}", icon.Icon) ?? string.Empty;

            string pattern = @"class\s*=\s*[""']([^""']*)[""']";

            Match match = Regex.Match(htmlString, pattern);
            if (match.Success)
            {
                string classAttributeContent = match.Groups[1].Value;
                model.IconClasses = classAttributeContent;
            }
            else
            {
                model.IconClasses =  icon.Icon;
            }

            model.HasIcon = true;
            model.IconValue = icon.Icon;
            model.Html = new HtmlString(htmlString);
            model.PackageId = pckg?.Id;
            model.PackageName = pckg != null && pckg.Name != null ? (string)pckg.Name : "";
        }

        return model;
    }

    public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => GetPropertyCacheLevel(propertyType);

    public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IconicResponse);

    public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner,
        IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview,
        bool expanding)
    {
        if (inter == null) return null;
        var result = new IconicResponse();

        var icon = (SelectedIcon)inter;
        result.Icon = icon.Icon;

        var packages = _configuredPackages.GetConfiguredPackages(propertyType);

        if (icon != null && packages.ContainsKey(icon.PackageId))
        {
            var pckg = packages[icon.PackageId];

            if (pckg != null)
            {
                result.PackageId = pckg.Id;
                result.PackageName = pckg.Name;
            }
        }

        return result;
    }
}

public class IconicModel
{
    public string IconValue { get; set; } = "";
    public string IconClasses { get; set; } = "";
    public HtmlString Html { get; set; } = new HtmlString(string.Empty);
    public Guid? PackageId { get; set; }
    public string PackageName { get; set; } = "";
    public bool HasIcon { get; set; } = false;
}

public class CustomIconicValueConverterComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        //De-register existing (Default) PropertyValueConverter

        //If the type is accessible (not internal) you can deregister it by the type:
        builder.PropertyValueConverters().Remove<Our.Iconic.Core.ValueConverters.IconicValueConverter>();
    }
}

Then used in a Razor View like this:

@if (socLink.Icon!.HasIcon)
{
    <i class="@socLink.Icon.IconClasses"></i>
}

I can understand why the Front-end HTML "pattern" field was originally added to the package, since it makes it possible to "mix" icon packages and have them rendered out through the same Razor code snippet - but rendering the correct package-specific HTML syntax.

skartknet commented 1 month ago

I agree that possibly adding an additional IconClasses is the way to go. It looks like you are ~inserting~ extracting those additional classes from the Template that is used for the backoffice? Am I right?

hfloyd commented 1 month ago

Yes, but after the Icon value is replaced in the template (since I want all the classes, and don't want "{icon}"):

    var htmlString = pckg?.Template?.Replace("{icon}", icon.Icon) ?? string.Empty;

            string pattern = @"class\s*=\s*[""']([^""']*)[""']";

            Match match = Regex.Match(htmlString, pattern);