Dynamic Models, Stores, and Views -- Best way to - javascript

NOTE: Author is new to EXT JS and is trying to use MVC in his projects
imagine i have a web service whose data model is not fixed. I would want to have my models dynamically created, from which i dynamically create stores and hence dynamic components whose data is in those stores.
Lets start by seeing a sample class definition of a model:
Ext.define('MNESIA.model.User', {
extend: 'Ext.data.Model'
});
In this model definition, i have left out the 'fields' parameter in the config object. This is because whwnever i create an instance of a model of the type above, i want to dynamically give it its fields definition, in other words i can have many instances of this model yet all having different definition of their 'fields' parameter.
From here i create a definition of the store, like this:
Ext.define('MNESIA.store.Users', {
extend: 'Ext.data.Store',
autoLoad: true
}
});
There, i have a store definition. I have left out the 'model' parameter because i will attach it to every instance of this class dynamically. Infact, even the 'data' and 'proxy' settings are not mentioned as i would want to asign them during the instantiation of this store.
From there, i would want to have dynamic views, driven by dynamic stores. Here below i have a definition of a Grid
Ext.define('MNESIA.view.Grid' , {
extend: 'Ext.grid.Panel',
alias : 'widget.mygrid',
width: 700,
height: 500
});
I have left out the following parameters in the Grid specification: 'columns', 'store' and 'title' . This is because i intend to have many Grids created as instances of the specification above yet having dynamic stores, titles and column definitions.
Some where in my controller, i have some code that appears like this:
function() {
var SomeBigConfig = connect2Server();
/*
where:
SomeBigConfig = [
{"model":[
{"fields":
["SurName","FirstName","OtherName"]
}
]
},
{"store":[
{"data":
{"items":[
{"SurName":"Muzaaya","FirstName":"Joshua","OtherName":"Nsubuga"},
{"SurName":"Zziwa","FirstName":"Shamusudeen","OtherName":"Haji"},
...
]
}
},
{"proxy": {
"type": "memory",
"reader": {
"type": "json",
"root": "items"
}
}
}
]
},
{"grid",[
{"title":"Some Dynamic Title From Web Service"},
{"columns": [{
"header": "SurName",
"dataIndex": "SurName",
"flex": 1
},{
"header": "FirstName",
"dataIndex": "FirstName",
"flex": 1
},
{
"header": "OtherName",
"dataIndex": "OtherName",
"flex": 1
}
]}
]
}
]
*/
var TheModel = Ext.create('MNESIA.model.User',{
fields: SomeBigConfig[0].model[0].fields
});
var TheStore = Ext.create('MNESIA.store.Users',{
storeId: 'users',
model: TheModel,
data: SomeBigConfig[1].store[0].data,
proxy: SomeBigConfig[1].store[1].proxy
});
var grid = Ext.create('MNESIA.view.Grid',{
store: TheStore,
title: SomeBigConfig[2].grid[0].title,
columns: SomeBigConfig[2].grid[1].columns
});
// From here i draw the grid anywhere on the, page say by
grid.renderTo = Ext.getBody();
// end function
}
Now this kind of dynamic creating of models, then stores, and then grids does result into memory wastage and so this would require the destroy methods of each component to be called each time we want to destroy that component.
Questions:
Qn 1: Does the MVC implementation of EXT JS 4 permit this ?
Qn 2: How would i gain the same functionality by using the xtypes of my new classes. Say for example:
{
xtype: 'mygrid',
store: TheStore,
title: SomeBigConfig[2].grid[0].title,
columns: SomeBigConfig[2].grid[1].columns
}
Qn 3: If what i have written above really works and is pragmatically correct, can i apply this to all components like Panels, TabPanels, Trees (whereby their Configs are sent by a remote server) ?
Qn 4: If i have Controllers A and B, with controller A having a specification of views: [C, D] and Controller B having views: [E, F], would it be correct if actions generated by view: E are handled by Controller A ? i.e. Can a controller handle actions of a view which is not registered in its views config ?
NOTE: am quite new to Ext JS but would love to learn more. Advise me on how to improve my EXT JS learning curve. Thanks

