Getting started

This tutorial will teach you the following:

  • Retrieve the Metrological Application Framework

  • Writing your first app

  • Submitting your app to Metrological

And will assume the following:

  • Knowledge of Javascript

  • Basic understanding of Object Oriented Programming

For detailed information on any MAF3 Class, please refer to the Class Reference. For detailed information on designing your apps, please refer to the Design Guide.

Setup your work environment

First we have to retrieve the necessary files to start developing apps from our MAF-SDK Github repository. To clone them to your machine use the following commands in your terminal:

$ cd ~/To/Your/Desired/Location
$ git clone https://github.com/Metrological/maf3-sdk.git

After you received the source a node server has to be started. If you haven't installed node yet please refer to nodejs.org to see how to do this. After installing Node.js you can start the server by running the following commands:

$ cd ./maf3-sdk
$ npm install
$ node ./sdk.js

From now on you can access the server at localhost:8080 and start developing your apps. It is also possible to start the server to a port of your choice.

$ NODE_PORT=3333 node ./sdk.js

Filesystem

First application

At this point you are able to create your first application, but before you can start there are somethings that you need to know about the filesystem. The most important folders for you as app developer are the 'apps' and 'src' folders. The 'apps' folder contains all apps, including the app we are going to develop. The 'src' folder contains all the classes you can use for building your app. This source is open for anyone to read or to extend in your own app. Keep in mind that if you change these files the app won't work on our servers as those folders are not uploaded at the end of the proces. Therefore, please extend the classes inside your app if you want to modify them.

Now let's start with creating a folder for your app in the apps directory called 'com.CompanyName.app.AppName' with the following structure:

  • 📁 com.CompanyName.app.AppName
    • 📁 Contents
      • 📁 Images
      • 📁 Javascript
        • 📁 core
        • 📁 views
        • 📄 init.js
      • 📁 Localization
        • 📄 en-EU.strings
      • 📄 metadata.json

You can copy the EmptyTemplate or create one from scratch through the command line with the following lines:

cd apps/com.CompanyName.app.AppName
mkdir -p Contents/Images
mkdir -p Contents/Javascript
mkdir -p Contents/Localization
touch Contents/Localization/en-EU.strings
touch Contents/Javascript/init.js
touch Contents/metadata.json

The folders should be self-explanatory: the 'Images' folder contains all the images you want to use in the app, the 'Javascript' folder contains all Javascript files, in this folder we usually have two more folders to seperate views and core functionalities, called 'views' and 'core'. The folder is 'Localization' and containing all translation files used inside the app.

To start developing we have to register your app in the index.html located in the root of the maf3-sdk folder. The index.html contains an object containing various keys with value's for setting up the UI, language, plugins and of course the apps. To add your app to the index add 'com.CompanyName.app.AppName' to the apps array, like below:

MAE = {
//	ui: 'com.metrological.ui.AutoStart',
	ui: 'com.metrological.ui.Horizon',
	language: 'en',
	categories: [
		'favorites',
		'video',
		'news',
		'social',
		'games',
		'sport',
		'lifestyle'
	],
	apps: [
		'com.metrological.app.ButtonsTemplate',
		'com.metrological.app.GridTemplate',
		'com.metrological.app.TabsTemplate',
		'com.metrological.app.RSSTemplate',
		'com.metrological.app.VideoTemplate',
		'com.CompanyName.app.AppName'
	]
};

As you can see there are multiple uis to choose from, selecting AutoStart ui will cause the first app in the list to boot when you open your appstore. Selecting a provider's ui will show their appstore in your browser. After you added your app to the list you need to link the indentifier of the application's folder in the metadata.json which is found in the 'Contents' folder of the application.

Metadata

Metadata.json

When you open the metadata.json file the following information should be there:

{
	"identifier": "com.metrological.app.EmptyTemplate",
	"name": "Empty App Template",
	"version": "0.0.1",
	"author": "Metrological Widgets",
	"company": "Metrological Widgets",
	"copyright": "Metrological Widgets 2014",
	"description": "A clean App structure",
	"categories": [
		"video"
	],
	"scripts": "Javascript/init.js",
	"images": {
		"about": "Images/Icon.png",
		"icon": {
			"192x192": "Images/Icon.png"
		}
	}
}

Some info regarding the keys, from top to bottom:

The identifier key must contain a unique identifier, this identifier must be exactly the same as the folder name of your application.

The name and description keys contain the name and description that are displayed on the UI. The description should match the App, not the channel or company it is from. It also is important to keep the descriptions as short as possible (max 100 characters), this way it will fit in the reserved spaces. Longer texts might look good in your current UI, but remember that the layout of other UIs can be completely different, so a longer text could be truncated.

The version key indicates the version of the application. Make sure that when you upload and submit your application that you will have to change the version number as well.

In the author, company and copyright keys you can add the information users get to see when they open the about view of your application.

The scripts key indicates which script files the browser will load when you start the application. This can be either a single string or an array.

The category key indicates the category section your application belongs to for example; video, news, sport, games.

The images key contains keys for several images used througout the application. The options you have are:

  • header (normal and focused)
  • about
  • icon (in this sample 192x192)

Please keep in mind that the header and about images are never used in a fullscreen application, in this case you are free to leave these two options out of metadata file.

Sidebar or Fullscreen

To sidebar or not to sidebar...

Once you have set up your metadata.json you can start with the development of your application. The layout of your application depends on which kind of content you will be showing. For contextual or quick look applications for example; social media, and news we recommend using the sidebar view. For applications which require a custom environment to create a better user experience we recommend using the fullscreen view. You can find the proper dimensions for each specific view in the design guide.

In this example we will make use of the sidebar view. To create a new view you need to make a new file in the 'Javascript/views' folder, as this view will be our main view we'll call it 'MainView.js'. The view class has to look like this:

var MainView = new MAF.Class({
	ClassName: 'MainView',

	Extends: MAF.system.SidebarView,

	initialize: function () {
		this.parent();
	},

	createView: function () {

	},

	updateView: function () {

	}
});

The next step is to register your newly created view to your application. This is done by using the init.js file, this files is usually placed in the Javascript folder of your application. This file should look as followed:

include('Javascript/views/MainView.js');

MAF.application.init({
	views: [
		{ id: 'view-MainView', viewClass: MainView },
		{ id: 'view-About', viewClass: MAF.views.AboutBox },
	],
	defaultViewId: 'view-MainView',
	settingsViewId: 'view-About'
});

