I've got a parent class ParentPanel that extends Ext.form.Panel in Ext JS 5. The parent panel is roughly defined as follows:
Ext.define('...ParentPanel', {
config : {
isItTrue : true
},
dockedItems: [],
initComponent : function () {
var me = this;
//If some evaluation is true...
if (isItTrue) {
me.dockedItems.push({
// the components I am trying to add...
});
this.callParent():
}
//....
The basic problem is that any class extending ParentPanel will call the initComponent function, adding the items to the parent dockedItems config. for every true evaluation (so I end up with repetitions as each child is sharing the parent's dockedItems config.). What I want to do is to only have the item added to the inheriting classes dockedItems configuration, not the parent.
Is this even possible is Ext JS? If so, understanding the design issue, how would I work around this?
Something like this:
initComponent: function () {
this.dockedItems = [];
// ...
}
ExtJS doesn't change the nature of Javascript...
Related
This is a contrived example but it is similar to real-life situations where, for example, you might have a list of links built from data that you are AJAXing in from a server.
import {Component, e, render} from './node_modules/bd-core/lib.js';
// a list of strings that for alert when you click them
class AlertLinkList extends Component.withWatchables('data') {
handleClick(event){
alert(event.target.innerHTML);
}
bdElements() {
return e.div(
{
// bdReflect: ?????
},
['yes', 'no', 'maybe'].map(
txt => e.div({bdAdvise: {click: 'handleClick'}}, txt)
)
)
}
}
var linkList = render(AlertLinkList, {}, document.body);
// I would like to change the strings but this (obviously) does nothing
linkList.data = ['soup', 'nuts', 'fish', 'dessert'];
I can't think of a straightforward way to solve this.
bdReflect only works on writable DOM attributes, I think, so for example I could use it to replace the innerHTML of the component but then I think I lose the bdAdvise assignments on the links (and it also seems kinda kludgey).
Any ideas?
OK here's one pattern that works for this...
get rid of the watchables in AlertLinkList
instead, use kwargs to populate the list
wrap the list in another component that simply re-renders the list with new content whenever the content changes (e.g. after fetching new content from the server)
// a list of strings that alert when you click them
class AlertLinkList extends Component {
handleClick(event){
alert(event.target.innerHTML);
}
bdElements() {
return e.div(
this.kwargs.items.map(
txt => e.div({bdAdvise: {click: 'handleClick'}}, txt)
)
)
}
}
// a wrapper that provides/retrieves data for AlertLinkList
class LinkListWrapper extends Component {
bdElements() {
return e.div(
{},
e.a(
{bdAdvise: {click: 'updateList'}},
'Click to Update List',
),
e.div({bdAttach: 'listGoesHere'}),
);
}
updateList(event) {
// the data below would have been retrieved from the server...
const resultRetrievedFromServer = ['soup', 'nuts', 'fish', 'dessert'];
this.renderList(resultRetrievedFromServer)
}
renderList(items) {
render(AlertLinkList, {items}, this.listGoesHere, 'only')
}
postRender() {
const initialData = ['yes', 'no', 'maybe']
this.renderList(initialData);
}
}
var linkList = render(LinkListWrapper, {}, document.body);
The only issue I see here is that it may be suboptimal to re-render the entire wrapped component if only one small part of the data changed, though I suppose you could design around that.
Let's begin solving this problem by describing the public interface of AlertLinkList:
A component that contains a homogeneous list of children.
The state of each child is initialized by a pair of [text, url].
The list is mutated en masse.
Given this, your start is almost perfect. Here it is a again with a few minor modifications:
class AlertLinkList extends Component.withWatchables('data') {
handleClick(event) {
// do something when one of the children is clicked
}
bdElements() {
return e.div({}, this.data && this.data.map(item => e(AlertLink, { data: item })));
}
onMutateData(newValue) {
if (this.rendered) {
this.delChildren();
newValue && newValue.forEach(item => this.insChild(AlertLink, { data: item }));
}
}
}
See https://backdraftjs.org/tutorial.html#bd-tutorial.watchableProperties for an explanation of onMutateData.
Next we need to define the AlertLink component type; this is trivial:
class AlertLink extends Component {
bdElements() {
return e.a({
href: this.kwargs.data[1],
bdAdvise: { click: e => this.parent.handleClick(e) }
}, this.kwargs.data[0]);
}
}
The outline above will solve your problem. I've written the pen https://codepen.io/rcgill/pen/ExWrLbg to demonstrate.
You can also solve the problem with the backdraft Collection component https://backdraftjs.org/docs.html#bd-core.classes.Collection
I've written a pen https://codepen.io/rcgill/pen/WNpmeyx to demonstrate.
Lastly, if you're interested in writing the fewest lines of code possible and want a fairly immutable design, you don't have to factor out a child type. Here's a pen to demonstrate that: https://codepen.io/rcgill/pen/bGqZGgW
Which is best!?!? Well, it depends on your aims.
The first solution is simple and general and the children can be wrangled to do whatever you want them to do.
The second solution is very terse and includes a lot of additional capabilities not demonstrated. For example, with the backdraft Collection component
mutating the collection does not destroy/create new children, but rather alters the state of existing children. This is much more efficient and useful when implementing things like large grids.
you can mutate an individual elements in the collection
The third solution is very terse and very fixed. But sometimes that is all you need.
I'm creating custom UI components using ES6 classes doing something like this:
class Dropdown {
constructor(dropdown) {
this.dropdown = dropdown;
this._init();
}
_init() {
//init component
}
setValue(val) {
//"public" method I want to use from another class
}
}
And when the page load I initiate the components like this:
let dropdown = document.querySelectorAll(".dropdown");
if (dropdown) {
Array.prototype.forEach.call(dropdown, (element) => {
let DropDownEl = new Dropdown(element);
});
}
But now I need to acces a method of one of these classes from another one. In this case, I need to access a method to set the value of the dropdown
based on a URL parameter, so I would like to do something like:
class SearchPage {
//SearchPage is a class and a DOM element with different components (like the dropdown) that I use as filters. This class will listen to the dispached events
//from these filters to make the Ajax requests.
constructor() {
this._page = document.querySelector(".search-page")
let dropdown = this._page.querySelector(".dropdown);
//Previously I import the class into the file
this.dropdown = new Dropdown(dropdown);
}
setValues(val) {
this.dropdown.setValue(val);
//Set other components' values...
}
}
But when I create this instance, another dropdown is added to the page, which I don't want.
I think an alternative is to create the components this way, inside other ones, and not like in the first piece of code. Is this a valid way? Should I create another Dropdown class that inherits from the original one?
A simple solution is to store the Dropdown instance on the element to avoid re-creating it:
class Dropdown {
constructor(element) {
if (element.dropdown instanceof Dropdown)
return element.dropdown;
this.element = element;
element.dropdown = this;
//init component
}
…
}
This code is an example from Marionette:
AppLayout = Backbone.Marionette.Layout.extend(
{
template: "#layout-template",
regions:
{
menu: "#menu",
content: "#content"
}
});
var layout = new AppLayout();
layout.menu.show(new MenuView());
layout.content.show(new MainContentView());
The last two lines confuse me. Why doesn't it read:
layout.regions.menu.show(new MenuView());
layout.regions.content.show(new MainContentView());
Can someone please explain why layout.menu works and layout.regions.menu doesn't?
What if I wanted to access template? Wouldn't that be layout.template? template and regions are at the same depth inside layout.
Here is the constructor function from the marionette code:
// Ensure the regions are avialable when the `initialize` method
// is called.
constructor: function () {
this._firstRender = true;
this.initializeRegions();
var args = Array.prototype.slice.apply(arguments);
Marionette.ItemView.apply(this, args);
},
I believe it was implemented that way because 'layout.menu' is shorter and simpler than 'layout.regions.menu'. Looks like you expected the literal "#menu" to be replaced with a region manager object.
The options you passed in when creating the view, including the template, can be found in layout.options. So in your case layout.options.template should equal '#layout-template', and the regions definition hash would be at layout.options.regions... still the same level.
Unless there is more to the example then you are showing like the Backbone.Marionette.Layout methods, then its not accessing regions.menu like you think it is.
With just the code you have provided the code above is actually creating a menu attribute, which then has a show attribute so your layout object would actually look like this:
layout {
menu : {
show : new MenuView
},
content : {
show : new MainContentView
},
template: "#layout-template",
regions:
{
menu: "#menu",
content: "#content"
}
}
In javascript the (dot) operator can be used to access a property of an attribute or if no property with that name exists then it will create that property.
I'm not familiar with the backbone.js framework but my guess is that they provide for skipping part of the property lookup chain. which means that the above would end up producing this as your layout object:
layout {
template: "#layout-template",
regions:
{
menu : {
show : new MenuView
},
content : {
show : new MainContentView
}
}
}
But again that's just a guess on my part since I don't use backbone.
You can learn more about the object model and how it works with inheritance right here.
I have the following controller in ExtJs:
Ext.define('FileBrowser.controller.BrowserController', {
extend: 'Ext.app.Controller',
views: ['browser.tree_dir', 'browser.grid_file'],
stores: ['store_dir', 'store_file'],
init: function () {
this.control({
'window > tree_dir': {
itemclick: {
fn: function (view, record, item, index, event) {
if (record.isLeaf() == false) {
Ext.getStore('store_file').load({
params: {
dir: record.data.id
}
});
var parentOfCurrentFiles = record.data.id
nodeId = record.data.id;
htmlId = item.id;
var grid_view = this.getView('browser.grid_file');
var grid_view_v = grid_view.getView();
grid_view_v.refresh();
}
}
}
}
});
},
onPanelRendered: function () {
console.log('The panel was rendered');
}
});
If you notice under 'itemclick' I am trying to refresh one of my views, my approach is not working. Can anyone explain to me how I can refresh the view? Thank you.
Replace var grid_view= this.getView('browser.grid_file'); with var grid_view= this.getView('browser.grid_file').create(); to get a real instance (as I already told you, getView() only return the view config, not a instance!) or if you have already created that grid and only one instance exist use the xtype along with a component query to receive it var grid_view=Ext.ComponentQuery('grid_file')[0]
Now to the refresh()
Basically you never need to call this method cause your grid is bound to a store and any change made on this store is directly reflected to your grid.
I would also recommend you to store view instances when creating them instead of using queries or directly use the ref property and let ExtJS do the work for you. The last one will the best solution you I guess... Take a look at ref's within the API examples and give it a try.
So what you are trying to do is, load the store and have the data reflect once you refresh the grid_view...?
In that case, you haven't done a setStore() to the grid, or if you have done that elsewhere, you are't doing a setData() to the store. Also you should call the refresh on the grid.
I've faced with this problem many times when optimize my Sencha Touch 2 apps.
It's obvious that I should keep the DOM light-weighted, my application contains an Ext.TabBar and a main Ext.Container above. Everytime when switch from a view to another, I simply remove current view and add new view to that Container.
But the problem is, there are some views which have customized data. I mean they have inner data such as html content, filtered store records, etc. When I remove them from my main Container, I want to somehow save their "states" to a global variable, for example: I'm doing an e-Commerce app with products with details. When remove details panel from main container, I want to do something like this:
var saved_detail_panel = mainContainer.getActiveItem();
if I could do that, later when I want to add that detail panel back to main container, I can simply use:
mainContainer.add(saved_detail_panel);
I've tried many times but could not find one that works yet.
Highly appreciate for any helps. Thank you.
Updated:
When I put this code in my event handler in a controller:
var temp = Ext.create('taxi.view.location.LocationPanel', {showAnimation: null});
taxi.main_container = temp;
It works well but is not performant. The thing I want to do is to create it once only in my app.js, like this:
launch: function(){
var temp = Ext.create('taxi.view.location.LocationPanel', {showAnimation: null});
};
and only use this in Controller:
taxi.main_container = temp;
It works for the first time. But in the second time, it shows this error:
Uncaught TypeError: Cannot call method 'replaceCls' of null
Can you use the app's "global namespace"? Then you can reference MyApp.savedValue anywhere in your app?
Ext.application({
name: 'MyApp',
// views: [],
// models: [],
// stores: [],
controllers: ['Main'],
launch: function() {
MyApp.savedValue = "Hello word";
Ext.Viewport.add(Ext.create('Sencha.view.tablet.MainView'));
}
});
One other idea in the Sencha examples is in the KitchenSink demo. In app/controllers/Main.js, they use a view cache, which is setup in the config:{} and accessed via a getter/setter. I think the main controller always persists so your cache is always available. In fact, aren't all of your controllers persist if they're loaded in the app.js?
controllers: ['Main','FooController', 'BarController'],
Snippets from: app/controllers/Main.js
config: {
/**
* #private
*/
viewCache: [], // Accessed via getViewCache(), setViewCache()
...
},
createView: function(name) {
var cache = this.getViewCache(), // Implied getter
ln = cache.length,
limit = 20, // max views to cache
view, i, oldView;
// See if view is already in the cache and return it
Ext.each(cache, function(item) {
if (item.viewName === name) {
view = item;
return;
}
}, this);
if (view) {
return view;
}
// If we've reached our cache limit then remove something
if (ln >= limit) {
for (i = 0; i < ln; i++) {
oldView = cache[i];
if (!oldView.isPainted()) {
oldView.destroy();
cache.splice(i, 1);
break;
}
}
}
// Create the view and add it to the cache
view = Ext.create(name);
view.viewName = name;
cache.push(view);
this.setViewCache(cache); // Implied setter
return view;
},
try this,
var appNS = new Ext.application({
name: 'app',
//others
launch: function () {
appNS.SavedValue = 'getting start..........';
},
//others
});
and then you can use it inside of controller
console.log(appNs.SavedValue);
i hope it might be helpful.
another soln is caching ur views. it is not actually caching.
first add an array to your application like .
Ext.application({
name: 'app',
viewCache: [],
viewCacheCount: 0,
launch: function () {
this.viewCache.push("app init......."); // push your view
}
});
and this can be retrive inside of any controller something like
ths.getApplication().viewCache.pop(); //pop your view
I dont know it might create some problem in case of some views/components that are auto destroyable.
Try to add autoDestroy: false in your view.
Ext.define('JiaoJiao.view.personal.Card', {
extend: 'Ext.NavigationView',
xtype: 'personalContainer',
config: {
tab: {
title: '个人',
iconCls: 'user',
action: 'personalTab'
},
autoDestroy: false,
items: [
{
xtype:'personal',
store: 'Personals'
}
]
}
});