Category Archives: SPEAK

Fetch and save Sitecore Item in SPEAK 2 using Sitecore.Services.Client

testapppage

Hello guys, yet another post about SPEAK ๐Ÿ™‚
In this medium small post I would like to share with you how I work with Sitecore items in SPEAK.

Let’s say you need to retrieve a Sitecore item, modify it and update it.
In this example the Sitecore item is exposed in an url, like this:

https://somehost.com/sitecore/client/MyApp/Applications/TestApp/TestAppPage?id=7E4CA53A-59AA-4C08-9EEF-71DDF66E8A8E&database=master&language=en&version=1

This will be a simple page containing two text boxes and a save button(See above)
Here is the Design Layout for the page:
tasklayout
I really like the Grid rendering and guess what it’s responsive friendly ๐Ÿ™‚

To get an item I do something like this:

GetTestData: function (callback) {

	var self = this;
	var testObject = {};
	testObject["id"] = Sitecore.Speak.utils.url.parameterByName("id");
	testObject["language"] = Sitecore.Speak.utils.url.parameterByName("language");
	testObject["version"] = Sitecore.Speak.utils.url.parameterByName("version");
	testObject["database"] = Sitecore.Speak.utils.url.parameterByName("database");
	testObject["databaseUri"] = new _sc.Definitions.Data.DatabaseUri(testObject["database"]);
	testObject["itemUri"] = new _sc.Definitions.Data
		.ItemUri(testObject["databaseUri"], testObject["id"]);
	testObject["itemVersionUri"] = new _sc.Definitions.Data
		.ItemVersionUri(testObject["itemUri"],
			testObject["language"],
			parseInt(testObject["version"]));

	var db = new _sc.Definitions.Data.Database(testObject["databaseUri"]);

	db.getItem(testObject["itemVersionUri"],
		function (item) {

			testObject["itemName"] = item.itemName;
			testObject["itemDisplayname"] = item.$displayName;

			var testHeaderField = item.getFieldById(self.Constants.TestHeaderFieldId);
			testObject["itemFieldTestHeader"] = testHeaderField.value;

			var testDescriptionField = item.getFieldById(self.Constants.TestDescriptionFieldId);
			testObject["itemFieldTestDescription"] = testDescriptionField.value;

			callback(testObject);

		});


}

The reason for the callback function is because of db.getItem. When the method has retrieved the Sitecore item I’ll put the data in a JSON object.
The Sitecore.Speak.utils.url.parameterByName is indeed handy ๐Ÿ™‚
(I’m not using Sitecore.Services.Client here)

To call the GetTestData method:

BindTestData: function () {
	var self = this;

	self.GetTestData(function (testObject) {
		self.TestData = testObject;

		//Do some binding stuff to the controls on your Speak layout page
		self.MyHeaderTextBox.Value = self.TestData.itemFieldTestHeader;
		self.MyDescriptionTextBox.Value = self.TestData.itemFieldTestDescription;
	});

}

Here we will bind/set the data to the text boxes.
(self.TestData is just a container)

To update a Sitecore item we will use Sitecore.Services.Client. Thanks to Kevin Obee’s great “living” documentation, http://docs.itemserviceapi.apiary.io, he made it so much easier to understand how it works.
There is also a great post that describes the differences between Sitecore Item Web API and Sitecore.Services.Client – What is Sitecore.Services.Client?

Here is how the we update the Sitecore item using Sitecore.Services.Client

UpdateSitecoreData: function (callback) {
	var self = this;

	var request = new XMLHttpRequest();

	var url = self.StringFormat("/sitecore/api/ssc/item/{0}?database={1}&language={2}&version={3}",
		self.TestData.id,
		self.TestData.database,
		self.TestData.language,
		self.TestData.version);

	request.open("PATCH", url);

	request.setRequestHeader('Content-Type', 'application/json');

	var body = {
		'TestHeader': self.TestData.itemFieldTestHeader,
		'TestDescription': self.TestData.itemFieldTestDescription
	};

	request.send(JSON.stringify(body));

	request.onreadystatechange = function () {

		return callback(this.readyState, this.status, this.responseText);

	};
}

Again I’m using a callback, since we need to wait for the response if the save was successful or not. TestHeader and TestDescription are the field names of the Sitecore item.

Another thing, if you would get the forbidden page (You do not have permission to view this directory or page) when using the Sitecore.Services.Client. That means you probably have to take a look at the security policy settings in Sitecore.Services.Client.config. Find the setting that works best for you.

And here we call UpdateSitecoreData:

SaveTestData: function () {
	var self = this;

	//Get the data from the text boxes 
	self.TestData.itemFieldTestHeader = self.MyHeaderTextBox.Value;
	self.TestData.itemFieldTestDescription = self.MyDescriptionTextBox.Value;
	
	self.UpdateSitecoreData(function (readyState, status, responseText) {
		//Failure
		if (readyState === 4 && (this.status === 404 || status === 500)) {
			self.StatusAlertConfirmationDialog["HeaderText"] = "Save - error";
			self.StatusAlertConfirmationDialog["Title"] = "An error occurred while saving, please contact support";
			self.StatusAlertConfirmationDialog["IconType"] = "error";
			self.StatusAlertConfirmationDialog["Message"] = responseText;
			self.StatusAlertConfirmationDialog.show();
			self.StatusAlertConfirmationDialog.IsVisible = true;

		}

		//Success
		if (readyState === 4 && this.status !== 404 && status !== 500) {
			self.BindTestData();
		}


	});

}

If you succeed with the update we will “rebind” the data by calling BindTestData, if not we will show a StatusAlertConfirmationDialog(ConfirmationDialog).

Here is the full PageCode file:

(function (Speak) {

	Speak.pageCode({
		initialized: function () {
			console.log("Start TestAppPage");
			var self = this;

			self.SaveTestButton.on("click", self.ConfirmSave, this);

			self.UpdateTestDataConfirmationDialog.on("close",
				function (data) {

					if (data === "ok") {

						self.SaveSearch();
				
					}
                    self.UpdateTestDataConfirmationDialog.IsVisible = false;
				},
				this);	

			self.BindTestData();


		},
		ConfirmSave: function () {
			var self = this;
			self.UpdateTestDataConfirmationDialog.show();
			self.UpdateTestDataConfirmationDialog.IsVisible = true;
		},
		BindTestData: function () {
			var self = this;

			self.GetTestData(function (testObject) {
				self.TestData = testObject;

				//Do some binding stuff to the controls on your Speak layot page
                self.MyHeaderTextBox.Value = self.TestData.itemFieldTestHeader;
                self.MyDescriptionTextBox.Value = self.TestData.itemFieldTestDescription;
			});


		},
		GetTestData: function (callback) {

			var self = this;

			var testObject = {};
			testObject["id"] = Sitecore.Speak.utils.url.parameterByName("id");
			testObject["language"] = Sitecore.Speak.utils.url.parameterByName("language");
			testObject["version"] = Sitecore.Speak.utils.url.parameterByName("version");
			testObject["database"] = Sitecore.Speak.utils.url.parameterByName("database");
			testObject["databaseUri"] = new _sc.Definitions.Data.DatabaseUri(testObject["database"]);
			testObject["itemUri"] = new _sc.Definitions.Data
				.ItemUri(testObject["databaseUri"], testObject["id"]);
			testObject["itemVersionUri"] = new _sc.Definitions.Data
				.ItemVersionUri(testObject["itemUri"],
					testObject["language"],
					parseInt(testObject["version"]));

			var db = new _sc.Definitions.Data.Database(testObject["databaseUri"]);

			db.getItem(testObject["itemVersionUri"],
				function (item) {

					testObject["itemName"] = item.itemName;
					testObject["itemDisplayname"] = item.$displayName;

					var testHeaderField = item.getFieldById(self.Constants.TestHeaderFieldId);
					testObject["itemFieldTestHeader"] = testHeaderField.value;

					var testDescriptionField = item.getFieldById(self.Constants.TestDescriptionFieldId);
					testObject["itemFieldTestDescription"] = testDescriptionField.value;

					callback(testObject);

				});


		},
		Constants: {
			"TestHeaderFieldId": "{3699A67C-A4BC-486D-9A37-9E4554AE46CA}",
			"TestDescriptionFieldId": "{E85C0CA0-75F2-4FB7-B2B5-03D8B97EBCCA}"
		},
		TestData: function () {
			var content;

			return content;
		},
		SaveTestData: function () {
			var self = this;

			//Get the data from the textboxes 
			self.TestData.itemFieldTestHeader = self.MyHeaderTextBox.Value;
			self.TestData.itemFieldTestDescription = self.MyDescriptionTextBox.Value;
			
			self.UpdateSitecoreData(function (readyState, status, responseText) {
				//Failure
				if (readyState === 4 && (this.status === 404 || status === 500)) {
					self.StatusAlertConfirmationDialog["HeaderText"] = "Save - error";
					self.StatusAlertConfirmationDialog["Title"] = "An error occurred while saving, please contact support";
					self.StatusAlertConfirmationDialog["IconType"] = "error";
					self.StatusAlertConfirmationDialog["Message"] = responseText;
					self.StatusAlertConfirmationDialog.show();
					self.StatusAlertConfirmationDialog.IsVisible = true;

				}

				//Success
				if (readyState === 4 && this.status !== 404 && status !== 500) {
					self.BindTestData();
				}


			});


		},
		UpdateSitecoreData: function (callback) {
			var self = this;

			var request = new XMLHttpRequest();

			var url = self.StringFormat("/sitecore/api/ssc/item/{0}?database={1}&language={2}&version={3}",
				self.TestData.id,
				self.TestData.database,
				self.TestData.language,
				self.TestData.version);

			request.open("PATCH", url);

			request.setRequestHeader('Content-Type', 'application/json');

			var body = {
				'TestHeader': self.TestData.itemFieldTestHeader,
				'TestDescription': self.TestData.itemFieldTestDescription
			};

			request.send(JSON.stringify(body));

			request.onreadystatechange = function () {

				return callback(this.readyState, this.status, this.responseText);

			};
		},
		StringFormat: function () {
			var s = arguments[0];
			for (var i = 0; i < arguments.length - 1; i++) {
				var reg = new RegExp("\\{" + i + "\\}", "gm");
				s = s.replace(reg, arguments[i + 1]);
			}
			return s;
		}
		
	});
})(Sitecore.Speak);

