Is there a better way to get subcontrol name? - qooxdoo

I am trying to get a reference in the "scrollbar-y" of the class qx.ui.list.List
Using the createChildControl event how can I check if the widget is the one named "scrollbar-y"?
So far I found two ways of which none seems elegant but both seem to get the job done
this.__list = new qx.ui.list.List()
this.__list.addListener("createChildControl", this.__onListCreateChildControl, this);
and later
__onListCreateChildControl: function (e){
debugger;
var child = e.getData();
if (child.constructor === qx.ui.core.scroll.ScrollBar && child.getOrientation() === "vertical") {
child.addListener("scroll", this.__onListScroll, this);
}
},
This checks implicitly. Apparently if it is a scrollbar and it is vertical it is our y scrollbar. Yeah it kinda looks like a duck but I have to check for both
if (quacks like one && walks like one)
The other way is
__onListCreateChildControl: function (e){
debugger;
var child = e.getData();
if (child.$$subcontrol === 'scrollbar-y') {
child.addListener("scroll", this.__onListScroll, this);
}
},
which uses the internal variable $$subcontrol. This works fine but it uses qooxdoo internals which seems like a hack.
P.S. I did try getChildControl('scrollbar-y') in various phases but since it is created in "as needed" basis I always get null.

You're right! There is no "straightforward" possibility to retrieve the ID (or name) of a widget created as a child of another widget in terms of child control creation.
Therefore I've submitted a PR to github which does exactly that: namely retrieving the id/name of a child control by exposing the internal $$subcontrol variable via a method getSubcontrolId https://github.com/qooxdoo/qooxdoo/pull/9140
The PR is currently in review state.

Related

Mutating array within an array (Polymer iron-list)

