Font Awesome Icon Picker for Episerver

A custom episerver widget might seem daunting, but it doesn't have to be. Build an icon picker property together with me and see how easy it is!

Font Awesome Icon Picker for Episerver

It might seem like a big hurdle to dust off skills you probably didn't even pick up circa 2012, when Dojo was at its height. But do not fret, as you most often don't need to be a black belt in the EPiServer dojo at all. You can start small, and still make incredibly useful widgets. For the most part, you don't even need to do everything through dojo, as you will see.

In this post we will be creating a widget for easily selecting an icon from the amazing Font Awesome library. It will, in the end, be a simple string property that we'll use to generate the correct classes to a span element that shows the icon in the frontend.

The finished widget with the Icon Picker dialog open
The finished widget

Module.config

First of all, we need to setup module.config with a path to where we will host our new widget;

<?xml version="1.0" encoding="utf-8"?>
<module>
    <dojo>
        <paths>
            <add name="Nansen" path="~/ClientResources/Nansen" />
        </paths>
    </dojo>
</module>

I like to namespace it so I know that all our custom scripts are within the same directory. That way it's easy to see what you have added yourself and what you might have installed through Nuget etc.

Editor Descriptor

The next thing we want is to create an Editor Descriptor. This registers the new widget with EPiServer and makes it easy to implement as a property on pages with the use of an UIHint.

Create a file called IconPickerEditorDescriptor.cs and add the following:

using EPiServer.Shell.ObjectEditing;
using EPiServer.Shell.ObjectEditing.EditorDescriptors;
using System;
using System.Collections.Generic;

namespace Nansen.Web.Models.EditorDescriptors
{
    [EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "IconPicker")]
    public class IconPickerEditorDescriptor : EditorDescriptor
    {
        public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
        {
            ClientEditingClass = "nansen/IconPicker/widget";

            base.ModifyMetadata(metadata, attributes);
        }
    }
}

You will have to change your namespace according to how you have your project setup. Notice how we assign the name IconPicker to the UIHint attribute. The ClientEditingClass is the path to our upcoming javascript file, from the Name value we added in our module.config.

Page Property

In order to be able to test our widget, we need to add our new property to a page or block. Find a suitable place and add the following property declaration to it:

[Display(
    Name = "FontAwesome Icon Prop",
    GroupName = SystemTabNames.Content,
    Order = 200)]
[UIHint("IconPicker")]
public virtual string Icon { get; set; }

As you can see, it's a simple string property, but the magic comes from using our new [UIHint("IconPicker")] attribute. This will make Edit mode load up and serve our javascript instead of just showing the regular string input box.

Your first widget

Let's check so our config so far is correct. Let's make a small Hello World widget to test it out.

Create a file in ~/ClientResources/Nansen/IconPicker/ called widget.js and add the following:

define([
    `dojo/_base/declare`,
    `dijit/_Widget`,
    `dijit/_TemplatedMixin`,
],
function (
    declare,
    _Widget,
    _TemplatedMixin,
) {
    return declare(`nansen.IconPicker`,
        [_Widget, _TemplatedMixin],
        {
            templateString: `
            <div class="dijitInline">
                <h2>HELLO WORLD!</h2>
            </div>
        `
        }
    )
})

This small file is a good starting point when creating new widgets later on, however you will have to add a couple of methods that should always be available. We will have those in the upcoming script for our Icon Picker.

Save the file, and go to the page in EPi Edit mode. As long as everything was correctly setup with the Module configuration and your Editor Descriptor, you should now have something like this on the page:

Image of widget in EPiServer edit mode
Woah! Your first widget!

If you can't find your new amazing widget, open up the Developer toolbar and look for any 404s regarding your script file. It's important that the path in the EditorDescriptor is correct, otherwise it won't find the javascript file at all. If you do find an error, look at the path it is trying to load it from, and change it accordingly in the configuration above. Once you see the above widget, we're good to continue!

The Icon Picker widget

Down to what we're all here for; the actual Icon Picker widget.

Remove the code we added in the last step, and copy this in there instead:

define([ // eslint-disable-line
    `dojo/query`, // for querying dom elements in widget
    `dojo/on`, // event handling
    `dojo/_base/declare`,
    `dojo/_base/lang`,
    `dijit/_CssStateMixin`,
    `dijit/_Widget`,
    `dijit/_TemplatedMixin`,
    `dijit/_WidgetsInTemplateMixin`,
    `dijit/Dialog`,
    `epi/epi`,
    `epi/shell/widget/_ValueRequiredMixin` // provides validation for required
],
function (
    query,
    on,
    declare,
    lang,
    _CssStateMixin,
    _Widget,
    _TemplatedMixin,
    _WidgetsInTemplateMixin,
    Dialog,
    epi,
    _ValueRequiredMixin
) {
    return declare(`nansen.IconPicker`,
        [_Widget, _TemplatedMixin, _WidgetsInTemplateMixin, _CssStateMixin, _ValueRequiredMixin],
        {
            // user defined variables...
            fontAwesomeVersion: '5.13.0', // Change this to your correct version!
            SOLID: true, // Change dependent of icon styles loaded in css
            REGULAR: true, // Change dependent of icon styles loaded in css
            BRANDS: false, // Change dependent of icon styles loaded in css
            // non user stuff.
            intermediateChanges: false,
            dialog: null,
            value: null,
            pickedIcon: null,
            templateString: `
                <div class="dijitInline">
                    <div data-dojo-attach-point="IconPicker" class="IconPicker">
                        <span></span>
                    </div>
                </div>
            `,
            // Add the following code blocks in order here.
        }
    )
})

A bit more going on here already. We have more dependencies loaded from dojo and epi. To be totally honest I don't know if all of these are needed, but it doesn't really hurt to have them defined. The meat and potatoes of it is inside of the return declare() function.

We'll start out with some variables (line 31) that the users should change depending on which version of Font Awesome is installed in the project, and which styles are included in the css. We will use these later when doing the graphql call to the api to get our list of icons available. This way we'll only get the icons for the correct version and styles.

We then follow that up with some internally used variables (line 35), and finally our base templateString (line 40). This is the html that gets printed out initially to the property display in EPi Edit mode. We give it an attach point so we can find it with dojo later on. This is good practice, because you might end up with multiple properties of the same type on a page - if you just use regular DOM programming you might get a handle to another instance of it and open up the wrong dialog etc.

It also includes an empty span tag. This is where we will show our icon when selected later on.

At the bottom of the snippet you can see a comment (line 47); This is where all the following snippets should be placed, one after the other.

Let's add our postCreate() function, that automatically gets called when all props are setup, but before the widget is added to the DOM. This is where you put all of your initial setup and call everything else that'll happen when using the widget.

postCreate() {
    // call the base implementation
    this.inherited(arguments)

    // add css files to style our widget, and to load the font awesome icons in edit mode
    this.addCssLink(`/ClientResources/Nansen/IconPicker/styles.css`, `interal`)
    this.addCssLink(`https://pro.fontawesome.com/releases/v${this.fontAwesomeVersion}/css/all.css`, `external`)

    // create our dialog
    this.createIconDialog()

    // do we already have an icon set? In that case, show it!
    this.pickedIcon = this.value

    // bind click event to show the dialog we created above
    const that = this
    on(query(this.IconPicker),
        `click`,
        function (e) {
            e.preventDefault()

            that.showIconDialog()
        })
},

We'll start with adding some function calls to add additional css to the edit page, create the dialog that will show all the icons, set the current value if one exists and add a click event to open said dialog when clicked.

The on and query calls are the first dojo specific things you'll see. on is for adding events to dojo elements, and query is their version of the vanilla querySelectorAll(). The difference is that we can go to our named attached and pick up the correct instance of the widget! This is why we send in this.IconPicker to the query, it refers to the element we created in the templateString earlier (remember our data-dojo-attach-point="IconPicker"? That's it!).

Let's continue with our addCssLink() function.