In my option ,your model must map onto the store that is to be rendered to the view like for example,if implement the model part like this
{"model":[{"fields":[{name:'name',type:'string'},
{name:'id',type:'string'}]}]} ,this will be easily mapped onto the store for the view render it.

Related

Sequelize and defaultScope with Models associated

I am using Sequelize with Express, and Node js and I am trying to define defaultScope for Models.
Card and Tag have a Many To Many association.
Here are Models definitions and addScope
// Models Associations
// ONE TO MANY
List.hasMany(Card, {
as: "cards",
});
Card.belongsTo(List, {
as: "list",
});
// MANY TO MANY
Card.belongsToMany(Tag, {
as: "tags",
through: "card_has_tag",
updatedAt: false,
});
Tag.belongsToMany(Card, {
as: "cards",
through: "card_has_tag",
updatedAt: false,
});
// SCOPES
Card.addScope("defaultScope", {
include: {
association: "tags",
},
});
List.addScope("defaultScope", {
include: {
association: "cards",
include: "tags",
},
});
// What I would like to implement
// If I comment lines below => List and Card queries are working
Tag.addScope("defaultScope", {
include: {
association: "cards",
},
});
I would like to print by default all related infos with associated relations.
I want to get this info when I execute a sequelize query for each model.
LISTS with associated :
cards
tags
CARDS with associated:
tags
TAGS with associated :
cards
I manage to get 1 & 2, but when I add Tag.addScopenothing is working anymore.
When I change defaultScope by another string by defining a scope all (for example) , and when I use model.scope("all").findAll(), this is working, but it is not what I would like to do becaue I want to use defaultScope to have a default behavior so I don't have to specify scope in queries command like (findAll...)
Is there a way I can do that ?
The way you are trying to set it up results in an endless recursion, you simply can't have it like that.
If you set it up like that and query Card it will include Tag which will include Card which will include Tag and so on until you get Maximum call stack size exceeded.
There is a workaround you can use, which is to add another scope which includes nothing, then specify that scope for the model in the defaultScope.
Tag.addScope("noInclude", {});
Card.addScope("noInclude", {});
Tag.addScope("defaultScope", {
include: [
{
model: Card.scope("noInclude"),
as: "cards"
}
]
});
Card.addScope("defaultScope", {
include: [
{
model: Tag.scope("noInclude"),
as: "cards"
}
]
});
This should give you the desired behaviour.

Bind custom component store config to ViewModel store