I currently have an iron-list within another iron-list. The parent's data comes from a firebase-query element, and the child's data is computed from each parent item. The db structure and code looks a bit like this:
DB: [
category1: [
itemId1: {
price: 10,
title: "title"
}
]
]
<iron-list id="categoryList" items="{{categories}}" multi-selection as="category">
<template>
<div class="category-holder">
<iron-list id="{{category.$key}}" items="{{_removeExtraIndex(category)}}" as="item" selection-enabled multi-selection selected-items="{{selectedItems}}" grid>
<template>
<div class$="{{_computeItemClass(selected)}}">
<p>[[item.title]]</p>
<p>[[item.price]]</p>
</div>
</template>
</iron-list>
</div>
</template>
</iron-list>
After selecting any number of items, the user can tap on a fab to batch edit the price. This is where I'm having issues. I can't figure out how to access the correct child iron-list in order to call list.set...I'm currently trying the following very nasty method:
var categories = this.$.categoryList;
var categoryItems = categories.items;
(this.selectedItems).forEach(function(item) {
var index = item.itemId;
categoryItems.forEach(function(itemList, categoryIndex) {
if (itemList[index]) {
categories.set('item.' + categoryIndex + '.price', 10);
}
}, this);
}, this);
I'm iterating over the selected items in order to extract the item index and then iterating over the parent iron-list data (categoryItems) in order to check if the given item exists in that subset of data. If so, then I use the category index and attempt to call set on the parent iron-list using the given path to access the actual item I want to edit. As expected, this fails. Hopefully I've made myself clear enough, any help would be appreciated!
EDIT #1:
After much experimenting, I finally figured out how to correctly mutate the child iron-list:
(this.selectedItems).forEach(function(item) {
var list = this.$.categoryList.querySelector('#' + item.category);
var index = list.items.indexOf(item);
list.set(["items", index, "price"], 30);
}, this);
A couple of things worth noting. I'm using querySelector instead of the recommended this.$$(selector) because I keep running into a "function DNE" error. But now I have another problem...after calling the function, the value gets updated correctly but I get the following error:
Uncaught TypeError: inst.dispatchEvent is not a function
Here's a picture of the full error message:
I see the light, hopefully someone can help me out!
OK, I'll take a shot at this. I think the following happens, and I guess this based on how dom-repeat works:
var categories = this.$.categoryList;
var categoryItems = categories.items;
You take the variable that the iron-list is based on, but setting one array to another just creates a reference in javascript. As soon as you update categoryItems, you also update this.$.categoryList.items. When you later sets the new value, iron-list will do a dirty check and compare all subproperties, and because they are equal (because ... reference), the iron-list wont update the dom.
What you should do is to make sure it's a totally new copy and the way of doing that is to use JSON.parse(JSON.stringify(myArray)).
Further on, one major flaw I see in your code is that you're using querySelector to select an element, and then manipulate that. What you should do is to use this.categories and only that variable.
So your method should look something like:
// Get a freshly new array to manipulate
var category = JSON.parse(JSON.stringify(this.categories);
// Loop through it
category.forEach(category) {
// update your categoryList variable
}
// Update the iron list by notifying Polymer that categories has changed.
this.set('categories', category);

Marionette Regions and routing

I'm using a LayoutView to display a collection in table form. When a user clicks on a tr I swap the CompositeView for an ItemView that shows the details using the same region. It all works except the functionality of the back button is broken. Is there a way to trap the back event and switch views?
Or should I use two Views and pass the model id and then refetch the model? The problem with that though is the extra request and I lose the filter and sort values of the table unless I use local storage.
Including more code would be better, but in any case I will try to give some guidance for your problem.
To avoid fetching the data twice, you can keep a common object in a "parent" component, for example in the Router.
var theObject;
var router = Marionette.AppRouter.extend({
routes: {
"routeA/:id": "goToRouteA",
"routeB/:id": "goToRouteB"
},
goToRouteA: function(id) {
MyRegion.show(new myLayout({
model: this._getCommonObject(id)
}));
},
goToRouteB: function(id) {
MyRegion.show(new myLayout({
model: this._getCommonObject(id)
}));
},
/*Return the common object for the views*/
_getCommonObject: function(id) {
theObject = (theObject && theObject.get('id') == id) ? theObject : MyApp.request('getTheObject');
return theObject;
}
});
In this way, you can keep the reference to the same object without loosing information.
You just have to make sure to delete the object when it is not needed to avoid keeping old information, for example on the Region close event.

Backbone - use different views or templates for same collection

The base model of my application has a status attribute.
Let's assume, for simplicity, that status might be either pending or deleted.
I have an upper menu with these two status values, when you click one of them you see all objects with this status (I use router to trigger a filter).
My problem is that I need to draw a different template for each status.
deleted object has delete forever and recover buttons
pending object has delete, edit and some other buttons (also some textarea...)
I wonder what would be the best solution for this problem.
I thought of creating a different view for each status, but then I don't know how to deal with it in the collection level.
I also thought of creating different templates and deal with it in the model-view level, but again - I have no idea whether it is possible and if yes - how.
Finally, I can solve it with same template and view, hiding what is not necessary inside the view, but then the code becomes quite ugly in my point of view.
Ideas?? Thanks!
If you want to create a different view for each status, you do it this way :
Router {
clickDeletedMenu : {
var collection = new MyCollection();
var deletedView = new DeletedView({ model : collection });
collection.fetch({ status : 'deleted' }); // filter deleted objects
},
clickPendingMenu : {
var collection = new MyCollection();
var pendingView = new PendingView({ model : collection });
collection.fetch({ status : 'pending' }); // filter deleted objects
},
}
If you want to create differents templates, you do it this way :
View {
render : {
if (this.model.status == 'deleted') {
// render deleted template
} else {
// render pending template
}
}
}
Finally, in my point of view, you can use the same template and view, and hiding what is not necessary inside the template not the view.
nb : the code is used just to illustrate the idea, it's not going to execute :)

breeze - how to subscribe to changes on a many-to-many relationship

I've got a many-to-many relationship in Breeze:
Product *---1 ProductWidgets 1----* Widgets
Product needs to know when any of it's Widgets changes. Widgets can be added or removed from Products at any time.
Ideally, I'd want to do something like:
product.widgets.on('change', function () {});
...but I'm imagining I need something like:
var handleWidgetChange = function (changes) {
console.log("here are the changes", changes);
};
for(var i = 0; i < product.productWidgets.length; i++) {
// make sure we're getting events for the current set of widgets
product.productWidgets[i].widget.entityAspect.propertyChanged.subscribe(handleWidgetChange);
// if any of the current set of product widgets gets pointed elsewhere, catch that
product.productWidgets[i].entityAspect.propertyChanged.subscribe(function (change) {
if (change.propertyName === "widget") {
change.oldValue.entityAspect.propertyChanged.unsubscribe();
change.oldValue.entityAspect.propertyChanged.subscribe(handleWidgetChange);
}
})
}
// handle new product widgets and removal of product widgets
product.productWidgets.arrayChanged.subscribe(function (change) {
if (change.added) {
change.added[0].widget.entityAspect.propertyChanged.subscribe(handleWidgetChange);
} else if (change.removed) {
change.removed[0].widget.entityAspect.propertyChanged.unsubscribe();
}
});
Is there a recommended way to achieve this?
(Note: I'm using angular, and would love to just $watch('product.productWidgets', function () {}, true) but that gives a circular reference error.)
Memory leaks are a huge risk in JavaScript, in part because there are no weak references. You must be careful with events. You really don't want to iterate over entities adding and removing subscriptions.
You also do not want to use Angular watches for monitoring model changes because you'll drive UI performance into the ground. There are too many entities with too many properties and you'll surely make a mistake by leaving watches in place long after you should have stopped watching.
Fortunately, Breeze provides a central entity change monitoring facility. A Breeze EntityManager listens for changes to any of the entities it holds in cache.
var widgetType = manager.metadataStore.getEntityType('Widget');
var productWidgetType = manager.metadataStore.getEntityType('ProductWidget');
entityManager.entityChanged.subscribe(entityChanged);
function entityChanged(changeArgs) {
var entity = changeArgs.entity;
if (entity.entityType === productWidgetType ||
entity.entityType === widgetType) {
// do what you do when someone does something to an entity of this type
// perhaps call back into a method on that instance that knows what to do
entity.somethingChanged(changeArgs.entityAction);
}
}
This one event notifies you of any change to any entity in the manager's cache. It will be called frequently so be crisp in your evaluation. For example, consider deactivating your event handler during queries.
The changeArgs.entityAction tells you what just happened to the entity. There are many actions that trigger this event: a property could change, its EntityState could change (add/modify/delete/detach), etc.
You don't have to worry about the product.productWidgets array. When a ProductWidget is added or removed from that array, the ProductWidget.productId foreign key will change ... and you're picking that up in this entityChanged handler.
There is no need to worry about a memory leak because the EntityManager already holds a reference to the entity and will continue to do so until you detach the entity or dispose of the EntityManager instance (and all of your own or the UI's references to the entity). That, to my mind, is appropriate lifetime management.

advice on how to design an extendable backbone view

I have the following backbone application.
It's a generic crud view, with the following template:
<div id="<%= crudId %>">
<div class="header-view"></div>
<div class="table-view"></div>
<div class="form-view"></div>
</div>
You can see the crud live here: http://bbbootstrap.com.ar/index.html#Wine
The view itself has subviews, to be rendered in the table-view and the form-view.
The thing is I want it to be a base crud view, and to be easily entendable, adding new subviews, for example, adding a new panel to issue some bulk operations.
These are the possible solutions I came out with so far
1- inheritance: create a new CrudBulkView inheriting from CrudView, modify the template to have a bulk-view place holder.
pro: inheritance can provide quite an elegant and simple solution
cons: it's a bit limiting, I'd like to just be able to compose the BulkView and add it to the CrudView.
2- add a method to crudview like addView(view, place) with place being something like 'beforeForm', 'afterForm', 'beforeTable', etc... (it's much too hardcoded...
cons: too hardcoded
3- pass a function with each subview I want to add, that takes care of creating the dom and attaching to it, right after CrudView has rendered the container. the method could be called setEl and return the newly created el.
pro: really flexible
cons: adds some complexity to the process of attaching the subview to the dom
4-modify the crudView template and then attach to it, something like this:
<div id="<%= crudId %>">
<div class="header-view"></div>
<div class="table-view"></div>
<div class="form-view"></div>
<div class="bulk-view"></div
</div>
then bulkView.el would be '.bulk-view'
pro: simple approach
cons: have to mess around with strings, instead of dealing with the dom
I think it's not so strange what I'm trying to achieve. I just want to add a view to a container view, being as much decoupled as possible, and being able to establish where it should be rendered.
After reading your response to my previous answer I went through and modified my example to hopefully give you an idea of how you can implement a system with named views that allows you to control the ordering as you desire. Let me know if this helps or if you have any questions about how it works.
var viewCtor = Backbone.View.prototype.constructor;
// Assuming we have a reference to the subviews already
var BaseCrudView = Backbone.View.extend({
// This is null for an important reason, see comment in constructor
subViews: null,
// Override the constructor instead of initialize since this is meant to be a base object, so things that
// inherit don't have to remember to call the parent inialize every time.
constructor: function() {
viewCtor.apply(this, arguments);
// It is important this is initialized when instantiating the view rather than in the prototype.
// Backbone's extend() will "copy" the prototype properties of the parent when extending, which really
// just performs an assignment. If this were initialized above in the prototype then all children
// that inherit from that prototype would share the exact same instance of the array/object. If a child
// adds something to the array, it would be changed for all instances that inherit from the parent.
this.subViews = {
header: new HeaderView(),
table: new TableView
};
this.subViewOrder = [
'header',
'table'
];
},
addBefore: function(subView, name, beforeView) {
this.subViews[name] = subView;
var viewLoc = this.subViewOrder.indexOf(beforeView);
if(viewLoc == -1) {
viewLoc = 0;
}
this.subViewOrder.splice(viewLoc, 0, name);
},
addAfter: function(subView, name, afterView) {
this.subViews[name] = subView;
var viewLoc = this.subViewOrder.indexOf(afterView);
if(viewLoc == -1) {
viewLoc = this.subViewOrder.length - 1;
}
this.subViewOrder.splice(viewLoc + 1, 0, name);
},
moveBefore: function(name, beforeView) {
this.addBefore(this.subViews[name], name, this.subViewOrder.splice(this.subViewOrder.indexOf(name), 1));
},
moveAfter: function(name, afterView) {
this.addAfter(this.subViews[name], name, this.subViewOrder.splice(this.subViewOrder.indexOf(name), 1));
},
render: function() {
var that = this;
_.each(this.subViewOrder, function(viewName) {
// Assumes the render() call on any given view returns 'this' to get 'el'
that.$el.append(this.subViews[viewName].render().el);
});
return this;
}
});
var BulkCrudView = BaseCrudView.extend({
inialize: function() {
// Skipping the last parameter causes it to insert at the end
this.addAfter(new BulkView(), 'bulkView');
}
});
With this you could easily extend the BulkCrudView and modify its subViews array in initialize to add/insert whatever you want. Though, it'd work just as well to instantiate a BaseCrudView and work with the view methods. Just whatever feels cleaner and/or floats your boat.

Resources