THE BLOG

News, tips and tricks from 2Gears

Model Associations in Sencha Touch (and ExtJS) made easy

Posted · 12 Comments
Model associations feature 150

Using Model associations in Sencha Touch and ExtJS components can be tricky. Especially when loading store data from a REST backend the associations that are defined on the model are not instantiated and usable in components. This article demonstrates the use of our BaseModel class which fixes this problem and makes using model associations a breeze.

Data Layer

When designing a robust mobile application, the first thing that needs to be taken care of is the data layer. The data on which any application works is the foundation on which everything is built. Like any foundation, when it’s weak, the whole structure that is built on top of it will be weak as well. Luckily the data layer of Sencha Touch (and ExtJS) allows us to structure the application’s data very robustly. There are however a couple of things that can be a bit involved when working with associated data in Sencha Touch and ExtJS. This means that, in a typical application, the application’s controllers handle most model relationship functionalities. We think model associations are a crucial part of the data layer and should be transparently handled by the Model layer itself once it is properly set up.

The main problem with model associations is that they are not treated as first class citizens when the application’s stores load their data asynchronously from a REST backend. Our first article on “Using Model Associations in Sencha Touch and ExtJS” explained the problem and demonstrated how some these problems can be solved. We will skip the basics of Model Associations in Sencha frameworks and focus on a more real world example that shows how Model Associations can be put to use and how it simplifies the application’s controller code.

The example of this post is built around the imaginary BaristaStuff company. Together with SalesForce.com specialists Intigris we have built a similar (but much more complex) field engineer mobile application (FieldBuddy) for the European service management market that hosts all data on SalesForce.com. A complete example including the Salesforce.com communication is much more involved and is outside of the scope of this article. For information about communicating with Salesforce.com through APEX I warmly recommend the excellent article series by Don Robins . We will also be publishing an article on communicating through the Salesforce.com REST API in the near future. For now we will just focus on the mobile side of things.

All the code for the BaristaStuff example can be found on my GitHub account referenced in the conclusion below.

BaristaStuff Inc.

Our imaginary company BaristaStuff Inc. sells and maintains professional Espresso machines. BaristaStuff’s clients are bars and restaurants that use the machines on a daily basis and receive a support subscription when purchasing a machine. BaristaStuff uses a CRM system to register customer details, their purchases and service requests. When a customer calls customer support to report a problem or for making a maintenance appointment, a ‘ServiceRequest’ is created in the CRM system. Next, the ServiceRequest is assigned to a field engineer in the form of a ‘WorkOrder’. Field Engineers use the BaristaStuff Mobile app to check their work and register activities and time spent. The mobile application synchronizes this data with the CRM system, making the cooperation between BaristaStuff’s back office and field engineers almost real-time.

Like mentioned in the introduction, before implementing the mobile application it’s crucial to first look at the data structure the app will operate on and see how we implement that in Sencha Touch. Based on the functionalities that BaristaStuff needs, the CRM objects are selected that contain the required data. The data model below demonstrates the whole data structure.

The main objects (Models) are:

  • Brand
    The brands BaristaStuff carries
  • Product
    The specific product models, every product has a parent relation to it’s brand
  • Customer
    BaristaStuff’s customer list, containing their address details
  • Employee
    A list of field engineer employees
  • InstalledProduct
    When an espresso machine is installed, an InstalledProduct record is created that contains the Product (and thereby brand), the Customer it was installed for and the installation date. Thus a InstalledProduct represents that machine that was physically installed.
  • ServiceRequest
    When a maintenance or malfunction call is received by the call-center, a ServiceRequest is logged containing the details of the call.
  • WorkOrder
    The assignment of a ServiceRequest to a certain field engineer Employee on a certain date (and time).
  • Time
    When the field engineer spends time on a ServiceRequest it is immediately logged into the mobile application so the backoffice can monitor which ServiceRequests take what amount of time
  • Activity
    Every activity the field engineer does is also registered in the application. With this information the back-office can see whether parts need to be replenished in the field engineer’s car and what time can be invoiced to the client.

All the core data is of course available in the CRM system and can be downloaded and updated through a REST API. This means that for every object a list can be downloaded and single items can be updated, created or deleted. The downloaded records contain foreign key references to other objects in the system.

Functional requirements

