maanantai 11. huhtikuuta 2016

Using ReactJS and external rest-API in Sitecore Experience Editor

In this post I will talk you through a simple ReactJS-application that retrieves content from an external REST api, mixed with fields that can be edited in Sitecore Experience Editor. For this example, I mocked a REST-api to mockable.io, where you can define your json quite freely. The objective of the example is to use a field in Sitecore to pass an index of an apartment in a JSON to a controller, and then render the returned apartment to the page. All configured in Experience Editor.

But, first things first:
Create a new template in Sitecore 
The very first thing to do is to add a new data template to Sitecore. We'll use this template to save apartment index to, so we'll call this template "Apartment" and give it just one field called "Apartment index", with type "Number".

Create a new Controller rendering in Sitecore
Go to /sitecore/layout/Renderings/Views/[folder_path_to_your_liking] and create a new Controller rendering, called for example "Apartment". Or something. You'll need to select the Datasource Location and Datasource Template-fields, on location-field select just some folder you'll want to save items to, and to template you'd need to select the template just created above.

Install ReactJS.net through NuGet
I will not go into details with this, but you should install ReactJS.NET to your Visual Studio project through NuGet. Installation information here: http://reactjs.net/getting-started/download.html


Create a controller (.cs)
Back in Visual Studio, create a new controller. This controller will contact the external REST-api and populate the model and pass it to the view. My controller is called ApartmentController.cs, but yours can be something else. Following the MVC controller naming convention though, it needs to end to the word Controller.

Here we have an action in the controller that returns a JsonResult -> when called from the view (or in this case, the reactJS-app), this will return plain json back to the view.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public JsonResult json(int homeIndex)
{
    var model = new Models.Apartment();
    var webClient = new System.Net.WebClient();
    var apartments = webClient.DownloadString("http://[subdomain].mockable.io/[servicepath]");

    List<Models.Apartment> mylist = JsonConvert.DeserializeObject<List<Models.Apartment>>(apartments);
    var apartment = mylist[homeIndex];

    // before returning the data retrieved from REST, add the Sitecore translations for the view
    apartment.detailsText = ScLanguageProvider.GetResourceText("details");
    apartment.addressText = ScLanguageProvider.GetResourceText("address");
    apartment.descriptionText = ScLanguageProvider.GetResourceText("description");
    apartment.relatedText = ScLanguageProvider.GetResourceText("related apartments");

    return Json(apartment, JsonRequestBehavior.AllowGet);
}


Create a model (.cs)
A model is needed to hold some variables that are populated in the controller and rendered in the view / react app. My model is called Apartment (could be anything), here's a snippet of it (really just a POCO):


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 public class Related
 {
    public int id { get; set; }
    public string name { get; set; }
 }

 public class Apartment
 {
    public int index { get; set; }
    public string guid { get; set; }
    public string mainImage { get; set; }
    public string apartmentName { get; set; }
    public string apartmentMainImage { get; set; }
    public string apartmentDescription { get; set; }
    public string address { get; set; }
    public List<Related> related { get; set; }
    public string details { get; set; }
    public string detailsText { get; set; }
    public string addressText { get; set; }
    public string descriptionText { get; set; }
    public string relatedText { get; set; }
 }


Create a view (.cshtml)
Create a new view, give it some descriptive name. I gave mine a name of ReactApp.cshtml :) 
In the view, we'll have a script reference to our own reactJS-file (.jsx), and references to the react "core" and API at fb.me. Also we'll have a javascript variable "reactApartmentIndex" populated from a Sitecore field to pass as the index to the ReactJs-app that should be used to retrieve correct apartment from the external json.


Choosing an apartment by giving the index of it in a json is not probably the most intuitive way to go, but it's here only for demonstration purposes. A much better alternative would be for example to use another REST-api to populate a dropdown on the page, so that the content editor could only use an apartment that is really existing.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@{
    var isPageEditor = Sitecore.Context.PageMode.IsExperienceEditor;
    //var isPreview = Sitecore.Context.PageMode.IsPreview; // there could be different stuff for preview, but not used here
}

@if (isPageEditor)// show editable field with description on edit mode
{
    <div>Choose index for apartment</div>
    <div class="form-control">@Html.Sitecore().Field("Apartment index")</div>
}
<div id="content"><img src="/sandbox/images/ajax-loader.gif" alt="" /></div>
<script src="https://fb.me/react-0.14.0.js"></script>
<script src="https://fb.me/react-dom-0.14.0.js"></script>
<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.1.js"> </script>
<script type="text/javascript">
    @* this var for reactjs-application usage -> passes the given index to an ajax call *@
    var reactApartmentIndex = "@Html.Sitecore().Field("Apartment index", new { DisableWebEdit = true })";