addCssLink(path, id) {
    const cssId = 'IconPicker' + id
    if (!document.getElementById(cssId)) { // only if ID isn't found...
        const head = document.getElementsByTagName('head')[0]
        const link = document.createElement('link')
        link.id = cssId
        link.rel = 'stylesheet'
        link.type = 'text/css'
        link.href = path
        link.media = 'all'
        head.appendChild(link)
    }
},

This adds link tags to the header with specific ID's, so we can make sure we don't add more than one instance of it when multiple icon pickers are used on the same page. As you could see in the previous method, we use it to add our widget css, and the css for our specified version of Font Awesome, so we can show all icons in the preview and in the future dialog box.

Speaking of the dialog box, let's create it!

createIconDialog() {
    // get the session. see getSession for more details.
    const sObject = this.getSession()

    // create the new dialog wrapper. the list of icons will be injected into .IconPicker-nFontAwesome-list later on.
    this.dialog = new Dialog({
        title: `Add Font Awesome Icon`,
        content: `
        <div class="IconPicker-nFontAwesome-wrapper">
            <div class="IconPicker-nFontAwesome-controls">
                    <div class="IconPicker-nFontAwesome-controls-row">
                        <div class="IconPicker-nFontAwesome-search">
                            <input type="text" placeholder="Search..."></input>
                        </div>
                        <div class="IconPicker-nFontAwesome-counter"><strong>0</strong> <span>icons</span></div>
                    </div>
                    <div class="IconPicker-nFontAwesome-controls-row">
                        <div class="IconPicker-nFontAwesome-styles">
                            ${this.SOLID ? `
                            <label>
                                <input type="checkbox" ${sObject.solid ? `checked="checked"` : ``} name="solid" /> Solid
                            </label>` : ``}
                            ${this.REGULAR ? `
                            <label>
                                <input type="checkbox" ${sObject.regular ? `checked="checked"` : ``} name="regular" /> Regular
                            </label>` : ``}
                            ${this.BRANDS ? `
                            <label>
                                <input type="checkbox" ${sObject.brands ? `checked="checked"` : ``} name="brands" /> Brands
                            </label>` : ``}
                        </div>
                    </div>
                </div>
                <div class="IconPicker-nFontAwesome-list" style="font-size:40px;"></div>
            </div>
        </div>
    `,
        style: `width:690px;height:500px;`
    })

    // setup an input event to the search box within the dialog
    const that = this
    const searchBox = query(this.dialog.domNode).query(`.IconPicker-nFontAwesome-search input`)[0]
    searchBox.addEventListener(`input`, function (e) { that.filterIcons(this, e) }, false)

    // ...and a change event on the checkboxes.
    const styleCheckboxes = query(this.dialog.domNode).query(`.IconPicker-nFontAwesome-styles input`)
    styleCheckboxes.forEach(el => {
        el.addEventListener(`change`, function (e) { that.changeStyle(this, e) }, false)
    })
},

I opted to have all the html for the dialog inside of a template literal, but you can easily have it as external files in dojo. It's the same with the first templateString as well. However, I like having all my widgets contained in few files for easy distribution between sites etc.

Not much going on in this code, except for our first call to getSession(). We will look at this method a bit later, but essentially we are building a cache for our icons in session storage. It'll become more clear as we go on.

At the end of this method we just add a couple of events; we have a search box and checkboxes for the various styles Font Awesome delivers.

showIconDialog() {
    // get our icons
    this.getIconList()
    // show the dialog
    this.dialog.show()
    // set the search value back to null, if any exists since we want to show all icons on every dialog open
    query(`.IconPicker-nFontAwesome-search`).query(`input`)[0].value = ``
},

This is our method to show the dialog we just created. It's really straight forward; get our icons loaded, show the dialog, and clear out any eventual leftover search text in the search box.

So, how do we load all the icons? This is where, if I may say so myself, the nifty part comes into play. We are always getting our list of icons straight from Font Awesome. This is due to the many ways the css might have been implemented into the frontend of the site. We can never know how it's added. It might be through a CDN, self hosted files etc.

This is why we had to specify the version of Font Awesome used in the frontend, and which styles are in use. A lot of times designers and developers only work with one or two styles to keep the size of the bundle down. And we don't want to show icons that wouldn't work in the project later on! The only assumption we do is that the user is using the free version of Font Awesome.

