To demonstrate writing a plug-in for SPEasyForms, I’m going to write a pretty simple adapter that can be applied to user fields and adds the functionality to default the field value to the currently logged on user on new forms. It’s not just simple enough to provide a good sample for explaining creating plug-ins, it’s also something that customers ask me for pretty frequently. This post is going to explain the JavaScript behind the plug-in. The source code download will be a full-fledged no code sandbox solution, but if you need an explanation of the solution or packaging see my previous post Anatomy of a No Code Sandbox Solution; I’m not going to explain that again here.
The basic skeleton for the JavaScript plug-in looks like:
(function ($, undefined) { // return without doing anything if there is already a DefaultToCurrentUser adapter if (!$ || !$.spEasyForms || "DefaultToCurrentUser" in $.spEasyForms.adapterCollection.adapterImplementations) return; // shorthand alias for SPEasyForms instances we're going to need var containerCollection = $.spEasyForms.containerCollection; var visibilityRuleCollection = $.spEasyForms.visibilityRuleCollection; var adapterImplementations = $.spEasyForms.adapterCollection.adapterImplementations; /* Field control adapter for default to current user on user fields */ $.spEasyForms.defaultToCurrentUserAdapter = { type: "DefaultToCurrentUser", // return an array of field types to which this adapter can be applied supportedTypes: function () { return ["SPFieldUser", "SPFieldUserMulti"]; }, // modify a configured field in a new, edit, or display form transform: function (options) { }, // initialize dialog box for configuring adapter on the settings page toEditor: function (options) { }, // launch the adapter dialog box to configure a field launchDialog: function (options) { } }; // add adapter to adapter collection adapterImplementations[defaultToCurrentUserAdapter.type] = $.spEasyForms.defaultToCurrentUserAdapter; })(typeof (spefjQuery) === 'undefined' ? null : spefjQuery);
Of note:
- The meat of the plug-in is an object instance declared as an object literal and attached to the $.spEasyForms namespace. It has one required property and four required methods. I use straight-up duck typing. I’m not a big fan of lengthy attempts to make JavaScript look like a full blown object-oriented language, so if you don’t define something that is required I’m probably just going to blow up. The debugger will tell you what you’re missing quickly enough. And just don’t do that.
- Before I declare my instance, I check to see if SPEasyForms is undefined or if it already has a DefaultToCurrentUserAdapter. If either are true, I return immediately. The second check is pretty important, because it will allow me to later roll this adapter back into the main SPEasyForms project, and the new DefaultToCurrentUser will get used even if you forget to uninstall the AddOns solution.
- The type property is set to DefaultToCurrentUser. The type is stored with the configuration for an adapter, and is what tells SPEasyForms which adapter implementation to pass a given configuration node to for processing. It is also displayed in the drop down where the user selects which adapter to add to a field, if there is more than one adapter implementation available to add to the given field type.
- The supportedTypes method returns an array of SPFieldTypes by name to which this adapter can be applied.
- The other methods in the instance are more involved and will be fleshed out and explained below.
- After the instance has been defined, I add it to the adapterImplementations keyed by type. This is how the adapter collection finds my adapter instance when it encounters a configuration node with my type.
- I pass spefjQuery into the namespace. This is the instance of jQuery that SPEasyForms has been attached to.
Now we just need to implement the three remaining members of our instance, toEditor, launchDialog, and transform. The first two methods are what allows our plug-in to interact with the SPEasyForms settings page. The transform method is the method that gets called on the forms themselves. It is responsible for altering the appearance and/or behavior of the control and will be implemented last. The toEditor method gets invoked when the settings page is first loaded, and again whenever the page is redrawn because the configuration has changed, and looks like:
toEditor: function (options) { var opt = $.extend({}, $.spEasyForms.defaults, options); // add the dialog div to the UI if it is not already there if ($("#addDefaultToCurrentUserDialog").length === 0) { var txt = "<div id='addDefaultToCurrentUserDialog' " + "class='speasyforms-dialogdiv' " + "title='Default to Current User Adapter'>" + "Would you like to add/remove a Default to Current User adapter to " + "'<span id='defaultToCurrentFieldName'></span>'?</div>"; $("#spEasyFormsContainerDialogs").append(txt); } // initialize the jQuery UI dialog var defaultToCurrentOpts = { modal: true, buttons: { "Add": function () { return this.add(opts); }, "Remove": function () { return this.remove(opts); } }, autoOpen: false, width: 400 }; $('#addDefaultToCurrentUserDialog').dialog(defaultToCurrentOpts); },
First, this method adds a div to the div that contains all of the SPEasyForms dialogs (it could really add it anywhere in the page, but why?). Then it initializes the dialog with jQuery-UI to have 2 buttons, Add and Remove, whose handlers call out to two methods, add() and remove(), so we now also need to define those methods in our instance. The add method looks like this:
add: function (options) { var opt = $.extend({}, $.spEasyForms.defaults, options); // add an adapter to the adapters list and redraw the editor if ($("#defaultToCurrentFieldName").text().length > 0) { // construct a new adapter var result = { type: defaultToCurrentUserAdapter.type, columnNameInternal: $("#defaultToCurrentFieldName").text() }; // add it to the adapters passed into us, keyed by column internal name opt.adapters[result.columnNameInternal] = result; // set the config and redraw the form to reflect the changes $.spEasyForms.configManager.set(opt); containerCollection.toEditor(opt); } $('#addDefaultToCurrentUserDialog').dialog("close"); return false; }
It creates and object instance with two properties, type and columnNameInternal. These are the only required properties for an adapter configuration node, the type indicates which adapter implementation handles this kind of node, and the columnNameInternal indicates to which field this configuration node applies. You can add any other additional configuration properties to this instance that you need in order to your work, for instance the cascadingLookupAdapter adds properties to tell it about the relationship list and parent and child columns. Of course, if you do have additional properties you want to capture, you’ll need to add something to your dialog box to capture those properties. In this case, I don’t need any additional properties because all my adapter does is set the current user on new forms.
Anyway, after creating our new configuration node, we add it to the SPEasyForms adapters instance keyed by column internal name. We then set the configuration manager to indicate the configuration has changed and call containerCollection.toEditor to redraw the settings page to reflect the configuration change.
The remove method looks like this:
remove: function (options) { var opt = $.extend({}, $.spEasyForms.defaults, options); // remove the adapter from the adaptes list if ($("#defaultToCurrentFieldName").text().length > 0 && $("#defaultToCurrentFieldName").text() in opt.adapters) { delete opt.adapters[$("#defaultToCurrentFieldName").text()]; // set the config and redraw the form to reflect the changes $.spEasyForms.configManager.set(opt); containerCollection.toEditor(opt); } $('#addDefaultToCurrentUserDialog').dialog("close"); return false; }
This method just removes the configuration node from the adapters list, sets the configuration manager to indicate the configuration has changed and calls containerCollection.toEditor to redraw the settings page to reflect the configuration change.
Now we just need to implement the launchDialog method and we’re finished with the code to make the plug-in interact with the SPEasyForms settings page. Coincidentally, this method just launches the dialog we initialized in the toEditor method. First it sets the text of the span with ID defaultToCurrentFieldName to the internal name of the field we’re configuring, which is passed into it as opt.fieldName. This is used by the other methods to determine which field we’re being added/removed to/from.
launchDialog: function (options) { var opt = $.extend({}, $.spEasyForms.defaults, options); // initialize the field name in the dialog $("#defaultToCurrentFieldName").text(opt.fieldName); // launch the dialog $('#addDefaultToCurrentUserDialog').dialog("open"); }
We’ve now implemented enough of our solution that we can install it and see it do something. If we’ve done it correctly, we should be able to go to the SPEasyForms settings page, click on the shuffle icon next to a user field, and see our dialog:
click the Add button and we’ll see our adapter in the adapter list:
And if we now save our configuration, SPEasyForms will call our transform method for the SalesRep field every time a new, edit, or display form is loaded for the list we just configured. Therein lies the rub; our transform method doesn’t actually do anything yet, so lets take a look at that. When I first started working on this adapter, I was hoping the transform method would be something as simple as:
var currentUser = $.spEasyForms.sharePointContext.getUserInformation(opt).name; var displayName = containerCollection.rows[opt.adapter.columnNameInternal].displayName; $().SPServices.SPFindPeoplePicker({ peoplePickerDisplayName: displayName, valueToSet: currentUser, checkNames: true });
SPServices has a handy method that can get or set the value of a people picker field. Wouldn’t it be nice if it were that easy? Unfortunately, Office 365 and SharePoint 2013 use the new client people picker, and SPServices doesn’t work with that at all. So first I have to check if a client people picker is being used, and if so, use that to set the value to the current user. If not, it uses the SPServices method to set the field. That looks like:
transform: function (options) { var opt = $.extend({}, $.spEasyForms.defaults, options); if (visibilityRuleCollection.getFormType(opt) !== "new") { return; } if (containerCollection.rows[opt.adapter.columnNameInternal]) { var pplpkrDiv = $("[id^='" + opt.adapter.columnNameInternal + "'][id$='ClientPeoplePicker']"); var currentUser = $.spEasyForms.sharePointContext.getUserInformation(opt).name; if (pplpkrDiv.length > 0) { ExecuteOrDelayUntilScriptLoaded(function () { var clientPplPicker = SPClientPeoplePicker.SPClientPeoplePickerDict[pplpkrDiv[0].id]; if (clientPplPicker.GetAllUserInfo().length === 0) { clientPplPicker.AddUserKeys(currentUser); } }, "clientpeoplepicker.js"); } else { var displayName = containerCollection.rows[opt.adapter.columnNameInternal].displayName; var picker = $().SPServices.SPFindPeoplePicker({ peoplePickerDisplayName: displayName }); if (!picker.currentValue) { ExecuteOrDelayUntilScriptLoaded(function () { setTimeout(function () { $().SPServices.SPFindPeoplePicker({ peoplePickerDisplayName: displayName, valueToSet: currentUser, checkNames: false }); }, 1000); }, "sp.js"); } } } }
Note that my requirement was to default the field on the new form only, but my transform method is going to get called on all forms, so the first thing I do is check if I’m on the new form and if not just return. getFormType returns the word ‘new’, ‘edit’, or ‘display’ depending on what form I’m on, always in lower case.
Also, you can’t assume you will only be called once on form load. That’s because of things like conditional visibility based on the value of another field. So if I had a rule that said hide a field if another field has the value Blue, and the other field value changes, I need to reevaluate and apply conditional visibility. But after the conditional visibility is updated, one of my adapters may need to do something to the updated UI so the adapter transforms are called again. I deal with that here by checking if the user field already has a value, and if so, I don’t do anything. That way I don’t just keep adding the current user to the field over and over again. This also means if you set a default value using the SharePoint user interface this plug-in will not do anything, but if you want it to default to current user it doesn’t make sense to hard code a default in the SharePoint user interface anyway.
So after uploading my new JavaScript and clearing my browser cache, if I launch the new form for my list I see:
Note that in the above form, I’ve actually configured a second field to also default to current user (BackupSalesReps), which is a multi-user field, just to demonstrate that it works for either single or multiple user fields.
Also note that when I first went to test this on 2010, I had a spot of bother. I found that about 1 in every 15 times I loaded the form, something blanked out the people picker. I could see the field getting set with my userid, but a split second later it was blanked out. Clearly I was in a race condition with some OOTB people picker JavaScript. I debugged it for a bit trying to figure out what was going on, but didn’t have any luck. I also did some Binging to see if anybody else had seen this behavior and had a work around, but that wasn’t a very satisfying effort either. Ultimately, I did see a couple of suggestions to use ExecuteOrDelayUntilScriptLoaded and wait for SP.js, so I tried that, after which I only got blanked out maybe 1 in every 20 times. Not that helpful. In the end, I added a setTimeout to delay the SPServices call for 1 second, after which it always seemed to work. Is it a great solution? …No. But I needed to wrap this up for now, it seems to work pretty well, and I’m not above a good hack when all else fails. I’ll certainly look for a more definitive solution before rolling this back into the main, but for now I’m moving on.
And that concludes our implementation of a DefaultToCurrentUser adapter plug-in for SPEasyForms. In just about 100 lines of JavaScript I now have a reusable custom form field that anyone can configure on any user field in almost any list in my site collection, without needing to know anything about JavaScript or HTML. I realize that my source code is now closer to 150 lines, but that includes a lot of white space and comments.
The complete solution can be downloaded with the latest downloads package of SPEasyForms.AddOns from my CodePlex site, as either a ready to deploy WSP or in source code form. Hope you like it!
BTW, there are a some improvements I could make, and may make, before I roll this back into SPEasyForms proper. For instance, the dialog box always looks the same whether you’ve already added the adapter to a field or not, and always gives you the Add and Remove options. It would be a bit nicer if I did a little more work and changed it to say ‘would you like to add this…’ and only had the Add button, if the adapter was not already present on a field, and said ‘would you like to remove this…’ and only had the Remove button if the adapter was already added on the field. It is at least coded so hitting Add if its already there or Remove if its not doesn’t actually hurt anything.