If you noticed there is a call to ConfirmSave when the Save button is clicked which will popup a ConfirmationDialog.

ConfirmSave: function () {
	var self = this;
	self.UpdateTestDataConfirmationDialog.show();
	self.UpdateTestDataConfirmationDialog.IsVisible = true;
}

If you are wondering about the self.UpdateTestDataConfirmationDialog.IsVisible = true, yes the show() method is enough to pop up the dialog. But I need to know that the dialog is visible, that’s why I’m using the IsVisible attribute.

Finally in the initialized function we are listening to the UpdateTestDataConfirmationDialog’s close event. If OK button is clicked we will call the SaveSearch.

self.UpdateTestDataConfirmationDialog.on("close",
function (data) {

	if (data === "ok") {

		self.SaveSearch();
 
	}
	self.UpdateTestDataConfirmationDialog.IsVisible = false;
},
this);

Thatโ€™s it and keep doing some good SPEAK stuff out there.

Thatโ€™s all for now folks ๐Ÿ™‚

Put JSON data in your SearchDataSource and bind it to SearchableDroplist – Sitecore SPEAK

droplist

Hello guys, I have been working with SPEAK for a while and some of the controls I’ve been using a lot are:
SearchDataSource and SearchableDropList(SPEAK 2 control)

The SearchableDropList is a droplist with search, the datasources you can use are:
QueryDataSource
SearchDataSource

If you have less than 100 items then QueryDataSource is OK(since it’s using Sitecore Query or Sitecore Fast Query) but if you have more, then use the SearchDataSource(using good old index).

An extremely cool thing regarding SearchDataSource is that you can add JSON data to it. Just create a JSON array and add it like this:

(function (Speak) {

	Speak.pageCode({


		initialized: function () {
			
			var self = this;
			
			self.BindJsonDataToMyCoolSearchDataSource();


		},
        BindJsonDataToMyCoolSearchDataSource: function() {
			var self = this;

			var customJsonData = [];

			for (var i = 0; i < 10; i++) {

				var data = {};
				data["Value"] = i;
				data["Text"] = i + " some text"; 
				customJsonData.push();
			}


			self.MyCoolSearchDataSource.Items = customJsonData;

		}

	});
})(Sitecore.Speak);

To add the JSON data we just set the Items property on the SearchDataSource:
self.MyCoolSearchDataSource.Items = customJsonData;

Then in your SpeakLayout you will connect the SearchDataSource to your control, in this case the SearchableDropList:
speaksearch
To set the datasource for the SearchableDropList, we will use DynamicData. Here we point(bind) to the SearchDataSource:
{Binding MyCoolSearchDataSource.Items}

Next we will set DisplayFieldName to the JSON field “Text” and finally we set ValueFieldName to the JSON field “Value”.

Now you you will have a searchable droplist where you can search in your custom JSON data:
droplist

By the way the new version(Version 2) of Sitecore Rocks is so much faster and it has some very new nice features. It truly rocks ๐Ÿ˜‰

That’s it and keep doing some good SPEAK stuff out there

Thatโ€™s all for now folks ๐Ÿ™‚

Authorize your Web API controllers in Sitecore style

webapifilters01
I would like to share with you guys how easy it is to limit access to your Web API actions or controllers. Recently when I was working with a SPEAK component I had to make a Web API controller that only allowed users with a certain role(The Admin user will always have access).

So as always I looked at how Sitecore did and I just loved it ๐Ÿ™‚

They are using Authentication filters.

Authentication filters let you set an authentication scheme for individual controllers or actions. That way, your app can support different authentication mechanisms for different HTTP resources.

To apply an authentication filter to a controller, decorate the controller class with the filter attribute.

What we need is a filter to authenticate if user is part of a specific role.
I took a glance at on one of the filters Sitecore did, AuthorizedReportingUserFilter.