getIconList() {
    // get our session cache object
    const sObject = this.getSession()
    const that = this

    // if sObject.icons isn't null, we already have icons in the session cache so just return those instead of another call to the api
    if (sObject.icons) {
        this.generateListOfIcons()
        // short circuit the function call
        return true
    }

    // we didn't get short circuited, so no icons in session cache. let's make the api call!
    fetch(`https://api.fontawesome.com`, {
        method: `POST`,
        mode: `cors`,
        redirect: `follow`,
        headers: {
            'Content-Type': `application/json`,
            'Accept': `application/json`
        },
        body: JSON.stringify({
            // the graphql to get our list of icons. this will get all free icons for the version we've specified
            query: `query {
            release(version: "${that.fontAwesomeVersion}") {
                icons(license: "free") {
                    id
                    label
                    membership {
                        free
                    }
                }
            }
        }`
        })
    })
        .then(r => r.json())
        .then(data => {
            const icons = data.data.release.icons
            icons.sort((a, b) => a.id.localeCompare(b.id, 'en', { 'sensitivity': 'base' })) // sort icons alphabetically based on ID
            sObject.icons = icons // add it to our session object
            this.setSession(sObject) // set the new session object
            this.generateListOfIcons() // now generate the list of icons for the dialog
        })
},

Again, we check if we have any cached icons. If we do, we just continue to generateListOfIcons() without doing the fetch, with the short circuit of return true (line 7-10). If not, it's time to fetch our icons (line  14).

We build our graphql, and ask for all free icons for the specified version. I have yet to find an easy way to filter directly on style here, but it doesn't matter. We do our filtering of styles ourselves instead.

In the callback once the response has come in, we just sort the icons based on their ID, we add it to our session cache object sObject, store them in our session storage and go on to generating the list for our dialog:

generateListOfIcons(json) {
    const that = this
    const list = query(this.dialog.domNode).query(`.IconPicker-nFontAwesome-list`)[0] // get handle to list DOM element
    const counter = query(this.dialog.domNode).query(`.IconPicker-nFontAwesome-counter strong`)[0] // get handle to our counter DOM element
    if (list) {
        // first clear out any html we might have in the list element
        let html = ``
        list.innerHTML = html
        list.classList.add(`loading`) // add loading class

        // generate html that we can inject into the list element
        setTimeout(function () { // this timeout is just to put it at the end of the execution pipeline so it does show the loader etc.
            const sObject = json || that.getSession() // do we have a filtered list? if not, get it from the session cache object

            sObject.icons.forEach(icon => {
                icon.membership.free.forEach(style => {
                    // get the prefix for the type of icon it is (solid, regular, brand)
                    const prefix = that.getPrefix(style)
                    if (prefix && sObject[style]) {
                        html += `<div data-prefix="${prefix}" data-id="${icon.id}" title="${icon.label}"><span class="${prefix} fa-${icon.id}"></span></div>`
                    }
                })
            })

            list.innerHTML = html // inject it to our list element
            list.classList.remove(`loading`) // remove loading class
            const icons = list.querySelectorAll(`div`) // get handle to all icon dom elements
            counter.innerHTML = icons.length // show how many icons we have showing
            icons.forEach(icon => { // go through each icon and add a click event listener to select the icon
                icon.addEventListener(`click`, function (e) { that.selectIcon(this, e) }, false)
            })
        }, 10)
    }
},

We start with getting a handle to the DOM elements within the widget we want to operate on. Then we clear out any html that might already be in the list of icons, add a loading class and start our generation of elements.

We do this inside of a setTimeout with a short delay. This is a common workaround, to force the action to be put at the end of the pipeline the browser is executing - essentially giving it enough time to add the class we specified above before bogging it down with the more power hungry rendering of potentially thousands of new DOM elements. For each icon we make a get to getPrefix, more on that directly after this snippet.

One thing worth noting in the method, is that we can send in a json of icons. If we don't have anything sent in, we do all the work on the stored cache object. This comes into play when we do filtering and the search within the dialog box.

