Associations have undergone quite a few changes in Ext JS 5. Sencha has really put work into making them more functional and easier to use. Like all things though, they have their ups and downs. So, letโs dig into the new associations starting withโฆ
The Good
Music Associations? Yeah, it’s very good. It’s very good for the digestion.
The New Ext.data.Field.reference Approach
One of the nice things Sencha has done is to eliminate the need to specify both sides of the association โequationโ by giving us a new way to declare them. We do this by using the reference property of the field. So, if you have a Customer model with an Address association, you can simply have the following:
Ext.define('Customer', { extend : 'Ext.data.Model', fields : [ 'name' ] }); Ext.define('Address', { extend : 'Ext.data.Model', fields : [ { name : 'address'}, { name : 'customerId', reference : 'Customer'} ] }); Ext.define('Customers', { extend : 'Ext.data.Store', model : 'Customer', autoLoad : true, proxy : { type : 'ajax', url : 'customers.json' } }); // customers.json [{ id : 1, name : "Modus Create", addresses : [{ id : 1, address : 'Reston, VA' },{ id : 2, address : 'Providence, RI' }] }]
The Extras
This will create an association and inverse association for both Customer and Address and should get our data tied together easily. If you inspect the Customers store, youโll see that each record now has an addresses() getter that will return a store of this customerโs addresses. Also, each address record in that store will have a reference to the customer record and a โcustomerIdโ on its data property.
The Bad
Howโs your digestion now?
Also the New Ext.data.Field.reference Approach
As I mentioned, the new method for defining these relationships is all driven from the reference property of a field in the association model. As of today, you canโt use the Ext.data.Field.reference form of the relationship from the parent (customer) model. This is unfortunate as most people will naturally approach these definitions from the right/parent/customer side. I know what youโre thinking. Tim is great. If you have 10 different models that all need address associations, then yes, you are going to need 10 address models. The number of model classes you have is going to increaseโฆ possibly by quite a bit. You could, of course, create a base address model (minus the reference field) and then extend all of the others from this base class.
Legacy HasMany
What if you donโt want forty new models or what if you already have a code base that is using hasMany associations? The answer is that you cannot use the new reference association feature but you can still create association relationships from the parent by using the hasMany config property on your parent model, just like you have been. Building on our example above, we can add a Vendors model with an Address hasMany.
Ext.define('Vendor', { extend: 'Ext.data.Model', requires: ['Address'], fields: [ 'name' ], hasMany: [{ name: 'addresses', // associationKey: 'foo', model: 'Address' }] });
Fiddle: https://fiddle.sencha.com/#fiddle/914
Gotchas
If you run the fiddle above and check out the vendorsStore, youโll see that we still get a reference to the appropriate parent model on each of the address models. One gotcha with this approach is that you still need to include the association model classname in your requires array. If you leave that out, your association data wonโt load and you wonโt even get a console error. So, don’t forget to put that in.
Itโs also worth noting that โassociationKeyโ is still available as a config option for hasMany associations. Unfortunately, the documentation for the hasMany association config is completely gone. You can always refer to the Ext JS 4 docs until/if this gets added to the 5.0 docs.
Finally, under the covers, Ext JS 5 will actually run your hasMany associations through Ext.data.schema.Schema.addLegacyHasMany(). So, if you are curious, you can set a breakpoint at the end of that function and inspect the association it creates. You could then use that information to convert your hasManys over to field references if you wanted to.
The Ugly
Who the hell is that? One bastard association goes in, another one comes out.
Overrides
As hard as framework authors try, there is just no way they can predict or even accommodate everyoneโs needs. Weโve had some requirements on our own projects that have needed some overrides. Remember, the danger with overrides is that, as the framework changes, your overrides may break. Be sure to pay close attention to your unit tests for these when you upgrade. You are doing unit testing right? OK, letโs look at these overrides in order.
1) Missing Association Data on Some Records
What if the server doesnโt send the nested association data on every record? Some servers wonโt send empty objects in an attempt to keep transmission sizes down. For example, if you look at the fiddle again, youโll notice in vendors.json that โGranite Partsโ doesnโt have an address. What would normally happen is that the record with the missing data would not get an association store. We can change this with an override to Ext.data.schema.ManyToOne so that every record gets an association store even if its just an empty one.
/** * Association ManyToOne extension */ Ext.define('MyApp.data.schema.ManyToOne', { override: 'Ext.data.schema.ManyToOne', constructor: function(config) { this.Left.override({ /** * The original function had a check to make sure that the read root for this * association existed in the data object. In our case, the server may not send * the association data if its empty. In that case, we want an empty store. */ read: function(rightRecord, node, fromReader, readOptions) { var me = this, // We use the inverse role here since we're setting ourselves // on the other record key = me.inverse.role, // result = me.callParent([ rightRecord, node, fromReader, readOptions ]), result = me.callSuper([ rightRecord, node, fromReader, readOptions ]), store, leftRecords, len, i; // Did the root exist in the data? if (result.getReadRoot()) { // Create the store and dump the data store = rightRecord[me.getterName](null, null, result.getRecords()); // Inline associations should *not* arrive on the "data" object: // delete rightRecord.data[me.role]; delete rightRecord.data[me.associationKey ? me.associationKey : me.role]; leftRecords = store.getData().items; for (i = 0, len = leftRecords.length; i < len; ++i) { leftRecords[i][key] = rightRecord; } // Create an empty store if the root doesn't exist } else { store = rightRecord[me.getterName](null, null, []); } } }); this.callParent(arguments); } });
Fiddle: https://fiddle.sencha.com/#fiddle/914
2) Cleanup of โassociationKeyโ Data
Association data for hasMany associations that uses an โassociationKeyโ remains on the data object. Ext JS does a fine job of cleaning the data object of association data properties. By that I mean you wonโt see an โaddressโ property on any of the record data objects. It only falls down when you are using an โassociationKeyโ on a legacy hasMany. So, youโll notice up above, that weโve replaced the delete rightRecord.data[me.role]; statement with one that supports association keys. If you are using dot notation in your associationKey then you’ll need to modify the code above to traverse the data object and delete the appropriate property.
3) Support for โstoreConfigโ on hasMany
hasMany config objects used to support a storeConfig property which was especially handy when you needed to supply your association store with sorters, etc. Letโs add support back in with an override to Ext.data.schema.Schema. Technically, the logic should go in the addLegacyHasMany method but we are going to put our custom code into the addReference method because itโs less invasive.
/** * Schema extension */ Ext.define('Schema', { override: 'Ext.data.schema.Schema', addReference: function (entityType, referenceField, descr, unique) { // add the storeConfig from the legacy hasMany association to the inverse if (descr.legacy && descr.inverse && descr.storeConfig && !descr.inverse.storeConfig) { descr.inverse.storeConfig = descr.storeConfig; } this.callParent(arguments); } });
Fiddle: https://fiddle.sencha.com/#fiddle/914
Fin
With those overrides in place, we can now handle missing data from the server, clean up after association keys and sort our association stores. Now if we could only convince Sencha to support the Ext.data.Field.reference approach from the right/parent side…
Timothy Eagan
Related Posts
-
Ext JS to React: FAQ
This is part of the Ext JS to React blog series. React is Facebook's breakout…
-
Ext JS to React: Migration to Open Source
Worried about Migrating from Ext JS? Modus has the Answers Ideraโs acquisition of Sencha has…