using Sitecore.Globalization;
using Sitecore.Xdb.Configuration;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace Sitecore.Cintel.Endpoint.Plumbing
{
  public class AuthorizedReportingUserFilter : AuthorizationFilterAttribute
  {
    public override void OnAuthorization(HttpActionContext actionContext)
    {
      if ((Context.User.IsAdministrator || Context.User.IsInRole("sitecore\\analytics reporting")) && (XdbSettings.Enabled && Context.User.IsAuthenticated))
        return;
      string message = Translate.Text("Unauthorized Access");
      actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, message);
      base.OnAuthorization(actionContext);
    }
  }
}

As you guys can see, in OnAuthorization that is where all the magic happens. In this case a check is made if user is part of analytics reporting and is authenticated.

It’s almost what we need. So what to do? Let’s make our own filter, like this:

using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Sitecore;
using Sitecore.Globalization;

namespace Sandbox.Website.Code.HttpFilters
{
  public class AuthorizedCustomRoleFilter : AuthorizationFilterAttribute
  {
    private readonly string _role;

    public AuthorizedCustomRoleFilter (string role)
    {
        _role = role;
    }

    public override void OnAuthorization(HttpActionContext actionContext)
    {
       if (Context.User.IsAdministrator || Context.User.IsInRole($"sitecore\\{_role}") && Context.User.IsAuthenticated)
       return;

       string message = Translate.Text("Unauthorized Access");
       actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, message);
       base.OnAuthorization(actionContext);
    }
  }
}

As you guys can see we have a parameter in the construct, that one allows us to add whatever role you want.

Here is an example where we use the new filter on a Web API controller.

