Knockout Bound Fontawesome Iconpicker

So it took a while to get this to work the way I wanted, but I thought I would share it here for others. I found a great library that embeds a searchable icon picker for the fontawesome library. Initially I was having a real hard time to get it to work with knockout properly and ended up writing a custom binding handler to finally resolve the weird quirks I was experiencing. If anybody sees any improvements that could be made please speak up. I wanted to get it to work with a simple icon button but for some reason that was still finicky and inconsistent.

First in my plugin's settings template I added the input object to bind to with the new custom binding. In my case the observable for each row was icon.

<div class="input-prepend input-block-level"><span class="add-on" style="width: 25px;"><i class="fa-lg" data-bind="css: icon"></i></span><input type="text" data-bind="iconpicker: icon,iconpickerOptions: {hideOnSelect: true, collision: true}" class="iconpicker input-block-level"/></div>

image

Then I updated the __init__.py file to include the custom binding, the fontawesome icon picker css, and the fontawesom icon picker js file. Of course you'll also need to include the fontawesom css.

	##-- AssetPlugin mixin
	def get_assets(self):
		return dict(js=["js/taborder.js","js/spectrum.js","js/fontawesome-iconpicker.js","js/ko.iconpicker.js"],
					css=["css/taborder.css","css/font-awesome.min.css","css/spectrum.css","css/font-awesome-v4-shims.min.css","css/fontawesome-iconpicker.css"])

All wrapped together and then you get a nice searchable pop-up for selecting fontawesome icons.

It's a pretty solid library and you can find it on github over here for additional options that you can include in the iconpickerOptions data binding in your jinja template.

2 Likes

Hi @jneilliii,
thanks a lot for sharing, because it is really hard to find javascript libraries that we can use in OctoPrint.
The currently used libs were really old and the most libs use newer versions of bootstrap and jquery.
I will put this into my wiki "KnowledgeBase".

Is updating the UI libs on the roadmap?

Currently I am playing around with ko.components. Then I have the opportunity to create easy reusable components:
In your case it could looks like this:

self.pickerModel =  this.createIconPicker("greatpicker");

ComponentFactory.js
--------------------
this.createIconPicker = function(name){

        componentName = "iconpicker-" + name;

        var componentViewModel = {
            icon-options: ko.observable(),
            icon-selected: ko.observable()
        }

        componentTemplate = "<span...HTML-Template... data-bind='value: icon-...>'";

        ko.components.register(componentName, {
            viewModel: { instance: componentViewModel },
            template: componentTemplate
        });
        return componentViewModel;
}

MyTabs.jinja2
-------------
<iconpicker-greatpicker></iconpicker-greatpicker>

I think @foosel has been hesitant to do so because of the plugin space that has developed dependant on the older versions, and the amount of work it's going to take to translate the current core. With the Python 3 compatibility looming and the additional work on the comm layer she's been working on has taken up most of her time anyway.

I know there has been discussion on bootstrap 4/vue.js in the issue tracker, and it seems with the recent OctoVue it seems that it would be very possible. I mentioned on that post maybe converting the implementation to UIPlugin mixin as discussed in the issue linked above, and I may fork it to do that just to play with the UIPlugin mixin.

Nice! I would love to see that fork with the UIPlugin mixin.

Hi @jneilliii,

I was starting documenting the font-picker lib and for that reason I wrote a small demo.
The implementation is straight forward, works like a charm:

IconPicker
<div class="input-prepend input-block-level">
  <span class="add-on" style="width: 25px;"><i class="fa-lg" data-bind="css: mySelectedIcon"></i></span>
  <input id="myIconPicker" type="text" class="input-large text-right" data-bind="value: mySelectedIcon"/>
 </div>

// simple observer
self.mySelectedIcon = ko.observable("fab fa-500px");  // initial icon

// create picker-instance
myIconPicker = $('#myIconPicker').iconpicker();

// update observer during change
myIconPicker.on('iconpickerSelected', function(event){
  newIcon = event.iconpickerValue;
  self.mySelectedIcon(newIcon);
});

So, it is not clear to my, why do you implement a custom-handler? You are writing about "weird quirks", do you have more details for me?

The issues I was seeing was on save and sometimes the popup not showing at all. Maybe it was because I was using it within an observable array, but changing the setting using the picker when I saved the value within that row reverted and would not change.

Hi @jneilliii,
currently I didn't documented the usage of observable array in my KnowledgeBase...so it was a good challenge to look into my other plugins how I realized such a loop and implement an example.
Based on the font-picker my solution takes the following steps:

  1. Create Item-Model with update-function for a row
  2. Create observable array and add some items to the array
  3. Attach jquery-fontpicker functionality to all rows
  4. Implement sync between jquery-fontpicker and observable items
        // Single Item-Model
        IconItem = function(data) {
            this.iconName = ko.observable()
            // Fill Item with (initial) data
            this.update(data);
        }
        // Update the Item-Function
        IconItem.prototype.update = function (data) {
            var updateData = data || {}

            this.iconName = ko.observable(updateData.iconName)
        }

        // collects all IconItems
        self.allIcons = ko.observableArray();

        // fill icon-collection with dummy values (or a loop or a complete data-array)
        self.allIcons.push(new IconItem({
            iconName: "fab fa-500px"
        }));
        self.allIcons.push(new IconItem({
            iconName: "fas fa-ad"
        }));

        //        
        self.onAfterBinding = function() {
            // all inits / loop-rendering were done

            // init jquery-iconpicker
            allIconPickers = $('.supericonpicker').iconpicker();
            // attach selection listener for all iconPickers
            allIconPickers.each(function(index, item){
                $(item).on('iconpickerSelected', function(event){
                  newIcon = event.iconpickerValue;
                  // get IconItem-Model and fill the selection
                  iconItemModel = self.allIcons()[index];
                  iconItemModel.iconName(newIcon);
                });
            });
        }

Multi-IconPicker
<div data-bind="foreach: allIcons">
  <div class="input-prepend input-block-level">
    <span class="add-on" style="width: 25px;"><i class="fa-lg" data-bind="css: iconName"></i></span>
    <input type="text" class="supericonpicker input-large text-right" data-bind="textInput: iconName"/>
  </div>
</div>

The example does not include the saveAll-feature, because it is just converting the array to json and send the data to the server, like this:

ko.toJSON(self.allIcons()); 
--> "[{"iconName":"fab fa-500px"},{"iconName":"fas fa-ad"}]"

@OllisGit I know it's been a while since you posted your example above but I think I may need to transition to this approach over the custom binding. Did you experiment with this with a large set of icons/array of items referencing it?

I think my approach has introduced a memory leak based on this issue and some extensive testing but wanted to see if you were seeing the same thing or not before I went down that road of re-programming the whole thing versus removing it completely.

Unfortunately, I have no experience with the picker. I don't use it in my plugins.
It was just a proof-of-concept for me...and updating my UI-KnowledgeBase

But I updated my sandbox-plugin, so that you a have a running implementation of the above code-snippet.
If you see boxes instead of icons, just activate your tab-order plugin. reason: I don't pushed the fonts/folder to git.
image