</script>
<script src="@Url.Content("~/sandbox/js/reactapp.jsx")"></script> // the react app reference


Create a React-file (.jsx)
Last, but not least, we'll create the ReactJS-file which is the actual application, all others are basically plumbing for this. Or not, since our ReactJS wouldn't do much without them. Oh well... :). My ReactJS-app is called... ta-daa: ReactApp.jsx. Maybe a more descriptive name could be in order.

First, lets take a look to the "parent" react-class that will call the other methods (functions?) in the React app:
 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
var ApartmentApp = React.createClass({
    displayName: 'ApartmentApp',
    loadApartmentFromServer: function () {
        $.ajax({
            url: this.props.url,
            type: "POST",
            dataType: 'json',
            contentType: "application/json; charset=utf-8",
            data: JSON.stringify({ homeIndex: reactApartmentIndex }),
            success: function (msg) {
                this.setState({ data: msg });
            }.bind(this),
            error: function (xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },
    getInitialState: function () {
        return { data: [] };
    },
    componentDidMount: function () {
        this.loadApartmentFromServer();
    },
    render: function() {
        return (
            <div>
                <ApartmentContent data={this.state.data } />
                <ApartmentBlocks data={this.state.data } />
            </div>
      );
    }

});

Above we'll set the initial state of the component (getInitialState), and use the componentDidMount-method to call the ajax-method with the passed in index. Also the url of the ajax method to call is passed in from the ReactDOM.render-function that can be found further down. Also we'll call the render-function to render the outcome using two additional components that we'll pass the data to, ApartmentContent and ApartmentBlocks.



 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
var ApartmentContent = React.createClass({
    render: function () {
        var apartmentNode = this.props.data;
        return (
            <div>
                <div className="row">
                    <div className="col-md-12">
                        <h1>{apartmentNode.apartmentName}</h1>
                    </div>
                </div>
                
            </div>
     );
    }
});

var ApartmentBlocks = React.createClass({
    render: function () {
        var apartmentNode = this.props.data;
        var related = this.props.data.related != undefined ? this.props.data.related : [];
        return (
        <div className="row">
            <div className="col-md-8">
                <img className="thumbnail" src={apartmentNode.apartmentMainImage} />
            </div>
            <div className="col-md-4">
                <div className="alert alert-info">
                    <div className="container">
                        <h4 dangerouslySetInnerHTML={{__html: apartmentNode.addressText}}></h4>
                        <div>{apartmentNode.address}</div>
                    </div>
                </div>
                <div className="alert alert-info">
                    <div className="container">
                        <h4 dangerouslySetInnerHTML={{__html: apartmentNode.descriptionText }}></h4>
                        <div>{apartmentNode.apartmentDescription}</div>
                    </div>
                </div>
                <div className="alert alert-info">
                    <div className="container">
                        <h4 dangerouslySetInnerHTML={{__html: apartmentNode.detailsText}}></h4>
                        <div>{apartmentNode.details}</div>
                    </div>
                </div>
                <div className="alert alert-warning">
                    <div className="container">
                        <h4 dangerouslySetInnerHTML={{__html: apartmentNode.relatedText }}></h4>
                        <div>
                            <ol>
                                {related.map(function (rel) {
                                return (
                                    <li key={ rel.id }>{rel.name}</li>);
                                })}
                            </ol>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        );
    }
});

// ReactDOM.render has to be the last element
ReactDOM.render(
  <ApartmentApp url="/api/sitecore/Apartment/json" index="1" />,
  document.getElementById('content')
);

The ApartmentContent-component only renders the apartmentName passed in the props, while ApartmentBlocks renders the field titles that can be edited in Sitecore (for example
dangerouslySetInnerHTML={{__html: apartmentNode.relatedText }})
and of course the other content of the apartment, passed in in the props.
See also the how the related apartments are rendered from a generic list on line 50.

The last (but definitely the most important) thing is to call the render-method of the ReactDOM, which orchestrates the whole functionality of the app (on line 65 above). The first parameter of the render-method is a reference to the ApartmentApp-component with the default parameters (or props as they are called in React) url and index, and the latter part is an html-element where to put the component outcome (div-element on line 11 of the view).

TL;DR; an example how to use ReactJS in Sitecore, with Experience Editor support

keskiviikko 2. maaliskuuta 2016

Implementing forms with Sitecore Web Forms for Marketers & Typeform I/O

I had a chance to fiddle around with how to implement a form that is created in Sitecore Experience Editor (Sitecore 8.1, running on MVC5), with Web Forms For Marketers (aka. WFFM) ,that is then shown from Typeform I/O using their API.

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 :)