When talking to BaristaStuff we find out that the main reason for building a mobile field engineer application is to increase the level of cooperation between the back-office and field engineers and to make the work of the engineers themselves more efficient. In short for the BaristaStuff app the functionalities are:

  • After logging in, the field engineer’s app downloads the work orders that are assigned to him
  • The WorkOrder list is ordered by date and must contain basic information about the WorkOrder itself, the product that he will work on, the city he needs to drive to and a short description about the service request.
  • When tapping a WorkOrder, all the relevant WorkOrder information needs to be presented in a single screen. This means the screen will need to display Customer information, information about the product and the service request information itself.
  • The field engineer needs to be able to view and register Time and Activities
  • To find out what the product service history is (EG. was the boiler replaced already?) a list of previous service request needs to be presented that not only displays the service request information but also the time and activities that were registered on those service request.

Sencha Models

Now we know the server model structure it’s time to build the mobile side of things. To get a tight integration with the server we always choose to very closely mimic the server data structure in our mobile application. Creating the models themselves is easy enough in Sencha Touch. The model associations can be a bit more tricky though. Luckily our BaseModel class (demonstrated in the previous article) takes care of most of the nasty details. We have left out most field configuration to simplify the example a bit. As an example, the InstalledProduct Model is implemented as follows.

Ext.define("BaristaStuff.model.InstalledProduct", {
	extend: 'BaristaStuff.model.BaseModel',

	config: {
		idProperty: 'id',

		fields: [{
				name: 'id'
			}, {
				name: 'customerId'
			}, {
				name: 'productId'
			}, {
				name: 'installDate'
			}
		],
		belongsTo: [{
				model: 'BaristaStuff.model.Customer',
				name: 'Customer',
				primaryKey: 'id',
				foreignKey: 'customerId',
				foreignStore: 'Customers'
			},
			{
				model: 'BaristaStuff.model.Product',
				name: 'Product',
				primaryKey: 'id',
				foreignKey: 'productId',
				foreignStore: 'Products'
			}
		],
		hasMany: [{
				model: 'BaristaStuff.model.ServiceRequest',
				name: 'ServiceRequest',
				primaryKey: 'id',
				foreignKey: 'installedProductId',
				foreignStore: 'ServiceRequests'
			}
		]
	}
});

The belongsTo and hasMany configurations define the model associations the model has with other models. A detailed description of how the association configurations work is explained in the previous post.

Our models extend the BaseModel class that gives us access to all the association goodness. Later we will see if we can rewrite this functionality to a mixin to make it even more flexible. The stores we use also need a small addition to the functionality. To enable our records to find related models, the stores those records are stored in need to be able to search in the underlying unfiltered data. The default search methods of Sencha stores search in filtered data, which would break our record lookup methods if we filter the store.

The InstalledProducts store thus looks as follows:

Ext.define('BaristaStuff.store.InstalledProducts', {
	extend: 'BaristaStuff.store.BaseStore',

	config: {
		model: 'BaristaStuff.model.InstalledProduct',
		data: [{
			id: 1,
			customerId: 1,
			productId: 4,
			installDate: '2012-08-05'
		}]
	}
});

Using Model Associations

Ok enough with all the talk, let’s see how we can put these associations to work! When the user logs into the application he sees his WorkOrders in a list, ordered and grouped by date. This list already shows some problems when trying to use associations. While the list reads its data from the ‘WorkOrders’ store, it also displays data from parent records. This way we can display information about the product involved and the customer. The image below shows data being fetched from associated records.

The nice thing about Sencha Touch is that once the relations have been instantiated they can be easily used in a list by using dot-delimited relation notation (yes, those are tables ☺):

itemTpl: [
	'<div class="workorder">',
		'<table style="width: 100%">',
		'<tr>',
			'<td style="width: 35%"><b>{date:date("H:i")}</b></td>',
			'<td style="text-align: right;">Status: {ServiceRequest.status}</td>',
		'</tr>',
		'<tr>',
			'<td>City: </td>',
			'<td>{ServiceRequest.InstalledProduct.Customer.city}</td>',
		'</tr>',
		'<tr>',
			'<td>Product: </td>',
			'<td>{ServiceRequest.InstalledProduct.Product.Brand.name} {ServiceRequest.InstalledProduct.Product.name}</td>',
		'</tr>',
		'<tr>',
			'<td>Description: </td>',
			'<td>{ServiceRequest.description}</td>',
		'</tr>',
		'</table>',
	'</div>'
]