We check if we have enabled the prefix (solid, regular or brand) for each icon, and if it's enabled we create a new div element. When all icons have been added to our html string, we inject it to our list element inside the dialog.

After this we remove our loading class name, update the counter to show how many icons are in our dialog and set a click event on each and every one of them.

There we go, all the major implementations are out of the way. We still need some helper methods for various things, like filtering and searching the icons. We'll get to them right now.

getPrefix(style) {
    let prefix = null

    if (style === `solid`) {
        prefix = `fas`
    } else if (style === `regular`) {
        prefix = `far`
    } else if (style === `brands`) {
        prefix = `fab`
    }

    return prefix
},

The getPrefix method is just a small helper, to translate our full name of the various styles of icons into the corresponding class name Font Awesome uses to display the icons.

changeStyle(el) {
    // get the session cache object
    const sObject = this.getSession()
    // update sObject type based on if we checked or unchecked the checkbox
    sObject[el.name] = el.checked
    // store the object with the new setting
    this.setSession(sObject)
    // and regeneate the icon list
    this.generateListOfIcons(sObject)
},

changeStyle helps us store what styles we are showing. It literally only switches out a bool in our session cache object and saves it again - and then calls for a new generated list of icons.

selectIcon(that, e) {
    const faIcon = `${that.dataset.prefix} fa-${that.dataset.id}` // store our prefix and icon class name
    this.dialog.hide() // hide the dialog
    this.onFocus() // without this it won't trigger an update, since the base widget needs to be in focus for this to happen
    this._setValue(faIcon, true) // set the value of our property
},

The selectIcon method is used on click on an icon in the dialog. We store the style and ID of the icon as a class names string, hide the dialog and updates our value of the widget to said class names string.

Important to notice here is the this.onFocus() call (line 4). Without this EPi wouldn't recognize that a change has been made and automatically ask us if we want to publish the new change. This is because, from what I can understand from forum posts etc as I haven't found any actual documentation about it, that it should always be the base element setting the value. So we switch the focus from our dialog DOM elements to the DOM element in the initial templateString, and then sets the value. All of a sudden it all works and EPi asks if we want to publish our new shiny icon.

showSelectedIconInPreview(faIcon) {
    const preview = query(this.IconPicker).query(`span`)[0]
    preview.className = faIcon
    preview.title = faIcon
},

This method updates the span in our templateString to show our selected icon.

Image showing our selected icon

We did add a search input to our dialog. It's time to make it work!

filterIcons(that, e) {
    const query = that.value
    const sObject = this.getSession()
    const results = sObject.icons.filter(icon => { return icon.label.toLowerCase().indexOf(query.toLowerCase()) > -1 })
    this.generateListOfIcons({
        ...sObject,
        icons: results
    })
},

We get the value of the query string, and we filter out every label inside of our cached icon list based on this query. To make sure, we lowercase both so we don't have to worry about any case insensitivity.

This filtered list is then sent back to our generateListOfIcons method, which then uses the filtered list to re-generate all the icon HTML.

We have referred a lot to our session storage cache object. Why do we use it, and how does it work?

The answer to the first question is speed. It can take quite a bit of time to get the api response from Font Awesome. It's a lot of icon data to shuffle down to our widget. So we save it in the browsers Session Storage:

Showing the saved icons in sessionStorage
The api call has been made and the list of icons is saved in sessionStorage

That way we only make the actual call once per browser session - the first time the editor opens up the dialog box to choose an icon. In all other cases, it's served from our cache instead. And how does it work? It's really straight forward, as you will see:

getSession() {
    // get our object from session storage cache
    const sObject = sessionStorage.getItem(`nFontAwesome`)
    if (sObject) { // if we have an object, just return it
        return JSON.parse(sObject)
    } else { // otherwise...
        // create and return a base object, based on above settings and defaults
        // this is so we don't get any null ref errors.
        return {
            icons: null,
            solid: this.SOLID,
            regular: this.REGULAR,
            brands: this.BRANDS,
            fontsize: 30
        }
    }
},