At the top of the init.js you can load files with include function, with a parameter which defines the path to the source code. Each view has to be added to the views array. Each view contains a id which is used as an identifier that you have to use to load that specific view, and a viewClass is the class of that view. The defaultViewId defines which view will load first, and which view to load when you press the home button at the top of the screen. The settingsViewId defines which view will load when you press the info button at the bottom of the screen, in our case you can use the default about view from the framework, this will show the information you entered in the metadata file.

When you have your metadata, main view, and init.js file setup you should be able to start your application through one of the user interfaces. When the application has loaded it should look like the image below.

Hello World

Filling the application

At this point can see that your view is empty, what we are missing now is a header and content. Let start with adding a header image. In the metadata file you already pointed out a path to your header, but the image itself is still missing. As the Design Guide suggests that the banner has to be something related to the application itself. The icon for you application has to be set in the same way, refer to the Design Guide for details for design specifications it.

Now we are going to add content to the application, you can start an element like a the text label, called with MAF.element.Text.

createView: function () {
	var view = this;
	new MAF.element.Text({
		label: $_('Hello World!'),
		styles:{
			width: view.width,
			height: view.height,
			fontSize: 60,
			anchorStyle: 'center'
		}
	}).appendTo(view);
},

With our new element you show the user a piece of text, but at this moment you have no way of changing either the label or its style. Thats why you have to alter your code.

createView: function () {
	var view = this;
	this.elements.ourText = new MAF.element.Text({
		label: $_('Hello World!'),
		styles:{
			width: view.width,
			height: view.height,
			fontSize: 60,
			anchorStyle: 'center'
		}
	}).appendTo(view);
},

The this refers to the class of the view we are on. In our framework you have the possibility to add items to the elements or to the controls. The difference between the two of them is that the MAF elements stored in controls can remember the state they have. So for example if you want the focus to return to the same button when you return to the view you need to create a controls component instead of an elements component. Also make sure that in these cases you add a guid: config value to the button and use the MAF.controls version instead of the MAF.elements one. Aside of the state there is another distinction between element and control components. The element components don't have any default styling, while the control components do. If you want to remember the state, but dont want any default styling add theme: false to the config of the component.

Conclusion
At this point we discussed the fundamentals of using the MAF-SDK. After this point the documentation will cover various MAF components using parts of the templates included in the MAF-SDK on Github. For detailed information on components and their functions please refer to the Class Reference.

Buttons and Events

Adding a button

Plain text can't be focused or pressed upon, so to add some interaction to your application you can add a button to change the text when the user has pressed this button. For the following article we partially use the ButtonsTemplate found in the MAF-SDK source on Github.