I'm writing a custom component in Ext JS which needs a store as a part of its configuration. I want to keep this store in the component's ViewModel, and I also want it to be bindable. Currently I have code like this:
Ext.define('CustomComponent', {
extend: 'Ext.container.Container',
xtype: 'customcomponent',
viewModel: {
type: 'customcomponent'
},
config: {
store: 'ext-empty-store'
},
initComponent: function() {
var store = Ext.getStore(this.getConfig('store'));
this.lookupViewModel().set('myStore', store);
this.callParent(arguments);
}
});
Ext.define('CustomComponentViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.customcomponent',
data: {
myStore: null
}
});
This is not ideal for a number of reasons:
The store config option isn't bindable.
The store is contained in the data of the ViewModel, not the stores. This means it can't be accessed via the getStore method of the ViewModel.
To make the store bindable, I could write code like this:
Ext.define('CustomComponent', {
extend: 'Ext.container.Container',
xtype: 'customcomponent',
viewModel: {
type: 'customcomponent'
},
config: {
store: 'ext-empty-store'
},
publishes: 'store'
});
Ext.define('CustomComponentViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.customcomponent',
data: {
store: null
},
formulas: {
myStore: function(get) {
return Ext.getStore(get('store'));
}
}
});
This is not ideal either, though:
The ViewModel is polluted with the store config, which is not necessarily an Ext.data.Store. It could be a store's ID or a config object. Essentially, it's an implementation detail, and I'd like to keep it out of the ViewModel where it will be inherited by every child component.
The store is still not a part of the ViewModel's store configuration and so is still not accessible via getStore.
Essentially, I'm looking for a way to set up my View and ViewModel (and ViewController, if it would help) so that I can meet these three criteria:
The store config on the view is bindable.
The store config option is not kept in the ViewModel, or is somehow prevented from polluting the ViewModels of the component's children.
The store in the ViewModel is accessible via getStore.
Is it possible to meet all three of these criteria simultaneously? If not, what is the most canonical way to transform a bindable store config into a ViewModel store? The Ext JS source code doesn't use the View-ViewModel architecture to define components, so I can't just look at what they do.
I believe you could use the store like this.
Ext.define('CustomComponentViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.customcomponent',
formulas: {
myStore: function(get) {
return Ext.getStore(get('store'));
}
},
stores: {
myStore: {
fields: [
{
name: 'id'
},
{
name: 'name'
},
{
name: 'description'
}
]
}},
});
Ext.define('CustomComponent', {
extend: 'Ext.container.Container',
xtype: 'customcomponent',
viewModel: {
type: 'customcomponent'
},
config: {
bind: {
store: '{myStore}'
}
}
});
After reading the Ext JS 6.0.2 source code more thoroughly, I have come to the conclusion that meeting all three criteria I want is not possible without overriding or subclassing Ext.app.ViewModel. I'll go through the criteria one by one, and then discuss what I think is the best current practice.
Criteria
The store config must be bindable
This is actually accomplished by default; all config options are bindable. What I had meant when I said the store must be bindable was "binding to the store config will update the store in my ViewModel". The value of the store config will be sent to the ViewModel if the publishes option is set appropriately, i.e. publishes: 'store'. This will send the raw value of the store config back to the ViewModel as store, though, which is not what we want. I think the cleanest way to work around this is via the applyConfigName template method provided for each config option. Here's an example:
Ext.define('CustomComponent', {
extend: 'Ext.container.Container',
xtype: 'customcomponent',
viewModel: {
type: 'customcomponent'
},
config: {
store: 'ext-empty-store'
},
publishes: 'store',
applyStore: function(store) {
return Ext.getStore(store);
}
});
The applyStore function will be applied to the value of store before it gets set, transforming it from a store ID or config object into an actual store. This brings us to the second criterion:
The store config option is not kept in the ViewModel
By using the applyStore template method we can keep the store config option out of our ViewModel. (Indeed, it even keeps it out of our View!) Instead the value of store which is published to our ViewModel is the actual store which it defines. Unfortunately, however, publishing config values adds them to the data object of the ViewModel, not the stores object. This brings us to the final criterion:
The store in the ViewModel is accessible via getStore
This criterion is currently impossible to satisfy. The reason is two-fold:
getStore retrieves only stores added via the stores config option
In Ext JS 6.0.2, getStore retrieves stores by looking them up in the storeInfo private property of Ext.app.ViewModel. The storeInfo property is itself set via the setupStore private method, which is called only on each member of the stores config.
The stores config option only allows config objects
Unlike the store config of Ext.grid.Panel, the stores config option of Ext.app.ViewModel only allows the values of each store to be config objects; Store IDs and actual stores are not permitted. Setting the value to a binding directly seems like it works, e.g.:
Ext.define('CustomComponentViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.customcomponent',
data: {
dataStore: null
},
stores: {
storeStore: '{dataStore}'
}
});
But in actuality, the storeStore is a new Ext.data.ChainedStore which is configured with source: dataStore. This means that setting filters and sorters on storeStore will not affect dataStore, which is probably not what was desired.
The combination of these two restrictions means it is impossible to set a store in a ViewModel which can be retrieved with getStore.
So what should I do?
I think that with the available options, the best practice is to give up the ability to retrieve the store with getStore. While this is an annoying inconsistency, it is less likely to cause subtle bugs the way that allowing an implicit ChainedStore might. This gives us our final code:
Ext.define('CustomComponent', {
extend: 'Ext.container.Container',
xtype: 'customcomponent',
viewModel: {
type: 'customcomponent'
},
config: {
store: 'ext-empty-store'
},
publishes: 'store',
applyStore: function(store) {
return Ext.getStore(store);
}
});
Ext.define('CustomComponentViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.customcomponent',
data: {
store: null
}
});

How is localized data binding set up with JSON files and XML views?