In this snippet it is easy to see that using {ServiceRequest.description} gets the description field from the WorkOrder’s ServiceRequest parent record. The problem however is that model associations only get instantiated when a store is loaded with hierarchical data (see the previous post on that). With most REST API’s this is difficult or even impossible. REST APIs typically return lists of a single record type as you would expect. When using the BaseModel class and specifying the relations properly, you don’t need to worry about that. All the associations are linked up when data is requested from the model.

When tapping on a WorkOrder, the user is taken into the WorkOrder detail panel. This tab panel shows the WorkOrder’s data in a single form, contains a list with logged time and a list for the activities that are registered during the service visit. It also contains a history list that displays historic service requests on the same InstalledProduct, more on that later.

WorkOrder detail form

The WorkOrder detail form displays all information that the back-office registered for the ServiceRequest. Examples are the name and street address of the customer, the product brand and type, and the service request information itself. Like the WorkOrder list this form also shows data from several records, while it is only handed 1 WorkOrder record when the user taps it in the list.

Using associated data in a single form is a little trickier. Normally one would have to create a custom data object containing the values that have to be displayed in the form. Again, this moves a lot of functionality into the controllers while we think the Model should take care of most of that. We would like to specify the form items similarly to items in the List XTemplate.

items: [{
	xtype: 'fieldset',
	title: 'Customer',
	defaultType: 'textfield',
	items: [{
		name: 'ServiceRequest.InstalledProduct.Customer.firstName',
		label: 'First Name'
	},
	{
		name: 'ServiceRequest.InstalledProduct.Customer.lastName',
		label: 'Last Name'
	},
	{
		name: 'ServiceRequest.InstalledProduct.Customer.street',
		label: 'Street address'
	},
	{
		name: 'ServiceRequest.InstalledProduct.Customer.postalCode',
		label: 'Postal Code'
	},
	{
		name: 'ServiceRequest.InstalledProduct.Customer.city',
		label: 'City'
	}]
}]

The above code snippet shows how that the form field’s name attributes contain the same associations we used in the List. The only difference is that the Form does not recognize the associations and just looks for an explicit ‘ServiceRequest.InstalledProduct.Customer.city’ attribute in a flat data object:

var data = {
	'ServiceRequest.InstalledProduct.Customer.city’: ‘New York’
}

Luckily our BaseModel contains a getFlattenedData method that does just that. It flattens the relations a record has into a single-level object that can be used directly in a form.

>	workOrderRec.getFlattenedData(true)
>	Object {
		Employee.firstName: "John"
		Employee.id: 1
		Employee.lastName: "the Repairman"
		ServiceRequest.InstalledProduct.Customer.city: "Tampa Fl"
		ServiceRequest.InstalledProduct.Customer.firstName: "John"
		ServiceRequest.InstalledProduct.Customer.id: 3
		ServiceRequest.InstalledProduct.Customer.lastName: "Doe"
		ServiceRequest.InstalledProduct.Customer.postalCode: "54321"
		ServiceRequest.InstalledProduct.Customer.street: "Cappucino alley 3"
		ServiceRequest.InstalledProduct.Customer.telephone: "555-123456"
		ServiceRequest.InstalledProduct.Product.Brand.id: 1
		ServiceRequest.InstalledProduct.Product.Brand.name: "Ascaso"
		ServiceRequest.InstalledProduct.Product.brandId: 1
		ServiceRequest.InstalledProduct.Product.id: 1
		ServiceRequest.InstalledProduct.Product.name: "Steel BAR"
		ServiceRequest.InstalledProduct.customerId: 3
		ServiceRequest.InstalledProduct.id: 3
		ServiceRequest.InstalledProduct.installDate: "2013-01-05"
		ServiceRequest.InstalledProduct.productId: 1
		ServiceRequest.date: "2013-07-14 10:00"
		ServiceRequest.description: "Pump pressure stays at 1 bar only"
		ServiceRequest.id: 1
		ServiceRequest.installedProductId: 3
		ServiceRequest.status: "Open"
		date: Sun Jul 14 2013 10:00:00 GMT-0400 (EDT)
		description: undefined
		employeeId: 1
		id: 1
		serviceRequestId: 1
	}

 