createView: function () {
		// Reference to the current view
		var view = this;

		// Create a Text element with translated label
		var textButtonLabel = new MAF.element.Text({
			label: $_('MAF.control.TextButton'),
			styles: {
				height: 40,
				width: 400,
				hOffset: (view.width - 400) / 2
			}
		}).appendTo(view);

		// Create a Text button with a select event
		var textButton = new MAF.control.TextButton({
			label: $_('TextButton'),
			styles: {
				width: textButtonLabel.width,
				height: 60,
				hOffset: textButtonLabel.hOffset,
				vOffset: textButtonLabel.outerHeight
			},
			textStyles: {
				anchorStyle: 'center'
			},
			events: {
				onSelect: function () {
					log('onSelect function TextButton');
				}
			}
		}).appendTo(view);

Placing a MAF.control.TextButton is just like placing a MAF.element.Text, first we initialize it before appending it to the view. Pressing the button will cause a log in our browser's console window. This is caused by the onSelect event we added in the events of the textButton.

You may have noticed that the code shows a variable at the beginning of our createView function. The reason for this is because the this object has a different meaning for events of an element. If the log would contain both view and this you could see that view returns the instance of our MAF.system.SidebarView while this returns the instance of MAF.control.TextButton. An alternative way is to use this.getView function to fetch the view. This only works if the button is really appended to the view or an element that is already appended to the view with the appendTo function.

Conclusion
MAF.control.TextButton is only one of a few Buttons. To see more examples and workings of buttons please look in the ButtonsTemplate found in the MAF-SDK source on Github.

Subscribing to events

Another way of performing actions on event is by using a listener. Listeners are called every time the event that you listen to is fired, thus able to execute your code. To listen to an event on a specific event you have to use the subscribeTo function. The first parameter is the instance of a MAF class to listen to. The second is the event to respond on, to listen to multiple events an array of strings can be used. The final parameter is the scope you can send along, if no scope parameter is added the scope is the object of the first parameter. With this way of listening it is possible to execute code on instances you didn't create yourself, for example the home button in a sidebarView. In the following example the MAF.application singleton is the instance to listen to.

blockHome: function (state) {
	if (state) {			
		this._homeHandler = this.onActivateHomeButtonPress.subscribeTo(MAF.application, 'onActivateHomeButton', this);
	} else {
		this._homeHandler.unsubscribeFrom(MAF.application, 'onActivateHomeButton');
	}		
},
onActivateHomeButtonPress: function(event) {		
	//block going back to home
	event.stop();
},

To see which event can be subscribed to please refer to the Class Reference

Loading a new View

Application lifecycle

While creating a new view a lot of functions are called, the following list should be used as a developing guide.

Function NameCalled when..What does it do..What to place in it..
initView Once, when view is created Initializes the basic objects on the view Register your global listeners and variables
createView Once, when view is created View is loaded in the DOM tree Create and append most -if not all- of your elements here
updateView Everytime the user visits the view View is visible to the user from here on Update your elements contents if it is dynamic and place listeners that are canceled in the hideView
selectView Everytime the user visits the view View is selected as currentview Remembered states are restored to their elements
focusView Everytime the user visits the view The focus on the view is placed If you want an other focus, do so here
unselectView Everytime the user leaves the view The application deselects the view as it's current Cancel listeners that are no longer necessary
hideView Everytime the user leaves the view The view is hidden from the user Cancel listeners that are no longer necessary
destroyView Once, when application is destroyed Removes all elements on the view If the application leaves memory leaks and/or pollution warnings in your console remove them here.

It is important to understand that when you switch views within your app the closing functions of the old view are run after the opening functions of the new view. The following flowchart shows the order of all functions. Also note that the destroy functions are only called on closing the whole application. If you use initView instead of initView (like we do in the examples) you will need to include this.parent function as the first line in your function, is order to call their parents function.

Application Lifecycle Flowchart

CreateView and UpdateView function

The createView and updateView function are called upon the first load of the view, the updateView function however will be called everytime a user visits the view whereas the createView function will not. The created view will stay in the DOM tree until the application is closed, while creating elements in the updateView function will create duplicate elements unless they are removed somewhere else. Let's create a new view to navigate to as shown below, this time we called it ElementGridView and is saved in the same folder as the MainView.

var GridView = new MAF.Class({
	ClassName: 'ElementGridView',

	Extends: MAF.system.SidebarView,

	initialize: function () {
		this.parent();
	},

	createView: function() {

	},

	updateView: function() {

	}
});

Now you have to include the new view you made to the init.js file, like we did below.

include('Javascript/views/MainView.js');
include('Javascript/views/ElementGridView.js');

MAF.application.init({
	views: [
		{ id: 'view-MainView', viewClass: MainView },
		{ id: 'view-ElementGridView', viewClass: ElementGridView },
		{ id: 'view-About', viewClass: MAF.views.AboutBox },
	],
	defaultViewId: 'view-MainView',
	settingsViewId: 'view-About'
});

To load your new view you have to make use of the loadView function. To demostrate this we take another button from the ButtonsTemplate.

var nextView = view.controls.nextView = new MAF.control.TextButton({
			guid: 'loadExampleView2',
			label: $_('ExampleView2'),
			styles: {
				width: textButtonLabel.width,
				height: textButton.height,
				hOffset: textButtonLabel.hOffset,
				vOffset: view.height - 100
			},
			textStyles:{
				hOffset: 10
			},
			content: [
				new MAF.element.Text({
					label: FontAwesome.get('chevron-right'), // Create a FontAwesome icon 
					styles: {
						height: 'inherit',
						width: 'inherit',
						hOffset: -10,
						anchorStyle: 'rightCenter'
					}
				})
			],
			events: {
				onSelect: function () {
					// Load next Example view with data
					MAF.application.loadView('view-ExampleView2', {
						myData: [1, 2, 3]
					});
				}
			}
		}).appendTo(view);

The most important thing here is the onSelect event on the MAF.control.TextButton. For this function you can add two parameters; first the id of the view you want to load, and a optional parameter which transports data from your current view to the new view you wish to load. More on this subject can be found at Passing data to the next or previous view. The template has the 'view-ExampleView2' string as first parameter, but we want to change that into 'ElementGridView' to match it with our setup.

Conclusion
Loading new views isn't the hardest thing to do, organising and using them in a good way is. For developers it is important to give views clear names. Dont use too many views because users will most likely get confused of all the different pages. Apps should be simple and powerful.

Grids

Setting up your grid

For the following article we use the GridTemplate found in the MAF-SDK source on Github. In our ElementGridView we will place a MAF.element.Grid and assign a cellCreator and cellUpdater function.

var ElementGridView = new MAF.Class({
	ClassName: 'ElementGridView',

	Extends: MAF.system.SidebarView,

	createView: function () {
		var view = this;

		var backButton = new MAF.control.BackButton({
			label: $_('BACK')
		}).appendTo(view);

		// In the ControlGridView.js example their is a guid, when guid is not needed
		// but the element needs to be accessed outside the create view function
		// you can reference elements in the view.elements object
		var elementGrid = view.elements.elementGrid = new MAF.element.Grid({
			rows: 2,
			columns: 2,
			styles: {
				width: view.width,
				height: view.height - backButton.outerHeight,
				vOffset: backButton.outerHeight
			},
			cellCreator: function () {
				var cell = new MAF.element.GridCell({
					styles: this.getCellDimensions(),
					events:{
						onSelect: function () {
							log('onSelect function GridCell', this.getCellIndex());
						},
						onFocus: function () {
							if (this.getCellIndex() === 1 || this.getCellIndex() === 2) {
								this.animate({
									backgroundImage: 'Images/focus.png',
									backgroundRepeat: 'repeat-x',
									duration: 0.3,
									scale: 1.2
								});
							} else {
								this.animate({
									backgroundColor: 'white',
									duration: 0.3,
									scale: 1.2
								});
								this.title.animate({
									duration: 0.3,
									color: 'black'
								});
							}
						},
						onBlur: function () {
							if (this.getCellIndex() === 1 || this.getCellIndex() === 2) {
								this.animate({
									backgroundImage: null,
									duration: 0.3,
									scale: 1.0
								});
							} else {
								this.animate({
									backgroundColor: null,
									duration: 0.3,
									scale: 1.0
								});
								this.title.animate({
									duration: 0.3,
									color: 'white'
								});
							}
						}
					}
				});

				cell.title = new MAF.element.Text({
					styles: {
						width: cell.width,
						height: cell.height,
						color: 'white',
						fontSize: 30,
						anchorStyle: 'center',
						wrap: true
					}
				}).appendTo(cell);

				return cell;
			},
			cellUpdater: function (cell, data) {
				cell.title.setText(data.title);
			}
		}).appendTo(view);
	},

	updateView: function () {
		var view = this;
		view.elements.elementGrid.changeDataset([
			{ title: $_('Cell1') },
			{ title: $_('Cell2') },
			{ title: $_('Cell3') },
			{ title: $_('Cell4') }
		], true);
	}
});

The creator is the function that is called to create the necessary DOM elements. The updater is called whenever the content of the cells needs to be changed, for example loading different/new page of the grid. We will also need an changeDataset function, this will add data to your grid. The function has two parameters to pass down, the first one is to send the new data to the grid, and the second one is to reset the page of the grid. To reset the paging and go back to page 1 you have to pass true. If you want to stay on the current page you have to pass false. For now we use a static array as data to fill our grid with and reset our grid.

Conclusion
MAF.element.Grid has many ways to implement. To see more examples and workings of grids please look in the GridTemplate found in the MAF-SDK source on Github.

Requests

Requesting data from external and local files or API's

When creating an application which uses requests to a certain API, you need to keep in mind that there is no fixed time on how long a data delivery might take. You will also need to check if you actually received data, or if any errors have occurred. Thats where Request comes in handy.

new Request({
	url: 'http://cdn.metrological.com/examples/alphabet.json',
	onSuccess: function (json) {
		console.log('succes', json);		
	},
	onFailure: function (error) {
		console.log('failure', error);
	},
	onError: function (error) {
		console.log('error', error);
	}
}).send();

This Request has three different event functions.

  1. onSucces, this function is called when the server returns state 200, after this it will send data through the chosen parameter name.
  2. onFailure, this function is called when the server returns something else than state 200.
  3. onError, this function is called when an error has occured during either the onSucces, or onFailure event.

To update your grid with a request you will have to add a changeDataset call to the onSucces event.

updateView: function () {
	var view = this;
	new Request({
		url: 'http://cdn.metrological.com/examples/alphabet.json',
		onSuccess: function (json) {
			view.elements.aGrid.changeDataset(json, true);
			console.log('success', json);
		},
		onFailure: function (error) {
			console.log('failure', error);
		},
		onError: function (error) {
			console.log('error', error);
		}
	}).send();
});