I have an XMLView home page containing some tiles. These tiles are populated from a JSON file. The tiles have a 'title' attribute which requires i18n data binding.
Part of the XML view:
<TileContainer id="container" tiles="{/TileCollection}">
<StandardTile
icon="{icon}"
title="{title}"
press="onPress" />
</TileContainer>
JSON file:
{
"TileCollection" : [
{
"icon" : "sap-icon://document-text",
"title" : "{i18n>foo}"
},
... etc
The old way I accomplished data binding was directly in the view with title="{i18n>foo}". Of course now I have essentially two layers of data binding, one in the JSON for the i18n, and one in the view to get the JSON (which gets the i18n).
This is also my Component.js where I set up the i18n model.
sap.ui.core.UIComponent.extend("MYAPP.Component", {
metadata: {
rootView : "MYAPP.view.Home", //points to the default view
config: {
resourceBundle: "i18n/messageBundle.properties"
},
... etc
init: function(){
sap.ui.core.UIComponent.prototype.init.apply(this, arguments);
var mConfig = this.getMetadata().getConfig();
var oRouter = this.getRouter();
this.RouteHandler = new sap.m.routing.RouteMatchedHandler(oRouter);
oRouter.register("router");
oRouter.initialize();
var sRootPath = jQuery.sap.getModulePath("MYAPP");
var i18nModel = new sap.ui.model.resource.ResourceModel({
bundleUrl : [sRootPath, mConfig.resourceBundle].join("/")
});
this.setModel(i18nModel, "i18n");
}
This question arose from discussion about another question, so there may be more info there for anyone interested. Link
The approach I usually take is using a formatter function, which sole purpose is to get the correct localized value for a certain key (which is maintained in the resource model, and driven by the data model)
For instance, the Tile UI would look like this:
<TileContainer id="container" tiles="{/tiles}">
<StandardTile
icon="{icon}"
type="{type}"
title="{ path : 'title', formatter : '.getI18nValue' }"
info="{ path : 'info', formatter : '.getI18nValue' }"
infoState="{infoState}"
press="handlePress"/>
</TileContainer>
(Notice the formatter function getI18nValue for properties title and info; these are the properties to be translated. The other properties come as-is from the bound JSONModel)
The model could look like this:
tiles : [
{
icon : "sap-icon://inbox",
number : "12",
title : "inbox", // i18n property 'inbox'
info : "overdue", // i18n property 'overdue'
infoState : "Error"
},
{
icon : "sap-icon://calendar",
number : "3",
title : "calendar", // i18n property 'calendar'
info : "planned", // i18n property 'planned'
infoState : "Success"
}
]
where the title and info property values of the JSONModel (for instance, 'inbox' and 'overdue') correspond with a key in your resourcebundle files (and thus your ResourceModel)
The formatter function in the controller (or better, in a standalone JS file, for re-use in multiple views) is then pretty simple:
getI18nValue : function(sKey) {
return this.getView().getModel("i18n").getProperty(sKey);
}
It does nothing more than supplying the value from the model (for instance, 'inbox') and returning the localized value for this key from the resource model

Backbone Menu Not Sorting

I'm having trouble getting a Backbone collection to sort properly. I inherited the project, so there may be some shenanigans someplace else, but I want to rule out any syntax error on my part.
The project uses a JSON file to handle the data:
"classifications": [
{
"name": "1 Bedroom",
"alias": "1BR",
"id": "1BR",
"menu_desc": "Residences"
},
{
"name": "2 Bedroom",
"alias": "2BR",
"id": "2BR",
"menu_desc": "Residences"
},
{
"name": "3 Bedroom",
"alias": "3BR",
"id": "3BR",
"menu_desc": "Residences"
},
{
"name": "4 Bedroom",
"alias": "4BR",
"id": "4BR",
"menu_desc": "Residences"
},
{
"name": "Common Areas",
"alias": "Common",
"id": "Common",
"menu_desc": "Resident Amenities"
}
]
Previously, there were no one-bedroom units, and the order in which it rendered was this:
I added the one-bedroom classification, and suddenly the order was this:
I did some digging and found documentation about the comparator property, but it only seems to apply to collections. This project doesn't use a collection for the classifications. It does for the submenu items (which floor the units are on, etc.), but not the main menu:
var MenuClassificationListView = Backbone.View.extend({
id: "classification_accordion",
template: _.template( "<% var classifications = this.options.classifications; _.each(this.collection.attributes, function(v,k) { %>"+
"<h3 class='<%= k %>'><%= classifications.get(k).get('name') %>"+
"<p><%=classifications.get(k).get('menu_desc')%></p></h3>"+
"<% var model = new MenuClassificationList(v); var view = new MenuClassificationItemView({collection:model, classification:k}); %>"+
"<% print(view.render().el.outerHTML); %>"+
"<% }); "+
"%>"),
render: function(){
//console.log(this.options.classifications);
//console.log(this.collection.attributes);
//alert(1);
this.$el.html(this.template());
return this;
}
});
How do I incorporate the comparator?
Thanks,
ty
One way could be to define a collection for the classifications, same way they are defined for the other items you mention:
var Classifications = Backbone.Collections.extend({ // etc. etc.
That way you can add the comparator and it will always be sorted.
Another way is to sort (http://underscorejs.org/#sortBy) the array in the initialize function in your view:
initialize: function(options) { // sorry don't remember the exact syntax for the parameters passed in, but I believe options is what you need
this.options.sortedclassifications = _sortBy(options.classifications, function (c) { return parseInt(c.id); }); // or any other sorting logic
}
Then in the template use the sorted classifications:
template: _.template( "<% var classifications = this.options.sortedclassifications; _.each(this.collection.attributes, function(v,k) { %>" + // etc. etc.
This should give you what you need. However, if I may add a personal opinion, I would go through the effort of defining a Collection for the classifications and a model for the single classification. Moreover, I would keep the MenuClassificationListView but also create a MenuClassificationView that will hold the single classification template.
In this way you are able to compose views, change rendering of the single classification without changing the list and scope the events to the inner views (so clicking on a single classification is handled by the single classification view). It makes everything cleaner, more composable and readable, in my opinion.
_.sortBy does not need to be used as Backbone collections already come with built in functionality for sorting.
See: http://backbonejs.org/#Collection-comparator
Example:
var SortedCollection = Backbone.Collection.extend({
comparator: 'key'
});
var mySortedCollection = new SortedCollection([{a:5, key:2}, {key:1}]);
console.log( mySortedCollection.toJSON() );
// [{key:1}, {a:5, key:2}]
However, the collection will not be automatically re-sorted when changing the key attribute. See:
mySortedCollection.at(0).set( 'key', 3 );
console.log( mySortedCollection.toJSON() );
// [{key:3}, {a:5, key:2}]
You have multiple options to solve this problem: you can manually call mySortedCollection.sort() or you can initialize the collection by binding its change:key event to re-sort the collection. The change:key event is triggered by the model whose key attribute is changed. This event is automatically propagated to the collection.
var AutoSortedCollection = Backbone.Collection.extend({
comparator: 'key',
initialize: function() {
this.listenTo( this, 'change:key', this.sort );
}
});
In addition, I suggest removing functionality from the templates. It is easy to debug Backbone Views, but it gets harder to read the stack trace as you move functionality inside the template string. You also enforce proper separation of concerns by using your Backbone View for preparing all data for presentation and your template should just display it.
var MyView = Backbone.View.extend({
//...
serializeData: function() {
return {
classifications: this.collection.toJSON(),
keys: this.collection.length > 0 ? this.collection.at(0).keys() : []
}; // already sorted
}
render: function() {
this.$el.html(this.template( this.serializeData() ));
}
});
Your template string becomes much easier to read: you can directly use the variables classifications and keys, iterate on them with _.each and simply reference to values without having to deal with the Collection syntax.

Backbone-Relational related models not being created

I'm trying to create a nested, relational backbone project but I'm really struggling. The rough idea of what I'm trying to do is shown below but I was under the impression upon calling fetch() on Client, a number of bookings would automatically be created based on the bookings being returned as JSON.
The format of my JSON can be seen beneath the outline of the MVC:
/****************************************************
/* CLIENT MODEL - Logically the top of the tree
/* has a BookingsCollection containing numerous Booking(s)
/* Client
/* -Bookings [BookingsCollection]
/* -Booking [Booking]
/* -Booking [Booking]
/*****************************************************/
var Client = Backbone.RelationalModel.extend({
urlRoot: '/URL-THAT-RETURNS-JSON/',
relations: [
{
type: Backbone.HasMany,
key: 'Booking',
relatedModel: 'Booking',
collectionType: 'BookingsCollection'
}
],
parse: function (response) {
},
initialize: function (options) {
console.log(this, 'Initialized');
}
});
var Booking = Backbone.RelationalModel.extend({
initialize: function (options) {
console.log(this, 'Initialized');
}
});
var BookingsCollection = Backbone.Collection.extend({
model: Booking
});
Any help outlining what I'm doing wrong would be massively appreciated.
Thanks
EDIT
Thanks for taking the time to post the feedback, it's exactly what I was hoping for.
Is it the case that the JSON physically defines the actual attributes of models if you don't go to the effort of setting attributes manually?
In other words, if the JSON I get back is as you have suggested above, would Backbone simply create a Client object (with the 4 attributes id, title, firstname & surname) as well as 2 Booking objects (each with 4 attributes and presumably each members of the BookingsCollection)?
If this is the case, what is the format for referencing the attributes of each object? When I set up a non-backbone-relational mini-app, I ended up in a situation whereby I could just reference the attributes using Client.Attribute or Booking[0].EventDate for example. I don't seem to be able to do this with the format you have outlined above.
Thanks again.
The JSON being returned is not what Backbone or Backbone-Relational is expecting by default.
The expectation of Backbone and Backbone-Relational is:
{
"id": "123456",
"Title":"Mr",
"FirstName":"Joe",
"Surname":"Bloggs",
"Bookings": [
{
"id": "585462542",
"EventName": "Bla",
"Location":"Dee Bla",
"EventDate":"November 1, 2012"
},
{
"id": "585462543",
"EventName": "Bla",
"Location":"Dee Bla",
"EventDate":"November 1, 2012"
}
]
}
To use your response, you need to create a parse function on the Client model that returns the structure I've posted above.
A jsFiddle example of your model definitions working with my example JSON: http://jsfiddle.net/edwardmsmith/jVJHq/4/
Other notes
Backbone expects ID fields to be named "id" by default. To use another field as the ID for a model, use Model.idAttribute
The "key" for the Bookings Collection I changed to "Bookings"
Sample Code:
Client = Backbone.RelationalModel.extend({
urlRoot: '/URL-THAT-RETURNS-JSON/',
relations: [
{
type: Backbone.HasMany,
key: 'Bookings',
relatedModel: 'Booking',
collectionType: 'BookingsCollection'
}
],
parse: function (response) {
},
initialize: function (options) {
console.log(this, 'Initialized');
}
});
Booking = Backbone.RelationalModel.extend({
initialize: function (options) {
console.log(this, 'Initialized');
}
});
BookingsCollection = Backbone.Collection.extend({
model: Booking
});
myClient = new Client( {
"id": "123456",
"Title":"Mr",
"FirstName":"Joe",
"Surname":"Bloggs",
"Bookings": [
{
"id": "585462542",
"EventName": "Bla",
"Location":"Dee Bla",
"EventDate":"November 1, 2012"
},
{
"id": "585462543",
"EventName": "Bla",
"Location":"Dee Bla",
"EventDate":"November 1, 2012"
}
]
});
console.log(myClient);​
Post Edit
Yes, the JSON pretty much defines the attributes of the model. You can use a combination of parse(), defaults, and validate() to better control what attributes are valid and allowed.
The canonical way of reading and setting properties on a Backbone Model is through the get(), escape(), and set() functions.
set is especially important as this does a bunch of housekeeping, such as validating the attribute and value against your validate function (if any), and triggering change events for the model that your views would be listening for.
In the specific case of the situation in this answer, you might
myClient.get('Title'); // => "Mr"
myClient.get('Bookings'); //=> an instance of a BookingsCollection with 2 models.
myClient.get('Bookings').first().get('Location'); //=> "Dee Bla"
myClient.get('Bookings').last().get('Location'); //=> "Dee Bla"
myClient.get('Bookings').first().set({location:"Bora Bora"});
myClient.get('Bookings').first().get('Location'); //=> "Bora Bora"
myClient.get('Bookings').last().get('Location'); //=> "Dee Bla"

Categories

Resources