First we'll get our item from session storage. If it doesn't exist, we will get a null value, otherwise a long string. Everything in the browsers sessionStorage (as well as localStorage for non-temporary storing of data) are saved as strings.

So if we get a string back, we parse this into JSON and returns back to whoever called the function.

If it's null, we create the base object, the skeleton, or schema if you will, for our object. This is so we won't get any problems with undefined errors in the script.

Do you remember the short circuit in getIconList above?

By sending back this base object, getIconList will see that icons is null, and fetch them instead of just returning the object. Nifty!

So we can get our icons from the browser session storage. How do we get the object in there in the first place? It's really as easy as:

setSession(object) {
    sessionStorage.setItem(`nFontAwesome`, JSON.stringify(object))
},

The only thing to note here is the stringify of our JSON object, since it needs to be a simple string to be stored.

Now for the more widget specific parts. These will highly be the same for all widgets you create in the future, except for what to do when the value of the property is set:

_setValue(value, updateTextbox) {
    // avoids running this if the widget already is started
    if (this._started && epi.areEqual(this.value, value)) {
        return
    }

    // set value to this widget (and notify observers).
    this._set(`value`, value)

    // set value to tmp value
    if (updateTextbox) {
        this.pickedIcon = value
        // updates our template with the currently selected icon
        this.showSelectedIconInPreview(this.pickedIcon)
    }

    if (this._started && this.validate()) {
        // Trigger change event
        this.onChange(value)
    }
},

_setValue is called internally in EPiServer as well during the initialization of widgets etc, which is why we check at the top of this if the value is the same as the one already set and return if it is. Otherwise the page would register a change directly, and every time the page loads you would get a new version to publish.

If it is a new value, we set this value with the internal _set method, and then we update our widget. This is where we call our showSelectedIconInPreview to show the change for the editor. Once all this is done, and it has gone through our validation etc, we trigger the change event.

The following are more generic widget methods, all called internally by EPiServer.

onChange(value) {
    // Event
},
// just a quick validator that our value is correct and set. in this case that it's not empty or null, pretty much.
isValid() {
    return !this.required || (lang.isArray(this.value) && this.value.length > 0 && this.value.join() !== ``)
},
// Sets the value of the widget to "value" and updates the property
_setValueAttr(value) {
    this._setValue(value, true)
},
// sets the property to ReadOnly if the PropertyEditorDescriptor has it marked as such.
_setReadOnlyAttr(value) {
    this._set(`readOnly`, value)
},
// indicates whether the onChange method is used for each value change or only on demand.
// set through the PropertyEditorDescriptor.
_setIntermediateChangesAttr(value) {
    this._set(`intermediateChanges`, value)
}

To be honest, I have no idea why the empty onChange method is needed. Again, no documentation have been found on it, but I see it in every code example etc and if not in there, EPiServer just refuses to pick up that a change has happened and will never ask if you want to publish a new version of the page.

isValid is our validator. In our case we could've skipped it outright and left empty as well since we know we are setting a value when clicking an icon, but I wanted you to see an example of how it works.

In this case we essentially just check if we have a value that isn't null or empty.

_setValueAttr is automatically called during startup, and sets our initial value on the property. This makes the currently selected icon show up on page load.

The last two, _setReadOnlyAttr and _setIntermediateChangesAttr sets the appropriate attributes on the widget, if specified through the PropertyEditorDescriptor. With this you can make the property read only etc.

Alright, that's it for the javascript part. Wasn't too bad I hope? As you can see, we really didn't have to mess around too much with dojo at all. The dialog functionality and a couple of queries and events, but for the most part it's all straight up vanilla js being used. It's only really when you need a handle to the current widget you need to fall back on using dojo libraries when doing more straight forward properties.

And the sky is the limit! Anything you can save as a simple string in the end doesn't need more work than this. It gets a bit more involved when you want your own custom type property, but... That's a story for another post. Instead, let's wrap this up with adding our css file. We want it look good as well!

Add some flair with css

I won't really document the css, since it is so straight forward. The only thing to mention is that I am following the Suit CSS naming convention, and when dealing with pure css in oppose to less or scss files like this I at least like to use indentation to symbolize when working with child elements.