Split requests from views

If your application has multiple views which all use the same request function, we recommend that you use a seperate file. Usually we call that file 'API.js' and place it in the 'Javascript/core' folder. This way you can always call the Request on any view as long as it is within the scope of your application.

To further enhance this way of programming, we included a message system to our framework. With this message system you can store data under a given parameter, in our example we used the parameter alphabet, you can call upon this function using the MAF.messages.store function. Every time the given message changes the message system will send out a callback which will be caught by a specific function.

To visualize this way of data management we are going to look at the RSSTemplate

// Create a class and extended it from the MAF.system.SidebarView
var ListView = new MAF.Class({
	Extends: MAF.system.SidebarView,

	ClassName: 'ListView',

	initialize: function () {
		var view = this;
		view.parent();
		// Register MAF messages listener to view.dataHasChanged
		view.registerMessageCenterListenerCallback(view.dataHasChanged);
	},

	// Create your dataHasChanged function
	dataHasChanged: function (event) {
		var view = this,
			controls = view.controls;
		if (event.payload.key === 'MyFeedData') {
			if (event.payload.value.length > 0) {
				controls.grid.changeDataset(event.payload.value, true);
				controls.grid.visible = true;
				controls.grid.focus();
			} else {
				controls.grid.visible = false;
			}
		}
	},

	// Create your view template
	createView: function () {
		// Reference to the current view
		var view = this;

		// Create a Grid, by adding it into the view.controls object and
		// setting guid focus will be remembered when returning to the view 
		var controlGrid = view.controls.grid = new MAF.control.Grid({
			rows: 6,
			columns: 1,
			guid: 'myControlGrid',
			orientation: 'vertical',
			styles: {
				width: view.width - 20,
				height: view.height,
				visible: false
			},
			cellCreator: function () {
				// Create cells for the grid
				var cell = new MAF.control.GridCell({
					styles: this.getCellDimensions(),
					events: {
						onSelect: function () {
							// Load the ItemView when a cell is selected
							MAF.application.loadView('view-ItemView', {
								item: this.getCellDataItem()
							});
						}
					}
				});

				cell.title = new MAF.element.Text({
					visibleLines: 2,
					styles: {
						fontSize: 24,
						width: cell.width - 20,
						hOffset: 10,
						vOffset: 40,
						wrap: true,
						truncation: 'end'
					}
				}).appendTo(cell);

				return cell;
			},
			cellUpdater: function (cell, data) {
				// Update cell when change dataset has been called
				cell.title.setText(data.title);
			}
		}).appendTo(view);

		// Create a scrolling indicator next to the grid to
		// indicate the position of the grid and ake it possible
		// to quickly skip between the pages of the grid
		var scrollIndicator = new MAF.control.ScrollIndicator({
			styles: {
				height: controlGrid.height,
				width: 20,
				hOffset: view.width - 20
			}
		}).appendTo(view);

		// Attach the scrollIndicator to the grid with news items
		scrollIndicator.attachToSource(controlGrid);
	},

	// When view is created or returning to view the view is updated
	updateView: function () {
		var view = this;
		if (view.backParams.reset !== false && !MAF.messages.exists('MyFeedData'))
			getData();
	},

	// When closing the application make sure you unreference your objects and arrays
	// and clean the messages that the app has stored
	destroyView: function () {
		MAF.messages.reset();
	}
});

In the code sample above we switched our grids changeDataset with the function for storing messages which works as followed; you call the function with the MAF.messages.store function, this function uses two parameters, the key defines the value you are going to listen to, and value contains the data you requested from the server.

Conclusion
Using Request is most of the times a vital point in your application. Please refrain to load unused data and try to optimize your api calls if possible. This will greatly boost your app's performance. We took a quick peek on how to store data, more on that subject can be found at Storing Data.

Theming

Start painting the canvas

Right now your application looks very basic, but there are ways to customize the style of your application. There are three ways to change the style of your application:

  1. When creating a new element, for setting the elements default value when the view is loaded.
  2. elementName.setStyle(key, value) and elementName.setStyles({key1: value1, key2: value, etc..}), both basicly do the same, however one setStyle only allows you to change one attribute and the other setStyles allows you to change multiple attributes of an element. These functions allow you to change your element while the application is running.
  3. And the use of Theming

Because you already seen the first two in the tutorial before, we will cover the third subject Theming. With this you can create a set of styles, like setting the applications font or focus color. The are two ways of using theming, adding the code to your init.js (or the view where it belongs to), or make a new javascript file. We recommend using the second method for large Themes this way you keep your init.js file nice and clean.

To start theming create a file called 'theme.js' and save it in the 'Javascript/core' folder and include it in your init.js file.

Theme.set({
	BaseFocus: {
		styles: {
			backgroundColor: '#0198E1'
		}
	}
});

Several elements we used up until now have been from the controls type, these types will generate a DOM element with the class BaseGlow, which defines the default background. Once the element has focus the class will change from BaseGlow to BaseFocus. To disable this automatic theming on an element you can use the configuration variable theme which we have already seen once before, the correct usage of this variable is shown in the example below.

