Add JavaScript to the Client OnClick Event of the Sitecore WFFM Submit Button
A SDN forum thread popped up a week and a half ago asking whether it were possible to attach a Google Analytics event to the WFFM submit button — such would involve adding a snippet of JavaScript to the OnClick attribute of the WFFM submit button’s HTML — and I was immediately curious how one would go about achieving this, and whether this were possible at all.
I did a couple of hours of research last night — I experimented with custom processors of pipelines used by WFFM — but found no clean way of adding JavaScript to the OnClick event of the WFFM submit button.
However — right before I was about to throw in the towel for the night — I did find a solution on how one could achieve this — albeit not necessarily a clean solution since it involves some HTML manipulation (I would opine using the OnClientClick attribute of an ASP.NET Button to be cleaner, but couldn’t access the WFFM submit button due to its encapsulation and protection level in a WFFM WebControl) — via a custom Sitecore.Form.Core.Renderings.FormRender:
using System.IO; using System.Linq; using System.Web.UI; using Sitecore.Form.Core.Renderings; using HtmlAgilityPack; namespace Sitecore.Sandbox.Form.Core.Renderings { public class AddOnClientClickFormRender : FormRender { private const string ConfirmJavaScriptFormat = "if(!confirm('Are you sure you want to submit this form?')) {{ return false; }} {0} "; protected override void DoRender(HtmlTextWriter output) { string html = string.Empty; using (StringWriter stringWriter = new StringWriter()) { using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(stringWriter)) { base.DoRender(htmlTextWriter); } html = AddOnClientClickToSubmitButton(stringWriter.ToString()); } output.Write(html); } private static string AddOnClientClickToSubmitButton(string html) { if (string.IsNullOrWhiteSpace(html)) { return html; } HtmlNode submitButton = GetSubmitButton(html); if (submitButton == null && submitButton.Attributes["onclick"] != null) { return html; } submitButton.Attributes["onclick"].Value = string.Format(ConfirmJavaScriptFormat, submitButton.Attributes["onclick"].Value); return submitButton.OwnerDocument.DocumentNode.InnerHtml; } private static HtmlNode GetSubmitButton(string html) { HtmlNode documentNode = GetHtmlDocumentNode(html); return documentNode.SelectNodes("//input[@type='submit']").FirstOrDefault(); } private static HtmlNode GetHtmlDocumentNode(string html) { HtmlDocument htmlDocument = CreateNewHtmlDocument(html); return htmlDocument.DocumentNode; } private static HtmlDocument CreateNewHtmlDocument(string html) { HtmlDocument htmlDocument = new HtmlDocument(); htmlDocument.LoadHtml(html); return htmlDocument; } } }
The FormRender above uses Html Agility Pack — which comes with Sitecore — to retrieve the submit button in the HTML that is constructed by the base FormRender class, and adds a snippet of JavaScript to the beginning of the OnClick attribute (there is already JavaScript in this attribute, and we want to run our JavaScript first).
I didn’t wire up a Google Analytics event to the submit button in this FormRender — it would’ve required me to spin up an account for my local sandbox instance, and I feel this would’ve been overkill for this post.
Instead — as an example of adding JavaScript to the OnClick attribute of the WFFM submit button — I added code to launch a JavaScript confirmation dialog asking the form submitter whether he/she would like to continue submitting the form. If the user clicks the ‘Cancel’ button, the form is not submitted, and is submitted if the user clicks ‘OK’.
I then had to hook this custom FormRender to the WFFM Form Rendering — /sitecore/layout/Renderings/Modules/Web Forms for Marketers/Form — in Sitecore:
I then saved, published, and navigated to a WFFM test form. I then clicked the submit button:
As you can see, I was prompted with a JavaScript confirmation dialog box.
If you have any thoughts on this implementation, or know of a better way to do this, please drop a comment.
Until next time, have a Sitecorelicious day! 🙂
Set New Media Library Item Fields Via the Sitecore Item Web API
On a recent project, I found the need to set field data on new media library items using the Sitecore Item Web API — a feature that is not supported “out of the box”.
After digging through Sitecore.ItemWebApi.dll, I discovered where one could add the ability to update fields on newly created media library items:
Unfortunately, the CreateMediaItems method in the Sitecore.ItemWebApi.Pipelines.Request.ResolveAction class is declared private — introducing code to set fields on new media library items will require some copying and pasting of code.
Honestly, I loathe duplicating code. 😦
Unfortunately, we must do it in order to add the capability of setting fields on media library items via the Sitecore Item Web API (if you can think of a better way, please leave a comment).
I did just that on the following subclass of Sitecore.ItemWebApi.Pipelines.Request.ResolveAction:
using System; using System.Collections.Generic; using System.IO; using System.Web; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.IO; using Sitecore.ItemWebApi; using Sitecore.ItemWebApi.Pipelines.Read; using Sitecore.ItemWebApi.Pipelines.Request; using Sitecore.Pipelines; using Sitecore.Resources.Media; using Sitecore.Text; using Sitecore.Data.Fields; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request { public class ResolveActionMediaItems : ResolveAction { protected override void ExecuteCreateRequest(RequestArgs args) { Assert.ArgumentNotNull(args, "args"); if (IsMediaCreation(args.Context)) { CreateMediaItems(args); return; } base.ExecuteCreateRequest(args); } private void CreateMediaItems(RequestArgs args) { Assert.ArgumentNotNull(args, "args"); Item parent = args.Context.Item; if (parent == null) { throw new Exception("The specified location not found."); } string fullPath = parent.Paths.FullPath; if (!fullPath.StartsWith("/sitecore/media library")) { throw new Exception(string.Format("The specified location of media items is not in the Media Library ({0}).", fullPath)); } string name = args.Context.HttpContext.Request.Params["name"]; if (string.IsNullOrEmpty(name)) { throw new Exception("Item name not specified (HTTP parameter 'name')."); } Database database = args.Context.Database; Assert.IsNotNull(database, "Database not resolved."); HttpFileCollection files = args.Context.HttpContext.Request.Files; Assert.IsTrue(files.Count > 0, "Files not found."); List<Item> list = new List<Item>(); for (int i = 0; i < files.Count; i++) { HttpPostedFile file = files[i]; if (file.ContentLength != 0) { string fileName = file.FileName; string uniqueName = ItemUtil.GetUniqueName(parent, name); string destination = string.Format("{0}/{1}", fullPath, uniqueName); MediaCreatorOptions options = new MediaCreatorOptions { AlternateText = fileName, Database = database, Destination = destination, Versioned = false }; Stream inputStream = file.InputStream; string extension = FileUtil.GetExtension(fileName); string filePath = string.Format("{0}.{1}", uniqueName, extension); try { Item item = MediaManager.Creator.CreateFromStream(inputStream, filePath, options); SetFields(item, args.Context.HttpContext.Request["fields"]); // MR: set field data on item if data is passed list.Add(item); } catch { Logger.Warn("Cannot create the media item."); } } } ReadArgs readArgs = new ReadArgs(list.ToArray()); CorePipeline.Run("itemWebApiRead", readArgs); args.Result = readArgs.Result; } private static void SetFields(Item item, string fieldsQueryString) { if (!string.IsNullOrWhiteSpace(fieldsQueryString)) { SetFields(item, new UrlString(fieldsQueryString)); } } private static void SetFields(Item item, UrlString fields) { Assert.ArgumentNotNull(item, "item"); Assert.ArgumentNotNull(fields, "fields"); if (fields.Parameters.Count < 1) { return; } item.Editing.BeginEdit(); foreach (string fieldName in fields.Parameters.Keys) { Field field = item.Fields[fieldName]; if(field != null) { field.Value = fields.Parameters[fieldName]; } } item.Editing.EndEdit(); } private bool IsMediaCreation(Sitecore.ItemWebApi.Context context) { Assert.ArgumentNotNull(context, "context"); return context.HttpContext.Request.Files.Count > 0; } } }
The above class reads fields supplied by client code via a query string passed in a query string parameter — the fields query string must be “url encoded” by the client code before being passed in the outer query string.
We then delegate to an instance of the Sitecore.Text.UrlString class when fields are supplied by client code — if you don’t check to see if the query string is null, empty or whitespace, the UrlString class will throw an exception if it’s not set — to parse the fields query string, and loop over the parameters within it — each parameter represents a field to be set on the item, and is set if it exists (see the SetFields methods above).
I replaced Sitecore.ItemWebApi.Pipelines.Request.ResolveAction in \App_Config\Include\Sitecore.ItemWebApi.config with our new pipeline processor above:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <!-- there is more stuff up here --> <!--Processes Item Web API requests. --> <itemWebApiRequest> <!-- there are more pipeline processors up here --> <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveActionMediaItems, Sitecore.Sandbox" /> <!-- there are more pipeline processors down here --> </itemWebApiRequest> <!-- there is more stuff down here --> </pipelines> </sitecore> </configuration>
I then modified the media library item creation code in my copy of the console application written by Kern Herskind Nightingale — Director of Technical Services at Sitecore UK — to send field data to the Sitecore Item Web API:
I set some fields using test data:
After I ran the console application, I got a response — this is a good sign 🙂
As you can see, our test field data has been set on our pizza media library item:
If you have any thoughts or suggestions on this, please drop a comment.
Now I’m hungry — perhaps I’ll order a pizza! 🙂
Change the Data Serialization Format in the Sitecore Item Web API
I had a major urge yesterday to continue my tinkering of the Sitecore Item Web API, and wondered how one would go about changing the serialization format of its response.
Without re-reading the documentation on how one could go about doing this — I forgot that this was discussed in its documentation (check out pages 16-17 in the Sitecore Item Web API 1.0.0 Developer’s Guide where you can see how to use an XML serializer) — I tackled this by experimentation, and came up with a slightly different solution than the one offered in the Developer’s Guide.
I considered using an XML serializer for this blog post, but decided to fish around on the internet to see what other data serialization formats exist, and discovered YAML — a format that looks similar to JSON.
I continued my internet surfing — I mean research — and found a .NET library — YamlDotNet — that assists developers in converting objects into YAML, and decided to give it a go.
The following Sitecore item Web API Serializer uses this YAML library:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Sitecore.Diagnostics; using Sitecore.ItemWebApi.Serialization; using YamlDotNet.RepresentationModel.Serialization; namespace Sitecore.Sandbox.ItemWebApi.Serialization { public class YamlSerializer : ISerializer { // Serializer in YamlDotNet.RepresentationModel.Serialization private static readonly Serializer Serializer = new Serializer(); public YamlSerializer() { } public string Serialize(object value) { Assert.ArgumentNotNull(value, "value"); string yaml = string.Empty; using(StringWriter stringWriter = new StringWriter()) { Serializer.Serialize(stringWriter, value); yaml = stringWriter.ToString(); } return yaml; } public string SerializedDataMediaType { get { return "application/x-yaml"; } } } }
The Serializer above just delegates responsibility to the YamlDotNet’s YAML serializer.
Now that we have our YamlSerializer ready to go, we have to somehow wire it up to the Sitecore Item Web API.
After some digging in Sitecore.ItemWebApi.dll, I learned the out of the box JsonSerializer is set in a preprocessRequest pipeline processor:
Following this lead, I created a custom preprocessRequest pipeline processor for setting our new YamlSerializer:
using System; using System.Web; using Sitecore.Pipelines.PreprocessRequest; using Sitecore.Diagnostics; using Sitecore.ItemWebApi.Serialization; using Sitecore.Sandbox.ItemWebApi.Serialization; namespace Sitecore.Sandbox.ItemWebApi.Pipelines.PreprocessRequest { public class ResolveSerializer : PreprocessRequestProcessor { public override void Process(PreprocessRequestArgs arguments) { Assert.ArgumentNotNull(arguments, "arguments"); Assert.ArgumentNotNull(arguments.Context, "arguments.Context"); Assert.ArgumentNotNull(arguments.Context.Request, "arguments.Context.Request"); ISerializer serializer = GetSerializer(arguments.Context.Request); if (serializer != null) { Sitecore.ItemWebApi.Context.Current.Serializer = serializer; } } private static ISerializer GetSerializer(HttpRequest request) { if(IsYamlRequest(request)) { return new YamlSerializer(); } return null; } private static bool IsYamlRequest(HttpRequest request) { Assert.ArgumentNotNull(request, "request"); return string.Equals(request["format"], "yaml", StringComparison.CurrentCultureIgnoreCase); } } }
We only set our YamlSerializer when client code requests data to be returned as YAML — this is made known when the client code sets “Yaml” in a HTTP request parameter named “format” (an example of this would be &format=Yaml via a query string).
I then added the new preprocessRequest pipeline processor in \App_Config\Include\Sitecore.ItemWebApi.config, and made sure it’s called right after /configuration/sitecore/pipelines/preprocessRequest/processor[@type=”Sitecore.ItemWebApi.Pipelines.PreprocessRequest.RewriteUrl, Sitecore.ItemWebApi”] — default instances are set here on the Sitecore Item Web API’s Context instance, and we should ensure this object exits before changing properties on it:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <!-- there is stuff up here --> <preprocessRequest> <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.PreprocessRequest.ResolveSerializer, Sitecore.Sandbox" patch:before="processor[@type='Sitecore.Pipelines.PreprocessRequest.CheckIgnoreFlag, Sitecore.Kernel']" /> <processor type="Sitecore.ItemWebApi.Pipelines.PreprocessRequest.RewriteUrl, Sitecore.ItemWebApi" patch:before="processor[@type='Sitecore.Sandbox.ItemWebApi.Pipelines.PreprocessRequest.ResolveSerializer, Sitecore.Sandbox']" /> </preprocessRequest> <!-- and more stuff down here --> </sitecore> </configuration>
After modifying some code in my copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK — I added &format=yaml as a query string parameter — I invoked it to retrieve an item in my local instance of Sitecore:
As you can see, the response is now in YAML.
If you have any thoughts on this, or have any recommendations on using other serialization formats, please drop a comment.