For the rest it works just like the default getData method that is normally used for loading a record into a form:

var detailPanel = this.getWorkOrderDetails();
detailPanel.setValues(record.getFlattenedData(true));

Give me more!

Wow, we can use the Model’s associations directly in XTemplates, forms, lists, etc. That’s pretty cool. But it gets better. Remember the ‘History list’ in the WorkOrder panel?

This list shows all the Service requests that are children of the same InstalledProduct the field engineer is currently working on. When the field engineer is working on an Espresso Machine’s boiler it would be good to see when it was last checked and what was done on previous service visits. One of his collegues might have maintained it last time. So what do we need to do:

  • Walk up the current WorkOrder record’s associations until we reach it’s InstalledProduct
  • Find all the ServiceRequests that are related to the InstalledProduct (walking down again)
  • For every found sibling ServiceRequest, we want to display information about the ServiceRequests themselves in a list, but also display information about the time that was logged and activities that were done when handling that historic Service request.

We will skip over the part where we do this when we would not have our BaseModel class, it’s just too damned time consuming. Again our BaseModel class comes flying to the rescue and makes the process almost trivial. One of the gems that BaseModel provides is the ‘getAssociatedRecords’ method. If we want all WorkOrders that are somehow related to a single Customer we would just call:

> customerRec.getAssociatedRecords('WorkOrder')
[Class, Class, Class, Class, Class]

We hand the method the name of the related records’ Model we would like to find, and bam, there we are. How cool is that? The getAssociatedRecords method works both ways. When using associations, the BaseModel dynamically figures out the path from the record’s Class to the associated model’s Class.

> workOrderRec.getAssociatedRecords('Customer')
[Class]

Back to the HistoryList, the list uses a simple store that is loaded with historic ServiceRequest records from the Controller. Before finding those records we first need to find the parent InstalledProduct record, and then find the related ServiceRequest records that are children of that InstalledProduct. The only thing we need to take care of is filtering out the ServiceRequest of the WorkOrder we are currently working on.

var historyList = this.getWorkorderHistoryList();
var installedProduct = record.getAssociatedRecords('InstalledProduct'); // Traverse up to get the parent InstalledProduct
var historyRecords = installedProduct[0].getAssociatedRecords('ServiceRequest'); // Traverse down to get all Service requests
// filter out my own service request
for (var i=historyRecords.length-1;i>=0;i--) {
	if (historyRecords[i].get('id') === record.get('ServiceRequest').id) {
		historyRecords.splice(i,1);
	}
}

var historyStore = historyList.getStore();
historyStore.removeAll();
historyStore.add(historyRecords);

There we are, in 11 lines of code we have found the historic ServiceRequest records and have loaded them into the list. The list uses an XTemplate to display the ServiceRequest data:

itemTpl: [
	'<div class="workorder">',
		'<table style="width: 100%">',
		'<tr>',
			'<td style="width: 35%">Date: </td>',
			'<td>{date:date("Y-m-j H:i")}</td>',
		'</tr>',
		'<tr>',
			'<td style="vertical-align: top">Description: </td>',
			'<td >{description}</td>',
		'</tr>',
		'<tr>',
			'<td style="vertical-align: top">Activities: </td>',
			'<td><tpl if="activities.length == 0">No activities logged</tpl><tpl for="activities"><div>- {type}: {comments}</div></tpl></td>',
		'</tr>',
		'<tr>',
			'<td style="vertical-align: top">Logged time: </td>',
			'<td><tpl if="times.length == 0">No time logged</tpl><tpl for="times"><div>- {start:date("H:i")} -  {end:date("H:i")}</div></tpl></td>',
		'</tr>',
		'</table>',
	'</div>'
]

The only tricky part now is getting the list to loop over the activities and logged time items to display them in the list items. For this we added a prepareData method to the list that automatically gets called by Sencha Touch when loading data into a list. In that method we again use getAssociatedData method to fetch and inject the required data into the record data:

prepareData: function(data, index, record) {
	var i;
	var activities = record.getAssociatedRecords('Activity');
	var activityArr = [];
	for (var i=0;i<activities.length;i++) {
		activityArr.push(activities[i].getData());
	}
	data.activities = activityArr;

	var times = record.getAssociatedRecords('Time');
	var timeArr = [];
	for (var i=0;i<times.length;i++) {
		timeArr.push(times[i].getData());
	}
	data.times = timeArr;

	return data;
}