Create a new file next to your widget.js, and name it - you guessed it - styles.css

Copy in this stylesheet, and you will be good to go.

.IconPicker {
    width: 70px;
    height: 70px;
    padding: 10px;
    border: 1px solid #dedede;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
}

    .IconPicker:hover {
        border: 1px solid #444;
    }

    .IconPicker span {
        display: block;
        font-size: 50px;
    }

/* Dialog Styling */
.IconPicker-nFontAwesome-wrapper {
    display: flex;
    flex-direction: column;
    height: 426px;
}

.IconPicker-nFontAwesome-controls {
    display: flex;
    flex-direction: column;
}

    .IconPicker-nFontAwesome-controls .IconPicker-nFontAwesome-search {
        flex-grow: 1;
    }

        .IconPicker-nFontAwesome-controls .IconPicker-nFontAwesome-search input {
            width: 95%;
            font-size: 18px;
            border: 1px solid;
            padding: 5px;
            border-radius: 5px;
            border-color: rgb(200,200,200);
        }

    .IconPicker-nFontAwesome-controls .IconPicker-nFontAwesome-counter {
        font-size: 16px;
        display: flex;
        align-items: center;
    }

        .IconPicker-nFontAwesome-controls .IconPicker-nFontAwesome-counter strong {
            font-size: 24px;
            margin-right: 5px;
        }

.IconPicker-nFontAwesome-controls-row {
    display: flex;
    flex-direction: row;
    margin: 0 0 20px;
}

.IconPicker-nFontAwesome-controls .IconPicker-nFontAwesome-styles {
    flex-grow: 1;
    display: flex;
    align-items: flex-start;
}

    .IconPicker-nFontAwesome-controls .IconPicker-nFontAwesome-styles label {
        margin-right: 15px;
        font-size: 16px;
    }

.IconPicker-nFontAwesome-controls .IconPicker-nFontAwesome-size {
    display: flex;
    justify-content: center;
    align-items: center;
}

.IconPicker-nFontAwesome-list {
    flex-grow: 1;
    overflow-y: auto;
    display: flex;
    flex-wrap: wrap;
    align-content: start;
}

    .IconPicker-nFontAwesome-list div {
        width: 10%;
        height: 18%;
        display: flex;
        font-size: inherit;
        align-items: center;
        justify-content: center;
        background-color: rgba(200,200,200,.0);
        transition: background-color ease-in-out 300ms;
    }

        .IconPicker-nFontAwesome-list div:hover {
            background-color: rgba(200,200,200,.3);
        }

        .IconPicker-nFontAwesome-list div span {
            font-size: inherit;
            transition: font-size linear 300ms;
        }

    .IconPicker-nFontAwesome-list.loading {
        background: no-repeat url('//cdnjs.cloudflare.com/ajax/libs/galleriffic/2.0.1/css/loader.gif') 50% 50%;
    }

So there we have it! You have created an Icon Picker for EPiServer!

"But", I hear you scream, "how do we render it on the website?"

Font Awesome uses regular tags to render. You can add the class names to pretty much any element, but I would suggest span. But as an added bonus, we will also create our own display template for it.

Display Template

By adding a display template for our property, we make it real easy to render it on our pages and blocks we create.

Create a new file where you have your Display Templates stored called IconPicker.cshtml and copy in the following:

@model String

@if (Model != null)
{
    <span class="@Model @ViewData["class"]"></span>
}

That's it! It will render a span, and add whatever classes come from our Model, which in this case becomes our icon string. We can also force more classes on there if we want to with the Viewdata.

In order to render our newly picked icon, all we need to do is use the usual:

@Html.PropertyFor(x => item.Icon)

Since it is a font icon, it will get the size of the surrounding text. If you want to change the size of it, simply change the font-size css property on either the parent or the span itself.

If you prefer to not use the Display Template, you can simply print out the value of the property on any element like so: <span class="@Model.Icon"></span> but note that you will loose On-page Edit functionality when doing it this way.

And there we have it! A custom Icon Picker for Font Awesome, that is super easy to implement and use on any number of pages or blocks in EPiServer.

Mastodon