Adding UIkit to Pinegrow: Part 7
Namaste!!
Welcome to the last (I think) part of our tutorial series on integrating the UIkit framework into Pinegrow. In previous sections, we have covered how to initialize a new plugin for Pinegrow, ran through the steps for adding a new HTML component, as well as covering how to give the end-user choices for adding classes and other modifiers to those components. We also covered how to create a partially custom selector for adding in icons and how to utilize the ‘toggle_buttons
‘ helper for customizing control buttons.
In this section, we will cover adding options that can be used on multiple HTML elements, as well as introducing a final helper, ‘PgCustomPropertyControl
‘. This helper allows us to create complex custom property controls, like the ones controlling the columns in the Bootstrap framework implementation.
Let’s get started! Within the UIkit framework, there are a number of utility classes and attributes that can be applied to a number of elements. One example is the ‘Visibility’ component. The ‘hidden
‘ attribute can be added to any element to both prevent it from being shown and remove it from the document flow. Basically, we want to be able to add this attribute to any element on the page except for the the main structure tags – ‘<html>’, ‘<head>’, ‘<body>’, or any ‘<script>’ tags.
The structure of controls that apply to multiple elements is just like those of the individual HTML elements. It starts by instantiating a new ‘PgComponentType
‘ object.
var uikit_def_all = new PgComponentType('uikit_all', 'All elements',{});
framework.addComponentType(uikit_def_all);
Unlike the ‘HTML’ objects, you don’t need to add a instantiate an object section within the component list using ‘PgFrameworkLibSection
‘, since you will only be displaying items within the properties panel. Simply passing your new object using 'addComponentType
‘ is sufficient.
Like using ‘PgComponentType
‘ for ‘HTML’ entities, the first two arguments passed in are a unique name – in this case, ‘uikit_all
‘, and a display name – ‘All elements
‘. This display name will be applied to the tree view. In this image the display name has been added to both the ‘<p>
‘ and ‘<span>
‘ elements.
The third argument that we will pass in is, of course, an object containing all of the property field actions and selectors.
var uikit_def_all = new PgComponentType('uikit_all', 'All elements',{
selector: function(pgel) {
if ([ 'html', 'body', 'head', 'script' ].includes(pgel.tagName)) {
return false;
} else {
return true;
}
},
});
framework.addComponentType(uikit_def_all);
The first key of the JSON object that we are passing is ‘selector
‘ and the value that we are passing in is a simple, anonymous function with ‘pgel
‘ as the only argument. Remember, ‘pgel
‘ passes the current element to our function. In this case, this function is fired when the page loads in and will screen every single element of the page. In practice, with a really large page this might have a noticable speed impact on initial load, but I haven’t really tested.
So, what does the function do? It sets up a conditional examining the element tag using ‘pgel.tagName
‘. If that is equal to a tag that we don’t want to modify with our controls it returns ‘false
‘, otherwise it will return ‘true
‘ and add the controls. This conditional can be modified, for example, to only return true if the tag is a ‘<div>
‘.
The next two key:value pairs are additional housekeeping for the properties.
display_name: 'tag',
priority: 2001,
The next key:value pair is “display_name: 'tag'
“. This signals to Pinegrow to keep the tag name of the element in the tree view. Otherwise, the tag name would be replaced with the display name. Next is the key, 'priority'
. This key takes a number that determines the order of display of the options within the properties panel. This number takes some fiddling to determine if your options will take priority over other properties that are intrinsic to the element. For example, Pinegrow adds ‘Name’, ‘Value’, and ‘Type’ options for ‘<button>
‘ tags. Using different priorities you can rank your properties before or after these. The 'priority'
tag also seems to determine the name the element gets if more than one selector matches.
The remainder of the key:value pairs consist of a section for each set of controls and a series of fields that name and define actions for each property.
sections: {
uikit_display_options: {
name: 'Display Options',
fields: {
ui_hidden: {
type: 'checkbox',
name: 'Make hidden?',
action: 'element_attribute',
attribute: 'hidden',
empty_attribute: true,
value: '1'
},
ui_invisible: {
type: 'checkbox',
name: 'Make invisible?',
action: 'apply_class',
value: 'uk-invisible'
}
},
},
In the above code example, there are two fields added – one to add the ‘hidden
‘ attribute, and one to add the ‘.uk-invisible
‘ class. Any additional sections or fields that should be displayed on all elements could be added into this same ‘PgComponentType
‘ object.
Shifting gears, let’s dive into custom controls. There are some cases where multiple classes/attributes of the same type can be added to an element. A good example of this for the UIkit framework is responsive widths. A single element could take a class of ‘.uk-width-1-1@s
‘ giving it full container width on a small or medium screen and ‘.uk-width-1-3@l
‘ narrowing it to one-third of the container on large or extra-large screens. We could simply have a series of select dropdowns to accomplish this, but having a compact set of dropdowns would make for a better user experience.
As I outlined at the beginning of the tutorial, we will be using another helper function that instantiates a new object called ‘PgCustomPropertyControl
‘. It can take more than a dozen key:value pairs and those keys wrap several functions that can be called. We are going to cover three of those keys and two functions. While the example we will be building is simple, I’m still going to build it with an eye toward making it somewhat reusable for other controls. To start, I’m going to create two arrays:
var uikit_responsive_sizes = [ 's', 'm', 'l', 'xl' ];
var uikit_responsive_widths = ['1-1','1-2','1-3','2-3','1-4','3-4','1-5','2-5','3-5','4-5','1-6','5-6','auto','expand'];
Stating the obvious, ’cause that is what I’m good at doing – The first array, ‘uikit_responsive_sizes
‘, lists the four size modifiers in the UIkit framework. The second array, ‘uikit_responsive_widths
‘, contains each of the potential widths. With that out of the way I’ll give you a big code dump (eww, that sounded wrong) that we can then pick through line-by-line.
var widthControl = function(control_id, settings) {
return function() {
var control = new PgCustomPropertyControl('uikit_' + control_id);
control.onDefine = function() {
for (var responsive_sizes = 0; responsive_sizes < uikit_responsive_sizes.length; responsive_sizes++) {
var field = 'uikit_' + settings[0].field_prefix + '_' + uikit_responsive_sizes[responsive_sizes];
this.registerInputField(field, createFieldDef(settings[0], uikit_responsive_sizes[responsive_sizes]));
}
};
control.onShow = function() {
var $table = $('<table/>', { class: 'grid-control columns-control five-col-grid' });
var html = '<td></td>';
uikit_responsive_sizes.forEach(function(size) {
html += '<td><label class="grid-control-size">' + size + '</label></td>';
});
var $row = $('<tr/>').html(html).appendTo($table);
$row = $('<tr/>', { class: '' }).appendTo($table);
var $td = $('<td/>').html('<label>' + settings[0].name + '</label>').appendTo($row);
for (var responsive_sizes = 0; responsive_sizes < uikit_responsive_sizes.length; responsive_sizes++) {
$td = $('<td/>').appendTo($row);
var field = 'uikit_' + settings[0].field_prefix + '_' + uikit_responsive_sizes[responsive_sizes];
this.showInputField($td, field, createFieldDef(settings[0], uikit_responsive_sizes[responsive_sizes]));
}
return $table;
};
control.onSetValues = function() {};
var createFieldDef = function(settings, width_name) {
var span_select = {
type: 'select',
name: null,
action: 'apply_class',
draggable_list: true,
show_empty: true,
options: []
}
settings.values.forEach(function(value) {
span_select.options.push({key: settings.field_prefix + '-' + value + '@' + width_name, name: value})
})
return span_select;
};
return control;
};
};
WOW, a bit intimidating! Starting with the first line:
var widthControl = function(control_id, settings) {
Simple, we are declaring a variable and setting it equal to a function that takes two arguments. The first argument, ‘control_id
‘, will let us pass in a semi-unique identifier, like ‘responsive-widths
‘ or ‘child-responsive-widths
‘, so that we can reuse this bit of code. The second argument, ‘settings
‘, will be used to pass in an object containing all of our property and field-specific settings. This will hopefully become clearer as we dig into the rest of the code.
return function() {
var control = new PgCustomPropertyControl('uikit_' + control_id);
In looking through the Pinegrow source code and the code for other frameworks, I’ve noticed that the code for this type of object is always wrapped in an anonymous function that is returned. In several places this is notated to say that it will keep the object helper functions and variables private. I’m not sure if this is a problem or not, but… on the next line (and if you follow the code above to the end) I wrap all the subsequent code in an anonymous function.
On the next line of code we finally instantiate our new object with a name of ‘control
‘. Much like our framework, there are several ways to pass data to this object. In this case, we are passing in the bare minimum to create the new object and will pass in additional properties using dot notation. In this case we are passing in a unique id for the object using the semi-unique ‘control_id
‘ we passed in and prefixing it with ‘uikit_
‘ to ensure no other framework has the same control.
control.onDefine = function() {
for (var responsive_sizes = 0; responsive_sizes < uikit_responsive_sizes.length; responsive_sizes++) {
var field = 'uikit_' + settings[0].field_prefix + '_' + uikit_responsive_sizes[responsive_sizes];
this.registerInputField(field, createFieldDef(settings[0], uikit_responsive_sizes[responsive_sizes]));
}
};
The first new property we will pass in is ‘onDefine
‘. Basically, ‘onDefine
‘ is used to reserve a set of fields with unique ids for us to populate. In this case we are passing it a function that loops through each of the responsive sizes defined in out first array. Note: this isn’t really a great way to do this. I’m hardcoding in the array to be used rather than passing it into the original control. This means that this code can only be used to set up width controls. However, I thought this would be better for the learning experience.
Inside the ‘for
‘ loop we first set the ‘field
‘ variable equal to a unique string that is prefixed with ‘uikit_‘, followed by the value of the ‘field_prefix
‘ key from the ‘settings[0]
‘ object we passed in to the original function, followed by the responsive size. We will return to the ‘settings
‘ object later in the tutorial, but this should generate a final string that looks something like ‘uikit_uk_width_m
‘.
The resulting string is then passed to one of the ‘onDefine
‘ internal helper functions, ‘registerInputField
‘, along with the defined ‘field
‘ variable, and a function that we will walk through further on in the tutorial called ‘createFieldDef
‘.
control.onShow = function() {
var $table = $('<table/>', { class: 'grid-control columns-control five-col-grid' });
var html = '<td></td>';
uikit_responsive_sizes.forEach(function(size) {
html += '<td><label class="grid-control-size">' + size + '</label></td>';
});
var $row = $('<tr/>').html(html).appendTo($table);
$row = $('<tr/>', { class: '' }).appendTo($table);
var $td = $('<td/>').html('<label>' + settings[0].name + '</label>').appendTo($row);
for (var responsive_sizes = 0; responsive_sizes < uikit_responsive_sizes.length; responsive_sizes++) {
$td = $('<td/>').appendTo($row);
var field = 'uikit_' + settings[0].field_prefix + '_' + uikit_responsive_sizes[responsive_sizes];
this.showInputField($td, field, createFieldDef(settings[0], uikit_responsive_sizes[responsive_sizes]));
}
return $table;
};
The next property we will pass in is ‘onShow
‘. This is the key that actually generates the display HTML and populates the fields we reserved with ‘onDefine
‘. The first two lines generate the table that will hold our elements and a blank data cell. This will act as a space holder for our row lable.
Next, we use a ‘forEach
‘ loop to cycle through each of the responsive sizes and add a lable for each. Those labels are then wrapped in a table row and then added to the table.
After creating an new row, we pluck the name value from our passed in settings – again, we will revisit the settings object next in the tutorial – and create a table data cell with that value. Using a ‘for
‘ loop we then create each of our controls. First, we create a new data cell and append it to the row. We then create the same identifier string that we made for the ‘onDefine’. Finally, we pass the target cell, ‘$td
‘ and the ‘field
‘ variable, along with our ‘createFieldDef
‘ to another intrinsic function, ‘showInputField
‘. This will then populate our newly created ‘<td>
‘. Finally, we return our newly created table.
Almost there!! Go grab an energy drink to fuel up – I’ll wait!
control.onSetValues = function() {};
The ‘onSetValues
‘ can be used for controls that are completely non-standard. In our case, we are using the built-in ‘select
‘ dropdown and ‘apply_class
‘ action, so we don’t need to provide any special code to direct Pinegrow how to handle our input. In fact, we could actually completely leave this line of code out and still have a fully functional control.
The final piece of code within our new ‘widthControl
‘ function is the ‘createFieldDef
‘ function that we called from both the ‘onDefine
‘ and ‘onShow
‘ functions. This function provides properties exactly like those in the normal HTML control element property controls.
var createFieldDef = function(settings, width_name) {
var span_select = {
type: 'select',
name: null,
action: 'apply_class',
draggable_list: true,
show_empty: true,
options: []
}
settings.values.forEach(function(value) {
span_select.options.push({key: settings.field_prefix + '-' + value + '@' + width_name, name: value})
})
return span_select;
};
Our ‘createFieldDef
‘ function takes two arguments – ‘settings
‘ and ‘width_name
‘. Looking back at ‘onDefine
‘ we passed in ‘settings[0]
‘ for ‘settings
‘ and one of the sizes from our responsive sizes array for ‘width_name
‘. We will come back to what the ‘settings[0]
‘ refers to in the next part of our tutorial.
Next up, the ‘span_select
‘ var contains an object that should look familiar. In that object we include a ‘type
‘ key with a value of ‘select
‘. As we remember from past tutorials (right?!?!?), this will create a dropdown populated with ‘key:name
‘ pairs from an array passed in through the ‘options
‘ key. We are also passing an ‘action
‘ key with an ‘apply_class
‘ value, plus passing a ‘show_empty: 'true'
‘ so that we can remove that particular class.
I can hear you yell, ‘So Bo, the options are an empty array, where are the values going to come from?!?‘. Glad you asked! Next up is a ‘forEach
‘ loop that refers again to the pesky ‘settings
‘ that got passed in as an argument to our function! Without further ado, let’s introduce our ‘settings
‘ object and then double back to look at this section of code again.
uikit_element_responsive_widths: {
type: 'custom',
name: 'Element Responsive Width',
action: 'none',
control: widthControl('resp-element-width', [
{
field_prefix: 'uk_width',
class_prefix: 'uk-width',
values: uikit_responsive_widths,
name: 'Elem. Resp. Sizes'
}
])
},
Once again, this should look fairly familiar. This is a section from a ‘PgComponentType
‘ object that would create a propert control. In this case the ‘type
‘ key receives a value of ‘custom
‘. While I’ve added a ‘name
‘ key, the value isn’t actually used. For an ‘action
‘ we add ‘none
‘ because the action will be added at the control level. For the ‘control
‘ key, we pass in our newly created ‘widthControl
‘ function along with two arguments. First, is a semi-unique control name – ‘resp-element-width
‘. Second, we pass an array of objects. In this case, we only have a single object, but if you remember back to the Bootstrap example at the very beginning of this tutorial section, there were actually three objects passed in. One for ‘Sizes’, one for ‘Order’, and a last for ‘Offset’. As written, our current function only deals with the first object passed in – ‘Way to give a crappy example, Bo!‘
Jumping back to the ‘onDefine
‘:
for (var responsive_sizes = 0; responsive_sizes < uikit_responsive_sizes.length; responsive_sizes++) {
var field = 'uikit_' + settings[0].field_prefix + '_' + uikit_responsive_sizes[responsive_sizes];
this.registerInputField(field, createFieldDef(settings[0], uikit_responsive_sizes[responsive_sizes]));
}
When creating the ‘field
‘ string we grab the ‘field_prefix
‘ value from the first object in the ‘settings
‘ array using dot notation. In this case, that value is ‘uk_width
‘ . Again, this isn’t the greatest example, but I hope you can see that if we incorporated some additional loops, we could create a whole series of controls by passing in multiple objects within the ‘settings’ array.
For the ‘createFieldDef’ call we pass in the first object from settings array.
var createFieldDef = function(settings, width_name) {
var span_select = {
type: 'select',
name: null,
action: 'apply_class',
draggable_list: true,
show_empty: true,
options: []
}
settings.values.forEach(function(value) {
span_select.options.push({key: settings.class_prefix + '-' + value + '@' + width_name, name: value})
})
return span_select;
};
We then access the value of the passed in ‘settings
‘ option in a ‘forEach
‘ loop using dot notation. For each of the ‘key
‘ names we add the ‘class_prefix
‘, in this case ‘uk-width
‘, a dash ‘-
‘, followed by the ‘value
‘ from the loop followed by an ‘@
‘ symbol, and finally the width we are working on. So for example, ‘uk-width-3-4@m
‘. For the name we just pass in the ‘value
‘ – in this example, ‘3-4
‘. This string is then pushed into the options of our ‘span_select
‘ variable. I know what you are saying now, ‘Wow, completely clear, thanks Bo! Wait… wait… where do the values for ‘settings.value’ come from!‘
You got me – let’s drop back to the call to our function:
uikit_element_responsive_widths: {
type: 'custom',
name: 'Element Responsive Width',
action: 'none',
control: widthControl('resp-element-width', [
{
field_prefix: 'uk-width',
class_prefix: 'uk-width',
values: uikit_responsive_widths,
name: 'Elem. Resp. Sizes'
}
])
}
When we look at the ‘values
‘ key passed to the ‘widthControl
‘ function we see an old friend, ‘uikit_responsive_widths
‘! That is an array that we defined near the beginning of the tutorial. Ahhh, full circle. So, what does our new control look like?
And… if you select one of the sizes it actually works!
Okay, so we have covered how to add simple property controls to multiple elements, as well as adding a sparkly custom control element. These custom elements can get very fancy – look at some of the Bootstrap controls. One control that isn’t standard within the Pinegrow framework is a multiple choice/radio button type selector. However, using custom elements this can be somewhat easily built.
While these tutorials haven’t covered every aspect of building a Pinegrow plugin, I feel like they covered the basics fairly well. Once you understand the basics, it is a lot easier to comb through the Pinegrow code to find new ways of altering it using custom code.
As always, if there are any questions, criticisms, general comments, please send me an email or comment on this post. I’ll try to respond to everyone.
Metta!!!
Update: I have now finished a full-length version of the UIkit plugin for Pinegrow. Purchase of this plugin not only adds the UIkit framework to Pinegrow for unlimited sites, but it also gives you access to the completed code for learning purposes! As a bonus, I’ll give you access to a Facebook page to discuss coding for Pinegrow. Click here to learn more!!