this.controls.ourTextButton = new MAF.control.TextButton({
	label: $_('Bye!'),
	theme: false,
	styles: {

When you start your application at this point you can see that the black background on the buttons has disappeared and there seems to be no focus color when you try to focus these buttons. In the following example we will show you a way to only change the style of the control buttons.

Theme.set({
	view: {
		styles: {
			backgroundColor: '#2F2F80'
		}
	},
	BaseFocus: {
		styles: {
			backgroundColor: '#0198E1'
		}
	},
	ControlTextButton: {
		normal: {
			styles: {
				color: '#F1F1F1',
				backgroundColor: '#5F5DFF'
			}
		},
		focused: {
			styles: {
				backgroundColor: '#cf7458'
			}
		}
	}
});	

When you look at the example above you can see we added a new class called ControlTextButton this points directly to the DOM element of a MAF.control.TextButton. From now on it will use this way of styling.

Deeper into the paint

Using subsets like normal and focused enables to specify a class deeper. The focused color CSS will be generated as .ControlTextButton.focused {}, this means that it is possible to create your own subsets if you append new Classes to your elements with ClassName as config or on the fly with the yourInstance.element.addClass and yourInstance.element.removeClass function. It is also possible to go deeper with the css, using a string as indentifier like 'ControlTextButton .FancyTextField', this way you target the FancyTextFields located in ControlTextButtons, the normal CSS rules (>, +, :before, :after, :first-child) can be used here. An important thing to know, is that some of the newer (CSS3) properties, like textShadow, are not supported on every settop box. When you make use of these properties they can be filtered on the settop box that is lacking that functionality. An example of using the normal CSS rules would look like this, notice that the first dot is skipped as it is generated by MAF, thus only accepting class names as the first node not ids.

Theme.set({
	'dialog > .frame > .text':{
		styles: {
			borderBottom: '0 !important',
			paddingLeft: '30px !important',
			paddingRight: '30px !important'
		}
	},
	'AboutBoxView .AboutBoxViewMetadataAuthorNote': {
		styles: {
			vOffset: '350px !important',
			hOffset: '130px !important'
		}
	},
	'ControlTextButton.redButton': {
		styles: {
			backgroundColor: 'red'
		}
	},
	'ControlTextButton.greenButton': {
		styles: {
			backgroundColor: 'green'
		}
	}
}); 
this.controls.ourTextButton = new MAF.control.TextButton({
	label: $_('Bye!'),
	events:{
		onSelect: function(){
			if (this.text === $_('Bye!')) {
				this.setText($_('Hello!'));
				this.element.addClass('redButton');
				this.element.removeClass('greenButton');
			} else {
				this.setText($_('Bye!'));
				this.element.addClass('greenButton');
				this.element.removeClass('redButton');
			}
		}
	}
}).appendTo(this);

Font Awesome

A couple of the shown examples already used the Font Awesome wrapper. Font Awesome is, just like the name implies, a font which contains a lot of modern day icons. These icons may not be the exact icons you would like to use but they still do their job. Using the Font Awesome wrapper instead of images speeds up the loading time of your app drastically, which results in a better flow. All of Font Awesome's icons and Font Awesome's options, like enlarging or stacking, are possible to add to your textlabel as shown in the following example.

_cellCreator: function () {
	var cell = new MAF.control.GridCell({
		events: {
			onSelect: function () {
				if (cell.retrieve('checked')) {
					cell.checkbox.setText(FontAwesome.get( 'stack', ['square-o', 'stack-2x'] ));
					cell.store('checked', false);
				} else {
					cell.checkbox.setText(FontAwesome.get( 'stack', ['square-o', 'stack-2x'], ['check', 'stack-1x'] ));
					cell.store('checked', true);
				}
			}
		}
	});

	cell.checkbox = new MAF.element.Text({
		label: FontAwesome.get( 'stack', ['square-o', 'stack-2x'] ),
		styles: {
			width: 40,
			vOffset: 10,
			hOffset: 25,
			color: '#c1b8a7'
		}
	}).appendTo(cell);

	cell.label = new MAF.element.Text({
		label: FontAwesome.get( 'camera-retro' ) + ' really nice picture of you',
		styles: {
			width: 400,
			vOffset: 10,
			hOffset: cell.checkbox.outerWidth,
			color: '#c1b8a7'
		}
	}).appendTo(cell);

	return cell;
},

Playing media

Its a TV app after all

Applications can display more than just text. It is also possible to load in audio and video to show to your users. To play media we make use of MAF.mediaplayer, this handles all events needed to play videos or music. Basic events such as play, stop, forward, rewind, and skipping numbers are handled by default. This way you are not enforced to configure a lot of things before you can play a video. You've probably noticed the checkered background which illustrates the area where you would normally see TV or your video, this background will be hidden on a settopbox. The following example can be found in the VideoTemplate, but is edited for the sake of this example.

// Create a class and extended it from the MAF.system.FullscreenView
var transportOverlay = new MAF.Class({
	Extends: MAF.system.FullscreenView,

	ClassName: 'transportOverlay',

	// Add back params when going to the previous view
	viewBackParams: {
		reset: false
	},

	// Initialize your view
	initialize: function () {
		this.parent(); // Call super class constructor
		MAF.mediaplayer.init(); // Initialize mediaplayer
	},

	// Create your view template
	createView: function () {
		// Reference to the current view
		var view = this,
			// Create the Media Transport Overlay 
			mediaTransportOverlay = new MAF.control.MediaTransportOverlay({
				theme: false,
				buttonOrder: ['rewindButton', 'playButton', 'stopButton', 'forwardButton'], // Set the order of the buttons
				buttonOffset: 20, // Set the default space before and after the buttons
				buttonSpacing: 20, // Set the space between the buttons
				fadeTimeout: 6, // Set the fader of the overlay to start after 6 seconds
				playButton: true, // Enable the "play" button
				stopButton: false, // Disable the "stop" button
				rewindButton: true, // Enable the "rewind" button
				forwardButton: true // Enable the "fast forward" button
			}).appendTo(view);
	},

	gotKeyPress: function (event) {
		if (event.payload.key === 'stop')
			MAF.application.previousView();
	},

	// The channelChanged function is called when you change the channel of your TV 
	onChannelChanged: function () {
		MAF.application.previousView();
	},

	// When view is created or returning to view the view is updated
	updateView: function () {
		// Reference to the current view
		var view = this;

		view.onChannelChanged.subscribeTo(MAF.mediaplayer, 'onChannelChange');
		view.gotKeyPress.subscribeTo(MAF.application, 'onWidgetKeyPress');

		// Add a new playlist with the video to the player
		var entry = new MAF.media.PlaylistEntry({
			url: 'http://video.metrological.com/aquarium.mp4',
			asset: new MAF.media.Asset('Aquarium')
		});
		MAF.mediaplayer.playlist.set(new MAF.media.Playlist().addEntry(entry));
		// Start the video playback
		MAF.mediaplayer.playlist.start();
	},

	// The hideView is called when you're leaving this view
	hideView: function () {
		// Reference to the current view
		var view = this;
		view.onChannelChanged.unsubscribeFrom(MAF.mediaplayer, 'onChannelChange');
		view.gotKeyPress.unsubscribeFrom(MAF.application, 'onWidgetKeyPress');
	}
});
  1. We built up our standard class or view for with the usual functions initialize, createView, and updateView.
  2. Then in the initialize function we also initialized our mediaplayer with the function MAF.mediaplayer.init. Without the initialization our MAF.mediaplayer will not work.
  3. In the createView we added a MAF.control.MediaTransportOverlay, this is a default player with multiple buttons, time labels, and a time bar. The forwardseekbutton and backwardseetButton are disabled by default, so you will have to enable them with their configuration variables.
  4. We wanted to start our video when loading our view, to do this you have to add some code to the updateView function. You have to create a playlist first, which you can retrieve with MAF.media.Playlist. This time we wanted to play a video through use of an URL, to do so you use the addEntryByURL function, with this function you give the URL as a string parameter, or create your own MAF.media.PlaylistEntry and add it with the addEntry function.
  5. Now you can set the playlist for the mediaplayer with the MAF.mediaplayer.playlist.set function, and start the mediaplayer with MAF.mediaplayer.playlist.start function.
  6. In the case a user presses the stop button or an error occurs, we wanted the application go back to the previous view. To do this you have to create a function that will handle events in case the mediaplayer changes its state. We called the function handleMediaEvents, to make this function listen to state changes you have to add a call in the initialize function of the view. In this case you have the function handleMediaEvents call the subscribeTo function, with this function you give two parameters, the listener, and the event that occurs. In your case MAF.mediaplayer and onStateChange.
Conclusion
MAF.mediaplayer is an essential component in the MAF-SDK, the VideoTemplate hold various ways of implementing this mediaplayer. For more information on MAF.mediaplayer please look in the Class Reference.

Storing data

Passing data to the next or previous view.

The easiest way of passing data to our next view is with the use of the persist and backParams. These objects can be accessed as a sub object from the view you are on. If you are not able to reach the view with this you will have to fetch it before you are able to call for the persist and backParams.

createView: function () {
	console.log(this.persist);
	console.log(this.backParams);
}

The persist is used to pass along data to the next view with the loadView function or renew your persist with the reloadView function. As long as a view remains in the history its persist data remains in tact.

MAF.application.loadView('GridView', { 'name':'Bob', 'age':19 })
MAF.application.reloadView({ 'name':'Bob', 'age':19 })

As the name suggest the backParams are there once you go back to a previous view. Maybe one of your views fetches new data from an API on every updateView function call. But that might change the data that was already on that view, which you might not want at that moment. That is where the backParams come into our application.

MAF.application.previousView({ 'noReset':true })

If you place an if statement like if(this.backParams.noReset) in your updateView function you can blockout renewing data if you come back from a previous view. If you make use of your physical backspace button or MAF.control.BackButton we encourage you to add the viewBackParams object to your view class. These buttons will send this object as backParams to the previous view.

viewBackParams: {
	noReset: true
},

It is also possible to mutate your persist and backParams like any other object.

Temporary storing in MAF.messages

While using the Request to fill our MAF.element.Grid we have already used MAF.messages to work with data. Through storing and fetching data it is possible to share information between views. You can of course use the persist object for sending data to the next view, but storing your information in the messages system is convenient when sharing the same object to multiple views that do not use a linear flow or for information that has to be accessible everywhere.

var exampleObject = { 'name':'John Doe', age:'26', work:'salesman' };
		MAF.messages.store("currentUser", exampleObject);

From now on there is an object called currentUser ready at your disposal. The object is now ready everywhere you want to call it. It doesn't matter what scope it is where you want to call it because MAF.messages is accessible everywhere. To check which content is in the object while developing you can use the following line

console.log(MAF.messages.fetch("currentUser"));

If you added a listener to MAF.messages (see requests) it uses the name of an object as key and the object itself as value, which can both be aquired from event.payload.

There are a few more interesting functions for MAF.messages so be sure to check them in the Class Reference so you dont miss out on them.

Temporary storing in MAF elements

A third method to store data is on an element itself, for example a Button or a GridCell. This way you can retrieve the data without having to address another element or the parent view. It is very useful if you want to access data through use of events that are linked to an element, or when you loop through a number elements. To store data into an element use the store function on the element itself, to get data from the element use the retrieve function. A downside of this method is that the data is only available as long as the element is available. Once it is gone, so is your data.

_cellCreator: function () {
	var cell = new MAF.control.GridCell({
		events: {
			onSelect: function () {
				if (cell.retrieve('checked')) {
					cell.checkbox.setText(FontAwesome.get( 'stack', ['square-o', 'stack-2x'] ));
					cell.store('checked', false);
				} else {
					cell.checkbox.setText(FontAwesome.get( 'stack', ['square-o', 'stack-2x'], ['check', 'stack-1x'] ));
					cell.store('checked', true);
				}
			}
		}
	});

	cell.checkbox = new MAF.element.Text({
		label: FontAwesome.get( 'stack', ['square-o', 'stack-2x'] ),
		styles: {
			width: 40,
			vOffset: 10,
			hOffset: 25,
			color: '#c1b8a7'
		}
	}).appendTo(cell);
	return cell;
},

Permanent storing in the app

There are two ways of permanent storing, currentAppConfig for storing at application level and currentAppData for storing data at profile per application level. Both have a set and get function to store and retrieve data with. Everything stored in these objects will be accessible until your browser data is cleared, or a factory reset on a set-top box. Some data belongs to a user and some data belongs to the application itself, so give it a thought before using it. Overall highscores for example need to be stored in the currentAppConfig, if you store it in the currentAppData and another profile is switched the other person cannot view those highscores. Personal records however could be stored within currentAppData, depending on if the other profiles should have access or not, if you want them to access the other user's personal records, store them into the currentAppConfig instead. As the name suggests currentAppConfig has its origins in storing application settings, like for example whether to use Fahrenheit or Degrees.

storeHighscores: function (scores) {
			currentAppConfig.set('Highscores', scores);
		},

		updateView: function () {
			updateSidebar(true);
			var scores = currentAppConfig.get('Highscores') || [];
			if (scores.length > 0) {
				this.elements.noScores.visible = false;
				this.elements.grid.changeDataset(scores, true);
				this.elements.grid.visible = true;
				this.elements.pageIndicator.visible = true;
			} else {
				this.elements.grid.visible = false;
				this.elements.pageIndicator.visible = false;
				this.elements.noScores.visible = true;
			}
		}

Social Media

Twitter

The Twitter object is a wrapper which simplifies calls to the Twitter api and handles the authentication for you. This means that we don't have any control of the inner workings of the api itself. The api function makes use of the paths specified by Twitter (ex. 'statuses/home_timeline', 'statuses/show/:id' and 'search/tweets'). When the user is not yet authenticated MAF launches the login sequence instead of doing the actual call.

To fetch the authenticated user's profile information like name and profile picture the code looks like this.

Twitter.api('me', function (me) {
	// this trigger not caused by user not authorized.
	currentAppData.set("myProfile", me);
});

If you wonder how to place a tweet, the following function can be helpful.

function postTweet(tweetMsg, status_id) {
	var parameters = (status_id) ? { in_reply_to_status_id: status_id,status: tweetMsg }: { status: tweetMsg };
	Twitter.api('statuses/update', 'post', parameters, function (result) {
		// this trigger not caused by user not authorized.
		MAF.messages.store('tweeted', result);
	});
};

It is also possible to add listeners to the Twitter object, which fires the onConnected, onDisconnected and onUnpairedProfile event. This way you can take action when a user is connected, disconnected or unpaired.

var HomeView = new MAF.Class({
	Extends: MAF.system.SidebarView,
	ClassName: 'HomeView',
	initialize: function() {
		this.parent();
		this.twitterSubscribe = this.twitterEvents.subscribeTo(Twitter, ['onUnpairedProfile', 'onConnected', 'onDisconnected'], this);
	},

	twitterEvents: function (event) {
		var v = this;
		switch (event.type) {
			case 'onUnpairedProfile':
				//Do something
				break;
			case 'onDisconnected':
				//Do something
				break;
			case 'onConnected':
				//Do something
				break;
		}
	},

	destroyView: function() {
		if (this.twitterSubscribe) {
			this.twitterSubscribe.unsubscribeFrom(Twitter, ['onUnpairedProfile', 'onConnected', 'onDisconnected']);
		}
		delete this.twitterSubscribe;
	}
};

More on the Twitter object can be found in the Class Reference

Facebook

The Facebook object is,like Twitter, a wrapper around the original Facebook Graph api and works the same as the Twitter object. Though the endpoints passed down the api function may be different, as the wrapper does not change these.

Facebook.api('me', function (me) {
	// this trigger not caused by user not authorized.
	currentAppData.set("myProfile", me);
});

Placing posts for example is with a different endpoint then with the Twitter object.

function postMessage(message) {
	Facebook.api(Facebook.userId+'/feed', 'post', { 'message': message }, function (result) {
		// this trigger not caused by user not authorized.
		MAF.messages.store('messagedPosted', result);
	});
};

But just like Twitter you can listen to the same events by subscribing to the Facebook object.

var HomeView = new MAF.Class({
	Extends: MAF.system.SidebarView,
	ClassName: 'HomeView',
	initialize: function() {
		this.parent();
		this.facebookSubscribe = this.facebookEvents.subscribeTo(Facebook, ['onUnpairedProfile', 'onConnected', 'onDisconnected'], this);
	},

	facebookEvents: function (event) {
		var v = this;
		switch (event.type) {
			case 'onUnpairedProfile':
				//Do something
				break;
			case 'onDisconnected':
				//Do something
				break;
			case 'onConnected':
				//Do something
				break;
		}
	},
	destroyView: function() {
		if (this.facebookSubscribe) {
			this.facebookSubscribe.unsubscribeFrom(Facebook, ['onUnpairedProfile', 'onConnected', 'onDisconnected']);
		}
		delete this.facebookSubscribe;
	}
};

More on the Facebook object can be found in the Class Reference

Youtube

The Youtube object is actually the most simple of the three, but if you don't know how the MAF.mediaplayer works yet please check the getting started page or the SDK page. The mediaplayer needs a video to play, but instead of adding the youtube url as item we will make use of the Youtube object.

function playYoutubeVideo(videoID){
	YouTube.get(videoID, function (config) {
		MAF.mediaplayer.playlist.set((new MAF.media.Playlist()).addEntry(new MAF.media.PlaylistEntry(config)));
		MAF.mediaplayer.playlist.start();
	}, this);
}

The get function takes one parameter, which is the video's id as a String format. The video id can be found at the end of the Youtube link you want to use (example, https://www.youtube.com/watch?v=XxXxXxXxXxX). The function returns an object that has the correct items to initialize a PlaylistEntry which can be used as an entry to fill the Playlist with. Detailed information on the Youtube object can be found in the Class Reference

Communicating

Start communicating

External devices can be used as a way to enrich the way of controlling your app, for example, by turning your smartphone into a gamepad or swipe remote. By communicating with other MAF systems you could create a chat for those watching the same movie, make multiplayer games or create a shared MAF.media.Playlist that gets updated when a friend adds another video. With the use of MAF.Room it is possible to do these communications. When you create a MAF.Room in your app, it will try to join it if the name already exists on our server or else make a new one. Rooms are bound to the app itself, so it wont be possible to access another app's MAF.Room.

Use your smartphone as remote

Some functionality cannot be achieved with just the use of a default STB/TV remote. Most of these remotes work with infrared signals, meaning that you cannot press more than one button at a time. This could be a problem for playing some games, walking and shooting cannot be done at the same time. Aside of the button problem, your smartphone hold many other functionalities you can make use of, for example its touchscreen to draw with. This example can be found at the Rooms example on Github.

// Create a class and extended it from the MAF.system.SidebarView
var Room = new MAF.Class({
	ClassName: 'Room',

	Extends: MAF.system.FullscreenView,

	initialize: function () {
		var view = this;
		view.parent();
		// Create a Room across all households
//		view.room = new MAF.Room(view.ClassName);
		// Create a Room for this specific household
		view.room = new MAF.PrivateRoom(view.ClassName);
	},

	// Create your view template
	createView: function () {
		var view = this,
			room = view.room,
			clients = {}; // Keep track of the clients connected
...
		// Set listeners for Room and Connection
		(function (event) {
			var payload = event.payload;
			switch (event.type) {
				case 'onConnected':
					log('room connected');
					// If connected but room not joined make sure to join it automaticly
					if (!room.joined) room.join();
					return;
				case 'onDisconnected':
					clients = {}; // Reset clients
					log('connection lost waiting for reconnect and automaticly rejoin');
					return;
				case 'onCreated':
					// Create an url to the client application and pass the hash as querystring
					var url = widget.getUrl('Client/draw.html?hash=' + payload.hash);
					qrcode.setSource(QRCode.get(url));
					log('room created', payload.hash, url);
					return;
				case 'onDestroyed':
					clients = {}; // Reset clients
					log('room destroyed', payload.hash);
					return;
				case 'onJoined':
					// If user is not the app then log the user
					if (payload.user !== room.user)
						log('user joined', payload.user);
					return;
				case 'onHasLeft':
					// If user is not the app then log the user
					if (payload.user !== room.user)
						log('user has left', payload.user);
					return;
				case 'onData':
					var data = payload.data;
					if (data.e === 'draw')
						return draw(data.c, data.k, data.x, data.y);
					if (data.e === 'clear')
						return reset();
					break;
				default:
					log(event.type, payload);
					break;
			}
		}).subscribeTo(room, ['onConnected', 'onDisconnected', 'onCreated', 'onDestroyed', 'onJoined', 'onHasLeft', 'onData', 'onError']);

		// If Room socket is connected create and join room
		if (room.connected) room.join();
	},

	destroyView: function () {
		var view = this;
		if (view.room) {
			view.room.leave(); // Leave room, will trigger an onLeaved of the app user
			view.room.destroy(); // Destroy the room
			delete view.room; // Unreference from view for GC
		}
	}
});

To be able to use your smartphone we will need some kind of way of linking it with MAF. First we need to create a MAF.Room or MAF.PrivateRoom to log into, the difference between these two that the MAF.PrivateRoom is designed to work within one household, whereas the normal MAF.Room can be used to create cross household connections. For now we use a MAF.PrivateRoom because we are going to link a smartphone as a remote to an app, no other people from outside this household will benefit from joining this room. Once the MAF.PrivateRoom is created we will need some way to let the device join the MAF.PrivateRoom, we are going to do that with a QRCode. To generate the QRCode we can use the get function to pass down the url of the page you want to show on your device, including the room's hash. To get the path to the html page use the getUrl function of the widget instance to get the app's path and add your own html file's path to it. This file will be responsible to send some kind of data back to the app. in our case it will be coordinates of lines that need to be drawn on the app's canvas. In order to do that maf-room.min.js has to be included in the device's html. Without it it cannot be used to send data back to the servers.

var Draw = (function (c) {
		var enabled = false,
			room = new MAF.Room();

		room.addEventListener('joined', function (event) {
			// A client has joined
			console.log('user joined', event.user);
		});

		room.addEventListener('data', function (event) {
			var d = event.data,
				fn = Canvas[d.e];
			return fn && fn(d.c, d.k, d.x, d.y);
		});

		window.addEventListener('unload', function (event) {
			room.destroy();
			room = null;
		}, false);

		function start(x, y) {
			enabled = true;
			room.send({ e: 'draw', k: 'start', c: c, x: x, y: y });
		}
		function end(x, y) {
			enabled = false;
			room.send({ e: 'draw', k: 'end', c: c, x: x, y: y });
		}
		function paint(x, y) {
			if (enabled) room.send({ e: 'draw', k: 'paint', c: c, x: x, y: y });
		}
		return {
			start: start,
			end: end,
			paint: paint
		}
	}(getRandomColor()));

To send data back use the send function of MAF.Room that is accessible through the included script. The subscribeTo in your app will be fired with onData which gives the oppertunity to work with the data, in this case update our canvas in the app to show the drawing that is made on the smartphone. Another way of using your smartphone as remote is by turning it into a swipe remote, a gamepad or a image gallery. The possibilities are almost endless as you can use html to build your remote with.

Closing your app

Leave no garbage behind

While developing it is also important to know what happens when you close your application. MAF takes care of closing the application and cleaning most objects, but sometimes unexpected things can happen like warning logs or even crashes. If it happens it most likely is one of the following causes:

Warnings

  • Global scope pollution
  • Memory leaks

Code ran after the app is closed by:

  • Listeners
  • Timers
  • Events

Most of these errors and warnings can be fixed in the destroyView function of the view, but some Global scope pollution warning not appear in the first place. When the error occurs it means that the application has a variable declared outside of the application's scope. This means that if another application makes use of the same global variablename it uses the previous application's data. That is why all variables cannot go outside of the view's scope. Most of the time this goes by accident, if you forget to store your variable to your local scope (this.varName) or forget the var declaration statement before your variable name when you initialize it, the variable will be out in the global scope. It is also possible that you wanted to declare it global on purpose, when you like to share content with other views for example, there are more useful methods available.

When MAF cannot clear all of your data memory leaks occur. If multiple applications with memory leaks are opened and closed the settop box will crash by a flooded memory. Thats why it is important to empty and delete all variable that MAF cannot destroy for you. Application crashes when closing the application are most likely caused by code that is executed after MAF cleared references to the used object, so if you made your own button that closes the app, be sure that there is no other code ran after the close command. It may also be possible that event code (like onDatasetChanged on a Grid), listener code (like onConnected on the Twitter object) or timer code (setInterval, defer) is ran after the application closed. Thats why it is important to stop timers and listeners in the destroyView function and check on existances in event codeblocks. An example of an application clearing it's garbage looks like this:

destroyView: function() {
	this.varOne = this.varTwo = this.varThree = null;
	delete this.varOne;
	delete this.varTwo;
	delete this.varThree;
	if (this.stateChange) {
		this.stateChange.unsubscribeFrom(MAF.mediaplayer, 'onStateChange');
		delete this.stateChange;
	}
}

If no warnings or errors pop in your console log the application closes correctly and you have nothing to worry about. But make it a habit of always stopping timers and listeners as settop boxes most likely run slower than your developing machine, thus might run other code after the application is closed instead of just in time.

External API's

Why build what is already made?

MAF-SDK has installed a few javascript libraries for you to work with, these work the same as described on their own websites.