In order to get anything out of Typeform I/O, you have to get yourself a API key from typeform.io. That shouldn't take more than a few minutes, and you're all set.
First, in Visual Studio I created a blank controller, which I gave a name "TypeformController.cs". To the default Index-action I only modified it to return a view we'll create shortly, like this:
1 2 3 4 | public ActionResult Index() { return View("~/views/forms/typeformio.cshtml"); } |
Then, I created a new view called TypeformIO.cshtml, which I just referenced in the controller. This view holds the presentation and also some ajax calls to both the controller and SSC (Sitecore Services Client, which is a REST-type of interface to Sitecore items). SSC is used here to get the fields and texts of the form every time the form is modified and saved.
Before calling SSC from unauthenticated (anonymous) user, you should change a setting in Sitecore.Services.Client.Config:
1 | <setting name="Sitecore.Services.AllowAnonymousUser" value="true" /> |
Also, change the value of Sitecore.Services.SecurityPolicy from the default Sitecore.Services.Infrastructure.Web.Http.Security.ServicesLocalOnlyPolicy to
Sitecore.Services.Infrastructure.Web.Http.Security.ServicesOnPolicy.
By default anonymous access to SSC is disabled, and your requests will face an unauthorized 403 message. If allowing anonymous access is clever or even meaningful, is a matter of a totally separate discussion :)
I configured the view as a controller rendering to Sitecore, and gave it a name TypeFormIO. Also I added this new rendering to Placeholder settings of a suitable placeholder, so that I could add it to a page in Experience Editor.
A lot of scripts in the view
In Visual Studio, I added a new dynamic placeholder called "wffmform" (name could be anything really) to the TypeFormIO.cshtml, which is only shown when the editor is in edit mode, otherwise it's hidden by css. In this solution, the WFFM-form always needs to be in the DOM, since jQuery is used to retrieve the id of the form and pass it to SSC to get the form fields and texts. For a more real world implementation I would recommend a different approach, for example saving the WFFM guid to a separate field and passing that value around: that would save some rendering effort + the form really shouldn't be there in the DOM at all, when it's hidden in live mode.
For this experiment to work I needed to:
- get the form title and introduction -fields as json from SSC in order to map them & pass to Typeform I/O
- get the form fields as json from SSC in order to map them as Typeform I/O -fields & pass to Typeform I/O
- post formatted data to Typeform I/O and get the response
- render the Typeform I/O - form to the page
I added a new jQuery ajax-call on a function in document.ready-event in order to get the WFFM form data from SSC as json, following a callback function that maps the WFFM-fields as Typeform I/O -fields and creates a json-object from them, then another callback function that sends the data to Typeform I/O and retrieves the response data, and still another callback function that renders the Typeform I/O -created form to the page using the url of the created form, returned from Typeform I/O.
Sounds tricky? Well, it's not, actually. Not THAT tricky :)
First set up some variables:
1 2 3 4 | var dataType = "json"; var postType = "POST"; var getType = "GET"; var contentType = "application/json; charset=utf-8"; |
Get the guid of the current form with jQuery selector, pass it to the getWffmForm-function, as well as a callback function getWffmFormFields (called in line 19):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // get the guid of the WFFM form var guid = jQuery('form[data-wffm]').attr('data-wffm'); // call the main function, all other functions called from callbacks getWffmForm(guid, getWffmFormFields); // returns the WFFM-item data, calls the SSC asynchronously function getWffmForm(guid, callback) { jQuery.ajax({ type: getType, url: "/sitecore/api/ssc/item/" + guid, // get the current WFFM-form item contentType: contentType, dataType: dataType, success: function (msg) { var item = (typeof msg) == 'string' ? eval('(' + msg + ')') : msg; var title = item.Title; // these are returned from SSC var introduction = item.Introduction; if (callback && typeof (callback) === "function") { callback(title, introduction, guid, getTypeform); } }, error: function (req) { // handle errors } }); } |
Get the form fields of the WFFM-form, and map them as Typeform I/O -fields, call getTypeform through the passed in callback (line 19 in prev. snippet):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | // returns the fields from WFFM item by using another (/children) endpoint of SSC function getWffmFormFields(title, introduction, guid, callback) { jQuery.ajax({ type: getType, url: "/sitecore/api/ssc/item/" + guid + "/children", // get the children (fields) of the current WFFM-form item contentType: contentType, dataType: dataType, success: function (msg) { var items = (typeof msg) == 'string' ? eval('(' + msg + ')') : msg; var jsonObject = []; for (i = 0; i < items.length; i += 1) { var required = items[i].Required == 0 ? false : true; var tmpElem = jQuery('<div></div>'); tmpElem.html(items[i]["Localized Parameters"]); var description = jQuery('Information', tmpElem).html(); if (description == null || description == undefined) description = ""; var fieldTypeGuid = items[i]["Field Link"]; var fieldTypeMapped = 'short_text'; // these should be really mapped in a separate controller if (fieldTypeGuid == '{42DB4C51-5A19-4BD3-9632-CB488DD63849}') { fieldTypeMapped = "long_text"; } // in Typeform I/O checkbox list and radio list are the same element, // with only "allow_multiple_selection"-property separating them. if (fieldTypeGuid == '{E994EAE0-EDB0-4D89-B545-FEBEF07DD7CD}' || fieldTypeGuid == '{0FAE4DE2-5C37-45A6-B474-9E3AB95FF452}') { fieldTypeMapped = "multiple_choice"; } if (fieldTypeGuid == '{C6D97C39-23B5-4B7E-AFC7-9F41795533C6}') { fieldTypeMapped = "dropdown"; } if (fieldTypeGuid == '{002E5FD5-8B12-4913-BA52-BCC5FEAD2785}') { fieldTypeMapped = "number"; } if (fieldTypeGuid == '{84ABDA34-F9B1-4D3A-A69B-E28F39697069}') { fieldTypeMapped = "email"; } if (fieldTypeGuid == '{E994EAE0-EDB0-4D89-B545-FEBEF07DD7CD}' || fieldTypeGuid == '{C6D97C39-23B5-4B7E-AFC7-9F41795533C6}' || fieldTypeGuid == '{0FAE4DE2-5C37-45A6-B474-9E3AB95FF452}') { // get the choices from the localized params var choices = []; var itemsTag = jQuery('Items', tmpElem).html(); var itemsTmp = decodeURIComponent(itemsTag); var tmpElem = jQuery('<div></div>'); tmpElem.html(itemsTmp); var choicesTmp = jQuery('Value', tmpElem); // push choices to array choicesTmp.each(function (index) { choice = { 'label': decodeURIComponent(jQuery(this).text().replace(/\+/g, " ")) } choices.push(choice); }); } tmp = { 'type': fieldTypeMapped, 'question': items[i].Title, 'description': description, 'required': required }; // adding special TypeformIO-required fields if (fieldTypeMapped == "multiple_choice" || fieldTypeMapped == "dropdown") { tmp['choices'] = choices; } // field type from Sitecore == checkbox list if (fieldTypeGuid == '{E994EAE0-EDB0-4D89-B545-FEBEF07DD7CD}') { tmp['allow_multiple_selections'] = true; } jsonObject.push(tmp); } // quick & dirty removal of html tags from WFFM-introduction var tmp = document.createElement("DIV"); tmp.innerHTML = introduction; introduction = tmp.textContent; // create the Typeform I/O "statement" fieldtype from the WFFM-form title & introduction statement = { 'type': 'statement', 'question': title, 'description': introduction }; jsonObject.unshift(statement); // put statement always as the first element if (callback && typeof (callback) === "function") { callback(jsonObject, title, renderForm); } }, error: function (req) { // handle errors } }); } |
Pass the data to our controller that will send the request to Typeform I/O, render the form through the passed in renderForm-callback (line 100 on previous snippet):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // passes data to TypeFormIO, adds elements + returned form href to elements in the callback // this calls our TypeformController function getTypeform(json, title, callback){ jQuery.ajax({ type: postType, url: '/api/sitecore/TypeForm/getForm', // as in /api/sitecore/controller/action contentType: contentType, data: "{title:'" + escape(title) + "', rawfields:'" + JSON.stringify(json) + "'}", dataType: dataType, success: function (msg) { var href = msg._links[1].href; // the location of the form in Typeform I/O if (callback && typeof (callback) === "function") { callback(href); } }, error: function (req) { // handle errors } }); } |
Finally render the form with renderForm-function that gets called from a callback of getTypeForm-function (line 14 in prev. snippet):
1 2 3 4 5 6 7 8 9 10 | // renders the Typeform into the page function renderForm(href) { var typeFormId = 'typeFormDiv'; jQuery('#loading').hide(); jQuery('#tfTitle').append('<div id="' + typeFormId + '" class="typeform-widget" data-url="" data-text="All fields" style="width:100%;height:500px;"></div>'); jQuery('#' + typeFormId).attr('data-url', href); jQuery('#' + typeFormId).append(' <script type="text/javascript"> (function () { var qs, js, q, s, d = document, gi = d.getElementById, ce = d.createElement, gt = d.getElementsByTagName, id = "typef_orm", b = "https://s3-eu-west-1.amazonaws.com/share.typeform.com/"; if (!gi.call(d, id)) { js = ce.call(d, "script"); js.id = id; js.src = b + "widget.js"; q = gt.call(d, "script")[0]; q.parentNode.insertBefore(js, q) } })();<\/script>'); jQuery('#formTemplate').show(); } |
And here is the html-part of the view. In the above function the typeform div / script is appended to the tfTitle-element:
1 2 3 4 5 6 7 8 | <div id="loading"><img src="/sandbox/images/ajax-loader.gif" alt="" /></div> <div id="formTemplate"> <div class="row"> <div class="col-md-12"> <h1 id="tfTitle"></h1> </div> </div> </div> |
All of the above javascript-snippets could be optimized quite a lot, but I didn't focus too much on the aspect of getting it right, rather than getting it to work :)
The controller
The action in the TypeformController takes 2 parameters, title and rawfields. The title-parameter is sent to Typeform as such, but rawfields is parsed as Typeform I/O understandable mode through serialization before sending. The action uses plain old HttpWebRequest in traffic from my WFFM-form to Typeform I/O. Here's the code of the controller action (note the lack of exception handling and the likes):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // sends data to Typeform I/O, returns deserialized response public JsonResult getForm(string title, string rawfields) { HttpWebRequest httpWebRequest = getHttpWebRequest("https://api.typeform.io/latest/forms", "YOUR-TYPEFORMIO-API-KEY-HERE", "POST"); List<TypeformRequest.TypeformField> fields = new List<TypeformRequest.TypeformField>(); object list = JsonConvert.DeserializeObject(rawfields, typeof(List<TypeformRequest.TypeformField>), new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); fields = (List<TypeformRequest.TypeformField>)list; var result = ""; using (var streamWriter = new StreamWriter(httpWebRequest.GetRequestStream())) { string json = JsonConvert.SerializeObject(new TypeformRequest() { fields = fields, title = title, webhook_submit_url = "https://someurl.com/where_you/need_to/have_a/service" }, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } // ignore nulls, as all of the fields do not have all properties ); streamWriter.Write(json); streamWriter.Flush(); streamWriter.Close(); } var httpResponse = (HttpWebResponse)httpWebRequest.GetResponse(); using (var streamReader = new StreamReader(httpResponse.GetResponseStream())) { result = streamReader.ReadToEnd(); } return Json(DeserializeJSon<TypeformResponse.Form>(result)); } |
And last but not least, there's a few models (TypeformRequest.cs for serializing request, TypeformResponse.cs for deserializing response from Typeform I/O) that I threw in the mix also. You'll find specifications & json of Typeform I/O API from docs.typeform.io, and from the json-examples you can create models for example using an online tool at json2csharp.com.
End result
This is how the form looks like in the Experience Editor / Form designer:
The form on Experience Editor:
... And finally the result after saving and publishing the form (also visible on edit-mode, but not unfortunately visible on the screenshot above):
Pros & cons
- Typeform I/O -forms cannot be localized, as the label texts are always in english, in static form. Questions and descriptions can be any language, as they are passed to the API
- Typeform.com -forms on the other hand can be localized, and the texts can be modified
- requires a service on a public url on the requesting site, which is used by Typeform I/O to post the form data to. Can be a bit tricky to test on a local development machine, unless dev is on publicly accessable cloud instance or the service is mocked (to mockable.io or similar)
- roundtrip of "requesting site -> Typeform I/O -> service on the requesting site" can be unreliable, and will need more development effort. Typeform I/O sends the form data to a url of a service defined in in the request, and that needs to be parsed as WFFM understandable answer.
- data is passed always through https, ofcourse the information security depends on also that the requesting page is under https
- the layout of the form can be edited, but possibilities are pretty limited: only fonts and colors. You cannot for example position the form in any way.
- Typeform.com and Typeform I/O are quite different, I/O is a subset of Typeform.com API
- I/O: no localization, label texts are static
- I/O: no variable logic -> value from another answer cannot be used in some other question. For example this is not possible: Question 1. What's your name: [name of the user] -> Question 2. Hi [name of the user], what do you think of blahblah??
- the lack of logic is due that the questions of the form are sent to Typeform I/O before form rendering, so that way the form is always quite static
- only logic that can be used is "Logic jumps": the next question shown can be determined from an answer to a yes/no-type of question
- I/O is in the beta-stage, probably will get the same features that Typeform.com has. Maybe.
- Forms created with Sitecore WFFM to Typeform I/O will lose some of the user tracking features, for example "form dropouts" cannot be tracked. This is because the Typeform I/O-form doesn't tell anything back to Sitecore before form is submitted
- some of the Typeform I/O -fields (website, rating, yes_no, opinion_scale) need changes to WFFM itself as custom fields
Please if any questions, remarks or whatever: leave a message :)
TLDR; well, I guess you just read it anyway :)
Ei kommentteja:
Lähetä kommentti