Ember - binding nested arrays - javascript

I have an Ember component that takes in an array as it's content and for each item renders a checkbox if it's an object or a new nested instance if it's an array.
The component needs to be able to bind data both to the first set of items in the root array and all the nested items. I though of using computed properties and observers but as Ember states You cannot use nested forms like todos.#each.owner.name or todos.#each.owner.#each.name.
Any idea how this can be achieved?
Component:
<dl class="selection-list">
{{#each content}}
<dd>
{{#isArray this}}
{{selection-list content=this}}
{{else}}
<label class="checkbox">
{{input type="checkbox" checked=selected disabled=disabled}}
{{label}}
</label>
{{#isArray children}}
{{selection-list content=children}}
{{/isArray}}
{{/isArray}}
</dd>
{{/each}}
</dl>
Model:
App.ApplicationRoute = Ember.Route.extend({
model: function () {
return {
items: [
{
label: '1'
},
{
label: '2',
children: [
{
label: '2.1'
},
{
label: '2.2',
children: [
{
label: '2.2.1'
},
{
label: '2.2.2'
},
{
label: '2.2.3'
}
]
}
]
},
{
label: '3'
},
{
label: '4'
}
]
};
}
});
Use of binding:
App.ApplicationController = Ember.ObjectController.extend({
selectionCount: function () {
return this.get('items').filterBy('selected').length;
}.property('items.#each.selected')
});
Example:
http://jsbin.com/yipuw/1/edit?html,js,output

I solved this using views. The key points are:
Changed the component to a view. We want to keep the same context for N levels of nesting.
Used Ember.Checkbox and handled the change event in there so we can modify selectCount as clicks happen instead of iterating through the list of checkboxes every time one of them changes.
Check it out: http://jsbin.com/nuzex/1/edit?html,js,console,output

I really don't feel like we've arrived at 'the' solution for the nested array binding issue as of yet. I've extended your bins with my own current working solution. The key to this is a pseudo-property whose only mission in life is to notify parents that something changed somewhere down the family tree.
http://jsbin.com/yivif/2/edit?html,js,output
Actually, here's a slightly tighter version
http://jsbin.com/yivif/10/edit?html,js,output

Related

nouisliders dissapear when moving them on some event

This is very much related to my first question about nousliders here: How to update div in Meteor without helper? (this title was not well chosen, because it was not about avoiding helpers)
The answer provided by Jankapunkt works very well, for example I can have 4 sliders, and reordering works without loosing slider states like min/max like this:
now I want some of the elements to be non-sliders, for example change 1 to a dropdown:
but when I click the switch button, 1 slider dissapears (the one that moves to the dropdown spot), and I get an error in console:
Exception in template helper: Error: noUiSlider (11.1.0): create requires a single element, got: undefined
I don't understand why adding if/else makes any difference ... the slider helper is waiting for ready {{#if ready}}...{{/if}} so it should work ? anyone understand why it doesn't ? and how to fix it ?
template onCreated looks now like this:
Template.MyTemplate.onCreated(function() {
const sliders = [{
id: 'slider-a',
prio: 1,
type: "NumberRange",
options: {
start: [0, 100],
range: {
'min': [0],
'max': [100]
},
connect: true
}
}, {
id: 'slider-b',
prio: 2,
type: "NumberRange",
options: {
start: [0, 100],
range: {
'min': [0],
'max': [100]
},
connect: true
}
}, {
id: 'dropdown-c',
prio: 3,
type: "Dropdown"
}, {
id: 'slider-d',
prio: 4,
type: "NumberRange",
options: {
start: [0, 100],
range: {
'min': [0],
'max': [100]
},
connect: true
}
}, ]
const instance = this
instance.state = new ReactiveDict()
instance.state.set('values', {}) // mapping values by sliderId
instance.state.set('sliders', sliders)
})
and template now looks like this, there is an if else statement to show Dropdown or NumberRange (slider):
<template name="MyTemplate">
{{#each sliders}}
<div class="range">
<div>
<span>id:</span>
<span>{{this.id}}</span>
</div>
{{#if $eq type 'Dropdown'}}
<select id="{{this.id}}" style="width: 200px;">
<option value="">a</option>
<option value="">b</option>
<option value="">c</option>
</select>
{{else if $eq type 'NumberRange'}}
<div id="{{this.id}}">
{{#if ready}}{{slider this}}{{/if}}
</div>
{{/if}}
{{#with values this.id}}
<div>
<span>values: </span>
<span>{{this}}</span>
</div>
{{/with}}
</div>
{{/each}}
<button class="test">Switch Sliders</button>
</template>
First of all you should be aware of the situation:
You are mixing classic ui rendering (the sliders) with Blaze rendering (the dropdown). This will bring you a lot of design problems and the solution below is more a hack than a clean appraoch using Blaze's API
Since your components are not only sliders anymore, you should rename your variables. Otherwise it is hard for a foreign person to decode your variable's context.
Your dropdown has currently no values saved, switching the button resets the dropdown.
You can't destroy a noUiSlider on the dropdown when hitting the switch button, causing the error you described above.
Therefore I want to give you some advice on restructuring your code first.
1. Renaming variables
You can use your IDE's refactoring functionality to easily rename all your variable names. If you don't have such functionality in your IDE / editor I highly suggest you to start your search engine to get one.
Since you have more input types than sliders, you should use a more generic name, like inputs which indicates a broader range of possible types.
There should also be a value entry on your dropdown, do be able to restore the last selection state when re-rendering:
Template.MyTemplate.onCreated(function () {
const inputs = [{
id: 'slider-a',
prio: 1,
type: 'NumberRange',
options: {
start: [0, 100],
range: {
'min': [0],
'max': [100]
},
connect: true
}
}, {
id: 'slider-b',
prio: 2,
type: 'NumberRange',
options: {
start: [0, 100],
range: {
'min': [0],
'max': [100]
},
connect: true
}
}, {
id: 'dropdown-c',
prio: 3,
type: 'Dropdown',
value: '', // default none
}, {
id: 'slider-d',
prio: 4,
type: 'NumberRange',
options: {
start: [0, 100],
range: {
'min': [0],
'max': [100]
},
connect: true
}
},]
const instance = this
instance.state = new ReactiveDict()
instance.state.set('values', {}) // mapping values by sliderId
instance.state.set('inputs', inputs)
})
Now you also have to rename your helpers and the template helper calls:
<template name="MyTemplate">
{{#each inputs}}
...
{{/each}}
</template>
Template.MyTemplate.helpers({
inputs () {
return Template.instance().state.get('inputs')
},
...
})
2. Handle multiple input types on switch event
You also should rename your variables in the switch event. Furhtermore, you need to handle different input types here. Dropdowns have no .noUiSlider property and they also receive not an array but a string variable as value:
'click .test': function (event, templateInstance) {
let inputs = templateInstance.state.get('inputs')
const values = templateInstance.state.get('values')
// remove current rendered inputs
// and their events / prevent memory leak
inputs.forEach(input => {
if (input.type === 'Dropdown') {
// nothing to manually remove
// Blaze handles this for you
return
}
if (input.type === 'NumberRange') {
const target = templateInstance.$(`#${input.id}`).get(0)
if (target && target.noUiSlider) {
target.noUiSlider.off()
target.noUiSlider.destroy()
}
}
})
// assign current values as
// start values for the next newly rendered
// inputs
inputs = inputs.map(input => {
const currentValues = values[input.id]
if (!currentValues) {
return input
}
if (input.type === 'Dropdown') {
input.value = currentValues
}
if (input.type === 'NumberRange') {
input.options.start = currentValues.map(n => Number(n))
}
return input
}).reverse()
templateInstance.state.set('inputs', inputs)
},
3. Correct rendering / update display list
Now comes the problem of mixing Blaze rendering with classic DOM updates: until this point you will run into an error. This is mainly because now our createSliders function will expect a div element with a certain id at the place where the dropdown has been rendered before the switch has been pressed. It won't be there because the Blaze render invalidation will not be finished at this point.
Fixing this using autorun in onCreated or onRendered will easily increase complexity or even messes up your code. A simpler solution is to use a short timeout here:
Template.MyTemplate.helpers({
// ...
slider (source) {
const instance = Template.instance()
setTimeout(()=> {
createSliders(source.id, source.options, instance)
}, 50)
},
// ...
})
4. Bonus: saving state of dropdown
In order to save the state of the dropdown, you need to hook into it's change event. You therefore need to assign it a class to map the event independently of the id:
<select id="{{this.id}}" class="dropdown" style="width: 200px;">...</select>
For which now you can create an event:
'change .dropdown'(event, templateInstance) {
const $target = templateInstance.$(event.currentTarget)
const value = $target.val()
const targetId = $target.attr('id')
const valuesObj = templateInstance.state.get('values')
valuesObj[targetId] = value
templateInstance.state.set('values', valuesObj)
}
Now you have saved your current dropdown value but in order to restore it in the next render, you need to extend the options in the html:
<select id="{{this.id}}" class="dropdown" style="width: 200px;">
<option value="a" selected="{{#if $eq this.value 'a'}}selected{{/if}}">a</option>
<option value="b" selected="{{#if $eq this.value 'b'}}selected{{/if}}">b</option>
<option value="c" selected="{{#if $eq this.value 'c'}}selected{{/if}}">c</option>
</select>
This should now also display the last selected state of the dropdown.
Summary
You can use this pattern to include even more input components.
Be aware, that mixing Blaze rendering with traditional DOM manipulations can increase the complexity of your code a lot. Same applies for many other rendering systems / libraries / frameworks out there.
The setTimeout solution should be the last approach to be used when other approaches are even less feasible.
Variable and method naming should always represent their context. If the context changes -> rename / refactor variables and methods.
Please next time post the full code here again. Your other post might get updated or deleted and the full code won't be accessible here anymore, making it hard for others to find a good solution.

Ext Js Attach Event Handler In Parent Container To Its Child Item

A component definition:
Ext.define('Retroplanner.view.dimension.DimensionMapping', {
alias: 'widget.dimensionMapping',
extend: 'Ext.form.Panel',
...
items: [{
xtype: 'combo'
}, ...
]
A 'select' handler of the child item must create a widget and add this widget to the items array of its parent.
Inside of this child item, it its 'select' handler, I can find its parent by some search techniques. But I would like to avoid it if it is possible. I do not have a reference variable to the parent neither.
A better approach would be - to create function in the parent, and attach it somehow to the child item:
Ext.define('Retroplanner.view.dimension.DimensionMapping', {
...
onSiRemoteCombo: function(cmb, rec, idx) {
alert("select handler");
var newItem = Ext.widget('somexType');
this.items.add(newItem);
}
The question, how to attach onSiRemoteCombo?
I've found a similar solution here: How to create listener for child component's custom event
First, it does not work for me. I can give a full example that I tried to use.
2nd, I would like to create items via the most common way/in the common place, not via initComponent method. I would like to have something like:
Ext.define('Retroplanner.view.dimension.DimensionMapping', {
...
afterRender: function() {
var me = this;
//exception here
//Uncaught TypeError: Cannot read property 'on' of undefined
me.items[0].on('select', onSiRemoteCombo, this);
},
items: [{
xtype: 'combo'
}, ...
],
onSiRemoteCombo: function(cmb, rec, idx) {
alert("Ttt");
var dimensionMapping = Ext.widget('propGrid');
this.getParent().add(dimensionMapping);
}
But I get an exception:
//exception here
//Uncaught TypeError: Cannot read property 'on' of undefined
me.items[0].on('select', onSiRemoteCombo, this);
And also, attach a listener after each rendering, really is a bad idea.
Are there any best practices for such use cases? Ideally, if it will work in different versions of Ext JS, at least in 5.x and 6.x
Attach a handler in a child and access its parent? A child should not depend on its parent. Only parent should know, what to do.
One way to solve this is by wrapping the combo item component initialization into form's initComponent method. This way when setting a listener for the combo, you can use this.formMethod to reference a form method. Here is some code:
Ext.define('Fiddle.view.FirstForm', {
extend: 'Ext.form.Panel',
bodyPadding: 15,
initComponent: function () {
Ext.apply(this, {
items: [{
xtype: 'combo',
fieldLabel: 'First Combo',
store: ['first', 'second'],
listeners: {
'select': this.onComboSelect
}
}]
});
this.callParent();
},
onComboSelect: function () {
alert('I am a first form method');
}
});
The second approach by using a string listener on the combo, and by setting defaultListenerScope to true on the form. This way the listener function will be resolved to the form's method. Again, some code:
Ext.define('Fiddle.view.SecondForm', {
extend: 'Ext.form.Panel',
bodyPadding: 15,
defaultListenerScope: true,
items: [{
xtype: 'combo',
fieldLabel: 'Second Combo',
store: ['first', 'second'],
listeners: {
'select': 'onComboSelect'
}
}],
onComboSelect: function () {
alert('I am a second form method');
}
});
And here is a working fiddle with both approaches: https://fiddle.sencha.com/#view/editor&fiddle/27un

Ext JS Remove GUI Item From Its Container By xtype

A panel contains of 3 items. The last item has an event handler attache. In the handler a new item (widget) is added to the parent panel. Before adding a new item, an old item of the same xtype should be deleted.
Here is an example that does not work:
Ext.define('Retroplanner.view.dimension.DimensionMapping', {
extend: 'Ext.form.Panel',
defaultListenerScope: true,
items: [{
xtype: 'component',
html: '<h3> Dimension Mappings</h3>',
},{
xtype: 'bm-separator'
},{
xtype: 'siRemoteCombo',
...
listeners: {
'select': 'onSiRemoteCombo'
}
}
],
onSiRemoteCombo: function(cmb, rec, idx) {
var item;
for(item in this.items){
//here item - is undefined,
//although this.items.length=3, as expected
//alert(item.xtype);
//isXType is not defined for undefined element:
if (item.isXType('propGrid')) {
this.remove(item);
break;
}
}
//the following code works as expected, if the previous is commented
var dimensionMapping = Ext.widget('propGrid');
this.items.add(dimensionMapping);
this.updateLayout();
}
});
I tried to use index, but it also does not work:
Ext.define('Retroplanner.view.dimension.DimensionMapping', {
...
defaultListenerScope: true,
items: [{
xtype: 'component',
...
},{
xtype: 'bm-separator'
},{
xtype: 'siRemoteCombo',
...
listeners: {
'select': 'onSiRemoteCombo'
}
}
],
onSiRemoteCombo: function(cmb, rec, idx) {
//the following code does not remove item in GUI interface.
if (this.items.length == 4)
this.remove(this.items[3], true);
var dimensionMapping = Ext.widget('propGrid');
this.items.add(dimensionMapping);
this.updateLayout();
}
});
I would like to be able to remove item by xtype, without any id or other types of references. But if it is not possible, which is the best way to do it? To remove GUI component from its container.
Check out component queries. It allows you to search for ExtJS components based on their attributes, globally or from within a container.
For easy access to queries based from a particular Container see the
Ext.container.Container#query, Ext.container.Container#down and
Ext.container.Container#child methods. Also see Ext.Component#up.
For your case down or child are appropriate. Something like this:
this.remove(this.down('propGrid'))
Here is a working fiddle: https://fiddle.sencha.com/#view/editor&fiddle/27vh. Just select a value from the combo, and the grid will be removed.
To remove an item with the prodGrid xtype, try this:
onSiRemoteCombo: function(cmb, rec, idx) {
if (this.down('prodGrid'))
this.remove(this.down('prodGrid'))
var dimensionMapping = Ext.widget('propGrid');
this.items.add(dimensionMapping);
this.updateLayout();
}

Knockout js observable arrays and checkbox, checked bind fail

I'm trying to figure out why the text entry field is not active when the checkbox changes?
<form data-bind="foreach: editables">
<input type="checkbox" name="edit" data-bind=" checked: active" />
<input type="text" name="edit" data-bind="value: name, disable: !active" />
<br/>
</form>
var viewModel = function () {
this.editables = ko.observableArray(
[{
active: true,
name: "mi"
}, {
active: false,
name: "yo"
}, {
active: true,
name: "cel"
}]);
};
ko.applyBindings(new viewModel());
http://jsfiddle.net/legolito/2FAJN/2/
I hope that someone can helpme. (english isn't my native language, so i'm sorry if something is bad with my grammar )
Have you considered making the active property an observable?
http://jsfiddle.net/tzG3t/
var viewModel = function () {
this.editables = ko.observableArray(
[{
active: ko.observable(true),
name: "mi"
}, {
active: ko.observable(false),
name: "yo"
}, {
active: ko.observable(true),
name: "cel"
}]);
};
ko.applyBindings(new viewModel());
That because you are not using ko.observable in ko.observableArray
See the knockout documentation on observableArrays
Key point: An observableArray tracks which objects are in the array, not the state of those objects
Simply putting an object into an observableArray doesn’t make all of
that object’s properties themselves observable. Of course, you can
make those properties observable if you wish, but that’s an
independent choice. An observableArray just tracks which objects it
holds, and notifies listeners when objects are added or removed.
So make it observable and problem solved.
Fiddle: http://jsfiddle.net/2FAJN/4/

Ext JS 4: Grid List Filter is NOT updated

I am running a weird problem when I try to set Grid Filter list dynamically.
Let me explain by my code snippets
I have a column with filter list is defined as
{
text : 'Client',
dataIndex : 'topAccount',
itemId : 'exTopAccount',
filter: {
type: 'list',
options:[]
}
}
I initialize list from store in 'viewready'
viewready: function(cmp,eOpts){
cmp.getHeaderCt().child('#exTopAccount').initialConfig.filter.options = clientsStore.collect('topAccount');
}
===> WORKS GOOD
Now, I have to build the new client store based on the records when user moves to next page. Therefore I build the store in the 'change' event of paging
listeners: {
'change' :function( toolbar, pageData, eOpts ) {
var store = Ext.StoreManager.get('ExceptionRecords');
clientsStore.removeAll(true);
store.each(function(record){
if(clientsStore.findRecord('topAccount',record.data.topAccount.trim()) == null ) {
clientsStore.add({topAccount: record.data.topAccount.trim()})
}
})
Ext.getCmp('exceptionGridContainer').view.refresh;
Ext.getCmp('exceptionGridContainer').view.getHeaderCt().doLayout;
console.log(clientsStore);
Ext.getCmp('exceptionGridContainer').view.getHeaderCt().child('#exTopAccount').initialConfig.filter.options = clientsStore.collect('topAccount');
}
}
I can now see the new data in clientsStore . But Grid filter list is not updated. still showing old data. I tried refresh,layout etc. Nothing helps
Any help will be appreciated
Thanks
Tharahan
Just changing the value of a property does not affect the component rendered or computed state. The menu is created when the list is first initialized. The first time you do that, it works because that's before the initialization, but the second time, that's too late.
If you can grab a reference to the instantiated ListFilter, I think you could force the recreation of the menu this way:
listFilter.menu = listFilter.createMenu({
options: [ ... ] // new options
// rest of the filter config
});
So, supposing you have a reference to your target grid, you could change the options for the column with dataIndex of "topAccount" by a call similar to this:
var listFilter = grid
.findFeature('filters') // access filters feature of the grid
.get('topAccount'); // access the filter for column
listFilter.menu = listFilter.createMenu({
options: [ ... ] // new options
// rest of the filter config
});
--- Edit ---
OK, complete example. Tested, working.
Ext.widget('grid', {
renderTo: Ext.getBody()
,height: 400
,features: [{
ftype: 'filters'
,local: true
}]
,columns: [{
dataIndex: 'a'
,text: 'Column A'
,filter: {
type: 'list'
,options: ['Foo', 'Bar']
}
},{
dataIndex: 'b'
,text: 'Column B'
},{
dataIndex: 'c'
,text: 'Column C'
}]
,store: {
fields: ['a', 'b', 'c']
,autoLoad: true
,proxy: {
type: 'memory'
,reader: 'array'
,data: [
['Foo', 1, 'Bar']
,['Bar', 2, 'Baz']
,['Baz', 1, 'Bar']
,['Bat', 2, 'Baz']
]
}
}
,tbar: [{
text: 'Change list options'
,handler: function() {
var grid = this.up('grid'),
// forget about getFeature, I read the doc and found something!
filterFeature = grid.filters,
colAFilter = filterFeature.getFilter('a');
// If the filter has never been used, it won't be available
if (!colAFilter) {
// someone commented that this is the way to initialize filter
filterFeature.view.headerCt.getMenu();
colAFilter = filterFeature.getFilter('a');
}
// ok, we've got the ref, now let's try to recreate the menu
colAFilter.menu = colAFilter.createMenu({
options: ['Baz', 'Bat']
});
}
}]
});
I was solving similar problem and answers to this question helped me a lot. Local List filter menu is in fact lazy loaded (only created when clicked) and I needed to set filter menu to be reloaded if the grid store has been reloaded with different data. Solved it by destroying of menu after each reload, so on next click menu is recreated:
var on_load = function() {
var grid_header = me.gridPanel.filters.view.headerCt
if (grid_header.menu) {
grid_header.menu.destroy();
grid_header.menu = null;
}
}

Categories