Conclusion

By just letting our models inherit from the BaseModel class (and our stores from the BaseStore class), and properly defining our associations we get rid of a lot of nasty Controller code. Immediately we are able to use Associated data in all sorts of components. We think this is a really nice combination between abstracting Model functionality out of our application and at the same time giving us much more flexibility when designing components that operate on those models and associations.

Feel free to use the code of this example and the BaseModel class. I put all the code for this application up on GitHub and a working example that you can play with on our site

Let me know if this helps you, I would love to see feedback about what components get built using it and to hear possible improvements.

12 Responses to "Model Associations in Sencha Touch (and ExtJS) made easy"
  1. Greg Silverman says:

    Hey Rob,
    I like this a LOT, but I am having issues with my hasMany associations not persisting. My hasOne associations on the other hand, are just fine. I realize this must be an issue with the loading of the data through proxy (I am using the ST 2.3.1 sql proxy). Any suggestions would be most welcome. I have literally been tearing me hair out for the last 3-days trying to figure out why this is happening.

    • admin says:

      Hm, well we just use the associations for display purposes. When we save a hasMany relation we just save the foreign key on the referenced record and reload the associations

  2. Masud says:

    Hi Rob,

    Is the example still working. I get no Customer or Product details for any of the items in the demo.

    I am trying to implement the same as a many-to-many model but seems like no data is being loaded. Any guidance will be highly appreciated.

  3. JT McGibbon says:

    Excellent work, Rob!

    In utilizing this in a real-world ExtJS application, I modified the BaseModel class to work with ExtJS 4.2.2, added a setFlattenedData() method (to put the data from a form which had used flattened data references back into the model and it’s hierarchy properly — example use: yourBaseModelInstance.setFlattenedData(form.getValues())) PLUS combined the other fine Model extension from Aaron Smith @ ModusCreate for writing (PUT/POST) hierarchical data back to the server.

    I have been using this in a production environment for a few months now and it works very well. The changes are contained in my GitHub repo Extensions to the Sencha ExtJS Model for ease of managing hierarchical data

    I did not fork your whole repo as the change was only to the BaseModel.js defnintion but feel free to comment/and or make use of the changes into your if you want.

    Cheers! :-)

    • Greg Silverman says:

      JT,
      Thanks for this! I needed your setFlattenedData() method! You saved me from doing a lot of work.

      Greg–

  4. Tony says:

    Hi Rob … I’m contemplating using your coding pattern. Another option I’m considering would be to create some database views that flatten the table model at the server level (for Many-1 relations) and then project a simpler model on to these views in Sencha Touch. I guess it really depends on what parts of the model you need to update vs just read. This view projection is also not following the pure REST pattern. Any feedback/thoughts/preferences?

    • Rob Boerman says:

      Hi Tony,
      When you flatten the data at the server everything should work out of the box. I have a couple of problems with this myself though:

      1. The server needs to know about which structures need to be flattened and is not pure REST anymore. I like my backends to be atomic. With a lot of public API’s you don’t even have this option. As an example we communicate with Salesforce.com a lot. That API only gives you atomic data. You CAN create your own API but that’s a lot of work.
      2. When model A has a child of model B, and model A gets loaded from the server with model B flattened underneath it I still don’t have a model B in my ‘model B store’. So when I have another component that edits model B from it’s store, the child data in model A is invalidated… so then you have to refresh A as well… with nested structures this gets messy fast
      3. When working with thoudands of models with a lot of associations, the flattened data structures get really big and complex. With multiple levels of nesting this becomes even worse.

      Good luck, let me know what you go with and how it works out
      Rob

  5. mkoch says:

    Hello, great articles, thank you!

    BUT … this doesn’t work with ExtJS! See http://www.sencha.com/forum/showthread.php?259051

    • Rob Boerman says:

      Hi,

      You’re right. There are a couple of differences between the data package of ExtJS and Sencha Touch which means you cannot plug and play this into an ExtJS app.
      The post is primarily meant to demonstrate how model associations can be used with stores that load their data separately. It’s not too difficult to port this to ExtJS which we do in a lot of our apps.

  6. Sandheep says:

    Great job :) thank youu….

Leave a Reply

Your email address will not be published. Required fields are marked *

five × 1 =

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>