namespace Sandbox.Website.Code.WebAPI.Controllers
{
  [AuthorizedCustomRoleFilter("Author")]
  public class MyController : ApiController
  {

So start doing a bunch of http filters for your web api controllers.

Thatโ€™s all for now folks ๐Ÿ™‚

View visitors in real-time in Sitecore using SignalR and MongoDB

VisitorsInRealtimeMap

In this post I would like to continue on the real-time subject, I see a great potential in using data in real-time. So why not combine my previous posts, Real-time in Sitecore using SignalR and MongoDB(where I showed you guys how to setup real-time and storing the data in Mongodb) and Make a Google Map SPEAK component in Sitecore.

How about show current visitors on a map in real-time. It’s almost like watching popcorn popping ๐Ÿ™‚
RealtimeVisitorsAnimated

Add methods to the SignalR Hub – RealTimeHub

In SignalR we need to let Sitecore users be able to listen to the visitor changes. In the class RealTimeHub we will add the AuthenticateListener() method which will be called from the client script in the new speak component. The method will authenticate the Sitecore user( called Listeners) and add the user to the “Listeners” group.

public async Task<bool> AuthenticateListener()
{
    IHubContextService hubService = this._hubContextService.Resolve(Context);

    if (!hubService.IsUserAuthenticated())
        return false;

    await SubscribeToChannel(RealTimeConstants.Signalr.Groups.Listeners);

    return true;
}

public async Task SubscribeToChannel(string channel)
{
    await Groups.Add(Context.ConnectionId, channel);
}

Next will be to create the “notify” methods when the visitor changes in the website. The methods will make client calls to the new SPEAK component.

this.Clients.Group(RealTimeConstants.Signalr.Groups.Listeners).onVisitorChanged(visitor);
this.Clients.Group(RealTimeConstants.Signalr.Groups.Listeners).onVisitorHasLeft(visitor);

The onVisitorChanged will be called in SendClientLocationData() method in class RealTimeHub when we get the geodata from the visitor.

The onVisitorHasLeft will be called when the visitors session has ended.

We will also add a method that presents current visitors for the “Listeners”.

public async Task ShowAllVisitors()
{
    IHubContextService hubService = this._hubContextService.Resolve(Context);

    if (!hubService.IsUserAuthenticated())
        return;
        
    await this._realTimeUserRepository.Collection().Find(v => !v.IsToBeDeleted).ForEachAsync((visitor, i) =>
    {
        this.Clients.Group(RealTimeConstants.Signalr.Groups.Listeners).onVisitorChanged(visitor);
    });
}

Create SPEAK component

The SignalR hub is now ready, lets start doing the SPEAK component. First we need to create a SPEAK page(VisitorsInRealtimeLayout):
VisitorsInRealTimeLayout
Here is the VisitorsInRealtimeLayout in “design layout”:
VisitorsInRealTimeLayoutDesign
We will use the map component, GoogleMapSpeak to present the visitors. The SubPageCode(the js file) will do the magic, connecting to SignalR to get visitor data.

To make SignalR work in SPEAK I added its “file paths” to the require.config.

require.config({
    paths: {
        "signalr.core": "/Scripts/jquery.signalR-2.2.0.min",
        "signalr.hubs": "/signalr/hubs?"
    },
    shim: {
        "signalr.core": {
            deps: ["jquery"],
            exports: "$.connection"
        },
        "signalr.hubs": {
            deps: ["signalr.core"]
        }
    }
});


define(["sitecore", "jquery", "signalr.hubs"], function (sitecore, jQuery) {

    var activeInfoWindow = null;

    var visitorsInRealtimePageCode = sitecore.Definitions.App.extend({

        initialized: function () {

            var self = this;

            self.setupRealtime();

        },
        setupRealtime: function () {

            var self = this;

            self.contactIconPath = "/sitecore/api/ao/v1/contacts/{0}/image?w=40&h=40";

            self.connection = jQuery.hubConnection();

            self.realTimeConnector = self.connection.createHubProxy("RealTimeConnector");

            self.realTimeConnector.on("onVisitorChanged", function (data) {

                if (activeInfoWindow != null)
                    activeInfoWindow.close();

                var infoWindow = new google.maps.InfoWindow();

                var visitor = data;

                if (visitor == null) {
                    console.log("No visitors");
                    return;
                }

                var marker =self.GoogleMapSpeak1.addMarkerToMap(visitor.ContactId,
                    visitor.RealTimeMetaData.GeoCoordinates.Coordinates[0],
                    visitor.RealTimeMetaData.GeoCoordinates.Coordinates[1],
                    self.stringFormat(self.contactIconPath, visitor.ContactId));


                infoWindow.setContent(self.renderHtmlContactInfo(visitor.ContactId));

                infoWindow.open(self.GoogleMapSpeak1.get("speakMap"), marker);

                marker.addListener('click', function () {
                    if (activeInfoWindow != null)
                        activeInfoWindow.close();

                    infoWindow.open(self.GoogleMapSpeak1.get("speakMap"), marker);
                });

                activeInfoWindow = infoWindow;
                
                console.log(visitor.ContactId);
                console.log(visitor.RealTimeMetaData.GeoCoordinates.Coordinates);

            });

            self.realTimeConnector.on("onVisitorHasLeft", function (data) {

                var visitor = data;

                if (visitor == null) {
                    console.log("No visitor");
                    return;
                }

                self.GoogleMapSpeak1.removeMarkerFromMap(visitor.ContactId);


                console.log(visitor.ContactId);
                console.log(visitor.RealTimeMetaData.GeoCoordinates.Coordinates);

            });

            self.realTimeConnector.on("onWhoIs", function () {

                self.realTimeConnector.invoke("AuthenticateListener").done(function (result) {

                    if (!result) {
                        console.log("Not authenticated, not allowed to see visitors");
                        return;
                    }

                    self.realTimeConnector.invoke("ShowAllVisitors").done();

                });

            });

            self.connection.start().done();

        },
        renderHtmlContactInfo: function (contactId) {

            var self = this;

            return self.stringFormat("<div><h4>{0}</h4>", contactId) +
                self.stringFormat("<p><a href='/sitecore/client/Applications/ExperienceProfile/contact?cid={0}'>Get detailed info</a></p>", contactId) +
                "</div></div>";

        },
        stringFormat: function () {
            var s = arguments[0];
            for (var i = 0; i < arguments.length - 1; i++) {
                var reg = new RegExp("\\{" + i + "\\}", "gm");
                s = s.replace(reg, arguments[i + 1]);
            }
            return s;
        }
    });

    return visitorsInRealtimePageCode;
});

Let me give you a quick explanation on how the script works. ๐Ÿ™‚
When the client is connected to the RealtimeHub(On the server side), the hub will make a client call to the WhoIs method. Right now nothing is done here but in the future we could log/store the “Listeners”. Next the AuthenticateListener method will be called(which is described in the beginning of the post) and when the “Listener” is authenticated it will make a server call to ShowAllVisitors(which is described in the beginning of the post). The ShowAllVisitors method will loop through the visitors and make a client call to onVisitorChanged. Method onVisitorChanged is also called when a new visitor connects.

In onVisitorChanged we will present the visitor on a map by using the GoogleMapSpeak control.

When the visitors session has ended, a client call will be made to onVisitorHasLeft. The visitor will be removed from the map by using the GoogleMapSpeak control.

Thatโ€™s all for now folks ๐Ÿ™‚

GoogleMap SPEAK component released in Sitecore Marketplace

GoogleMapSpeak

In my previous blog post, Make a Google Map SPEAK component in Sitecore, I was asked if I could move it into a Sitecore package and release it on the Sitecore Marketplace.

I managed to transform the blog post to a Sitecore module and released it on Sitecore Marketplace – GoogleMap SPEAK

What does it do?
It makes it easy to use Google Map in SPEAK.

How to use it?
1. Install the package.
2. Check out the sample js file and SPEAK page from the package:
/sitecore/shell/client/Applications/VisionsInCode/Layouts/VisitorsPageCode.js
/sitecore/client/VisionsInCode/Layouts/LatestVisitorsLayout

Read also blog post:
Make a Google Map SPEAK component in Sitecore

If you have questions, improvements, finding bugs โ€“ Please let me know.

Thatโ€™s all for now folks ๐Ÿ™‚

imageedit_7_4142219133

Make a Google Map SPEAK component in Sitecore

Map

I just love SPEAK! It has been around for a while but still feels new. I remember when SPEAK came, it felt very cool and fresh but it was so hard to get into. Back then I had
mostly been working with web forms(Sitecore), so the MVC concept was all new to me. Now when I’ve been working with MVC(Sitecore), SPEAK is so much easier to understand.

Here are some really good links to help you to understand how SPEAK works:
SPEAK – Sitecore documentation
Jakob Christensen’s great SPEAK videos
Martina Welander’s great SPEAK posts
Anders Laub’s great SPEAK posts

It’s always nice to present geographical data on a map so why not have it on the front page(Launchpad – “/sitecore/client/Applications/Launchpad”) combining it with Sitecore’s nice Timeline component and showing the latest visitors(like the visitors in “ExperienceProfile/Dashboard”).

This would be the mission for this post, I divided the work into following steps.
1. Create a custom map component
2. Create a sub page containing the custom map component and the Timeline component
3. Add the sub page to the front page (Launchpad – “/sitecore/client/Applications/Launchpad”)

The great posts from Mike Robbins – Add Experience Analytics Report To Launch Pad and Anders Laub – Add a SPEAK application to the Sitecore 8 Launchpad inspired and helped me a lot, thanks guys ๐Ÿ™‚

1. Create a custom map component

I started by looking on how Sitecore did their Business Components in “/sitecore/client/Business Component Library” and also pages like “/sitecore/client/Applications/Launchpad” and “/sitecore/client/Applications/ExperienceProfile/Contact”. I also read Pierre Derval posts about SPEAK which helped me a lot.

I decided to do a SPEAK 1.1 component, called GoogleMapSpeak1. I also wanted to have some map properties like height, width, map type etc.
Sitecore Rocks:
Rocks

The GoogleMapSpeak1 Paremeters.template:
Parameters
The Droplist points to the MapTypes folder.

Here is the rendering, GoogleMapSpeak1, with the “properties”(parameters):
GoogleMapRendering
I really tried to change/set the sort order on the parameters(properties) but it seems to sort in alphabetical order only ๐Ÿ˜ฆ

Project structure:
VS

The view – GoogleMapSpeak1.cshtml

@using Sitecore.Mvc
@using Sitecore.Mvc.Presentation
@using Sitecore.Web.UI.Controls.Common.UserControls
@model RenderingModel
@{
  var rendering = Html.Sitecore().Controls().GetUserControl(Model.Rendering);
  
  rendering.Class = "sc-GoogleMapSpeak1";
  rendering.Requires.Script("client", "GoogleMapSpeak1.js");
  
  rendering.SetAttribute("data-sc-height", rendering.GetString("Height", "Height", string.Empty));
  rendering.SetAttribute("data-sc-width", rendering.GetString("Width", "Width", string.Empty));
  rendering.SetAttribute("data-sc-latitude", rendering.GetString("Latitude", "Latitude", string.Empty));
  rendering.SetAttribute("data-sc-longitude", rendering.GetString("Longitude", "Longitude", string.Empty));
  rendering.SetAttribute("data-sc-zoom", rendering.GetString("Zoom", "Zoom", string.Empty));
  rendering.SetAttribute("data-sc-maptype", rendering.GetString("MapType", "MapType", string.Empty));
 
  var htmlAttributes = rendering.HtmlAttributes;
}
<div @htmlAttributes>
  <div id="map-canvas"></div>
</div>

I added the div for the map.

Now to the interesting part, the js file. In order to make Google Map work with RequireJS we need the require.async from the requirejs-plugins. The syntax to call the Google Map script:

async!http://maps.google.com/maps/api/js?v=3.exp&signed_in=true&sensor=false

GoogleMapSpeak1.js:

require.config({
    paths: {
        "async": "/SpeakComponents/require.async"
    }
});


define(["sitecore", "jquery", "async!http://maps.google.com/maps/api/js?v=3.exp&signed_in=true&sensor=false"], function (Sitecore, jQuery) {

    var model = Sitecore.Definitions.Models.ControlModel.extend({
        initialize: function (options) {
            this._super();

            this.set("width", null);
            this.set("height", null);
            this.set("latitude", null);
            this.set("longitude", null);
            this.set("zoom", null);
            this.set("mapType", null);
           
            this.set("markers", []);
            this.set("speakMap", null);


        },
        setupSpeakMap: function () {

            var mapDiv = jQuery("#map-canvas");

            mapDiv.css({ width: this.attributes["width"], height: this.attributes["height"] });

            var map = new google.maps.Map(document.getElementById('map-canvas'));

            this.set("speakMap", map);

        },
        showSpeakMap: function () {

            if (this.attributes["speakMap"] == null) {
                this.setupSpeakMap();
            }

            var map = this.attributes["speakMap"];

            if (this.attributes["latitude"] != null && this.attributes["longitude"] != null) {
                map.setCenter(new google.maps.LatLng(this.attributes["latitude"], this.attributes["longitude"]));
            }

            if (this.attributes["zoom"]) {
                map.setZoom(this.attributes["zoom"]);
            }

            if (this.attributes["mapType"]) {
                map.setMapTypeId(eval("google.maps.MapTypeId." + this.attributes["mapType"]));
            }

            this.set("speakMap", map);

        },
        addMarkerToMap: function (markerId, lat, lng, iconPath) {

            //Marker is already added to the map
            if (this.attributes["markers"][markerId] != undefined) {
                this.attributes["markers"][markerId].setMap(this.attributes["speakMap"]);
                return this.attributes["markers"][markerId];
            }
           
            var marker = new google.maps.Marker({
                position: new google.maps.LatLng(lat, lng),
                map: this.attributes["speakMap"],
                animation: google.maps.Animation.DROP
            });

            if (iconPath) {
                marker.setIcon(iconPath);
            }

            this.attributes["markers"][markerId] = marker;

            return marker;
        },

        removeMarkerFromMap: function (markerId) {

            var marker = this.attributes["markers"][markerId];

            //Marker is not on the map
            if (marker == null)
                return;

            marker.setMap(null);

            delete this.attributes["markers"][markerId];

        },
        showAllMarkersOnMap: function () {
            for (key in this.attributes["markers"]) {
                if (this.attributes["markers"].hasOwnProperty(key)) {
                    this.attributes["markers"][key].setMap(this.attributes["speakMap"]);
                }
            }
       
        },
        clearMarkersOnMap: function () {
            for (key in this.attributes["markers"]) {
                if (this.attributes["markers"].hasOwnProperty(key)) {
                    this.attributes["markers"][key].setMap(null);
                }
            }
        },
        deleteMarkers: function() {
            this.clearMarkersOnMap();
            this.attributes["markers"] = [];
        }
        
    


    });



    var view = Sitecore.Definitions.Views.ControlView.extend({
        initialize: function (options) {
            this._super();

            this.model.set("width", this.$el.data("sc-width"));
            this.model.set("height", this.$el.data("sc-height"));
            this.model.set("latitude", this.$el.data("sc-latitude"));
            this.model.set("longitude", this.$el.data("sc-longitude"));
            this.model.set("zoom", this.$el.data("sc-zoom"));
            this.model.set("mapType", this.$el.data("sc-maptype"));
           
            this.model.showSpeakMap();

        }
    });

    Sitecore.Factories.createComponent("GoogleMapSpeak1", model, view, ".sc-GoogleMapSpeak1");
});

I choose to show the map when the view initializes, by calling method showSpeakMap().

If you guys noticed I put the map object in the “speakMap” property. That means when you use/call the component from a page code, it will be something like this:

this.GoogleMapSpeak1.get("speakMap")

I also added some “marker methods” but they are not really necessary, the important one is the speakMap object.

The component is now done ๐Ÿ™‚

2. Create a sub page containing the custom map component

Lets test the map component. I wanted to place the map component on the frontapage(Launchpad) together with Sitecore’s nice Timeline component. Instead of adding them directly on the LaunchPad lets put them in a SubAppRenderer.

It was Mike Robbins post who gave me the idea.

First I need to create a SPEAK page(VisitorsLayout):
VisitorsLayout

One important thing. If you are going to render your speak page through the SubAppRenderer, you should not use placeholders and PageCode at all.

When you add SPEAK components to the layout of the common page, do not specify placeholders and do not add PageCode or structure components to the common page

Here is the VisitorsLayout in “design layout”:
DesignLayout
I’ve added a SubPageCode(the js file), the map component(GoogleMapSpeak1), timeline component(VisitorsTimeline) and a GenericDataProvider(VisitorsDataProvider) which is needed in order to get the visitor data.

The js file, VisitorsPageCode.js, is placed in the SubPageCode component. If you notice there is a providerHelper, it will be used to fetch/collect visitors data for the Timeline component. I could have created a controller for this, do an ajax call and so on but why should I do that when Sitecore has something that already is working.

define(["sitecore", "jquery", "/-/speak/v1/experienceprofile/DataProviderHelper.js"], function (sitecore, jQuery, providerHelper) {

    var infowindow = new google.maps.InfoWindow();

    var visitorsPageCode = sitecore.Definitions.App.extend({

        initialized: function () {

            this.GoogleMapSpeak1.addMarkerToMap(1,
                     this.GoogleMapSpeak1.get("latitude"),
                     this.GoogleMapSpeak1.get("longitude"),
                     "/sitecore/shell/Themes/Standard/Images/Sitecorelogo.png");

            this.setupTimeline();

        },
        setupTimeline: function () {

            var aggregatesPath = "/aggregates";
            var latestVisitorsTable = "latest-visitors";

            var url = this.stringFormat("/sitecore/api/ao/v1/{0}/{1}", aggregatesPath, latestVisitorsTable);

            providerHelper.initProvider(this.VisitorsDataProvider, latestVisitorsTable, url, null);

            providerHelper.setDefaultSorting(this.VisitorsDataProvider, "LatestVisitStartDateTime", true);
           
            var timelineData = {
                "dataSet": {
                    "journey": []
                }
            };

            providerHelper.getData(
                this.VisitorsDataProvider,
                jQuery.proxy(function (jsonData) {


                    jQuery.each(jsonData.data.dataSet, function (keyFirst, valueFirst) {
                        jQuery.each(valueFirst, function (key, value) {

                            timelineData.dataSet.journey.push({
                                "ContactId": value.ContactId,
                                "TimelineEventId": value.ContactId,
                                "EventType": "Quantifiable",
                                "ImageUrl": "contact.png",
                                "DateTime": value.LatestVisitStartDateTime,
                                "Duration": value.VisitCount,
                                "LatestVisitCityDisplayName": value.LatestVisitCountryDisplayName,
                                "LatestVisitCountryDisplayName": value.LatestVisitCountryDisplayName
                            });


                            //console.log(value);
                        });
                    });

                    this.VisitorsTimeline.set("data", timelineData);

                }, this)


              );

            this.VisitorsTimeline.on("change:selectedSegment", this.selectTimelineSegment, this);

        },
        selectTimelineSegment: function () {

            var self = this;

            var contactIconPath = "/sitecore/api/ao/v1/contacts/{0}/image?w=40&amp;h=40";

            this.getCoordinates(this.VisitorsTimeline.get("selectedSegment").LatestVisitCityDisplayName, function (coordinates) {

                self.GoogleMapSpeak1.clearMarkersOnMap();

                self.GoogleMapSpeak1.get("speakMap").setCenter(coordinates);

                var marker = self.GoogleMapSpeak1.addMarkerToMap(self.VisitorsTimeline.get("selectedSegment").ContactId,
                       coordinates.G,
                        coordinates.K,
                        self.stringFormat(contactIconPath, self.VisitorsTimeline.get("selectedSegment").ContactId));


                infowindow.setContent(self.renderHtmlContactInfo(self.VisitorsTimeline.get("selectedSegment")));
            
                marker.TimelineEventId = self.VisitorsTimeline.get("selectedSegment").TimelineEventId;

                infowindow.open(self.GoogleMapSpeak1.get("speakMap"), marker);
        
            });



        },
        renderHtmlContactInfo: function (timelineSegment) {
            return this.stringFormat("<div><h4>{0}</h4>", timelineSegment.ContactId) +
                this.stringFormat("<div>Number of visits: {0}", timelineSegment.Duration) +
                this.stringFormat("<p><a href='/sitecore/client/Applications/ExperienceProfile/contact?cid={0}'>Get detailed info</a></p>", timelineSegment.ContactId) +
                "</div></div>";

        },
        getCoordinates: function (city, callback) {
            var geocoder = new google.maps.Geocoder();

            var latlng;

            //Geocoding using google maps api.
            geocoder.geocode({ 'address': city }, function (results, status) {
                if (status === google.maps.GeocoderStatus.OK) {
                    latlng = results[0].geometry.location;
                }
                callback(latlng);

            });
        },
        stringFormat: function () {
            var s = arguments[0];
            for (var i = 0; i &lt; arguments.length - 1; i++) {
                var reg = new RegExp(&quot;\\{&quot; + i + &quot;\\}&quot;, &quot;gm&quot;);
                s = s.replace(reg, arguments[i + 1]);
            }
            return s;
        },
        generateGuid: function () {

            function s4() {
                return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
            }

            return (s4 + s4() + &quot;-&quot; + s4() + &quot;-4&quot; + s4().substr(0, 3) + &quot;-&quot; + s4() + &quot;-&quot; + s4() + s4() + s4()).toLowerCase();
        }
    });
    return visitorsPageCode;
});

When a Timeline segment is selected it will add a marker(with an infowindow) on the map. To get the coordinates I used Google’s geocoding api.

3. Add the sub page to the front page(Launchpad)

Finally we can now add VisitorsLayout, to the LaunchPad. First we need to locate the Launchpad:
LaunchPad

Lets take a look at it in the Design Layout.
LaunchpadVisual
I’ve added some Border components(VisitorHeaderWrap and VisitorBodyWrap), Text component(VisitorHeader) for the title and the SubAppRenderer component for including the SPEAK page – VisitorsLayout.

Here you can see how the datasource in the SubAppRenderer points to the SPEAK page – VisitorsLayout:
DatasourceSubApprenderer

Here is the final result:
imageedit_7_4142219133

Thatโ€™s all for now folks ๐Ÿ™‚