How to create dependency with Mithril.js? - javascript

I have a long list of items that I want to show in a <ul>. I want to add a "filter" input, so the user can narrow down the list of items to those matching the filter.
My controller contains a filter prop and a list array:
function Ctrl() {
this.filter = m.prop('');
this.list = [];
}
I've added an update method to the controller, which looks at the filter prop and updates the contents of the list array:
Ctrl.prototype.update = function (value) {
var _this = this;
if (this.filter()) {
searchItems(this.filter(), function (items) {
_this.list = items;
});
} else {
this.list = [];
}
};
Finally, my view iterates over the list array and renders the items. Additionally, it displays an input on top, bound to the filter prop:
var view = function (ctrl) {
return m('#content', [
m('input', {
oninput: m.withAttr("value", ctrl.filter),
value: ctrl.filter()
}),
m('ul', [
ctrl.list.map(function (item, idx) {
return m('li', m('span', item.getName()));
})
])
]);
};
My question is, how to make the update function fire when the value of filter changes, so that I get the updated list of items?
Do I need to position two oninput events? One to update filter and one to fire update?
Should I use a single oninput event and update the filter property within he update function?
Anything else?

When you use m.withAttr, what you're saying is that when the event handler fires (oninput), you will take some attribute of the element (value) and pass it into your second argument, which is a function (ctrl.filter). Your current sequence of events:
filter property gets updated
mithril redraws
What you want to do, is call the update function (instead of the getter/setter ctrl.filter function), and bind it so you can retain the proper context in the function:
m('input', {
oninput: m.withAttr("value", ctrl.update.bind(ctrl)),
value: ctrl.filter()
}),
Then, in your update function the value will be passed to the function and you can set it there.
Ctrl.prototype.update = function (value) {
this.filter(value);
...
Now what'll happen:
ctrl.filter property gets updated
ctrl.list gets filtered based on ctrl.filter
mithril redraws
Another way to handle this is to not have any "list" property in your controller / model, but to let the view grab a filtered list instead. There's only one thing really changing, after all, and that's the "filter" property. The filtered list is derived from that, so by creating another property on the controller, you're effectively duplicating the same state.
Additionally, you could keep m.withAttr('value', ctrl.filter) and benefit from that simplicity.
Something like:
var filteredItems = ctrl.getFilteredItems();
var view = function (ctrl) {
return m('#content', [
m('input', {
oninput: m.withAttr("value", ctrl.filter),
value: ctrl.filter()
}),
m('ul', [
filteredItems.map(function (item, idx) {
return m('li', m('span', item.getName()));
})
])
]);
};

Related

Mithril component don't return anything

can you help with Mithril? Again? :)
In this component I want to show some data, but m() doesn't show anything, however data is't empty.
Thank you very much
var default_panel_component = {
controller: function(args) {
return {
'data': args.data //Object {name1: "data1", name2: "data2", name3: "data3"}
}
},
view: function(ctrl) {
return m("table", [
$.each(ctrl.data, function (key, value) {
console.log(key), //key1
console.log(value), //data1
console.log(typeof value), //string
m("td", value)
})
])
}
};
The issue here is that you are using $.each to build the children of the m('table') call, but each will just be executed on every object, not returning anything. So, essentially, you are left with an empty table.
What you need is [$.map][1], that translates all items in an array or object to a new array of items. With that you will be returning a m('td') element for each property on ctrl.data.
You can see it working on this fiddle. I also set the tds as children of a table row, so take that into account if you need to generate several rows.

Mithril - Re-sort array of child components after child component modifies parent component data

If someone can think of a better title for this question, please let me know.
I just figured out the answer to this question after an insane amount of time. I'm sure someone else will make the same mistake, so I'll post the answer below.
The question will make more sense once I provide some background info. I have a parent component which has an array of objects with matching keys (e.g. ctrl.arrayOfObjects). I sort this array by the value of one of the keys shared by each object (e.g., ctrl.arrayOfObjects[0].keyToSortBy). Then, I pass each element in the now-sorted array to another component.
This child component has an onclick method which changes the data shared between the parent and the child, i.e., one of the objects in the parent's ctrl.arrayOfObjects. It changes the value of the key which was used to sort the parent component's ctrl.arrayOfObjects (e.g., sharedObject.keyToSortBy).
My problem is that, after triggering this onclick event, the parent component does not re-sort the array of objects. The onclick event does modify the data it's supposed to (sharedObject.keyToSortBy), which I know because the page does re-render the new value of sharedObject.keyToSortBy. But it does not re-sort the parent component's ctrl.arrayOfObjects.
The weird part is that if I do a console.log, the sort method is being called, but the page does not display any changes.
Here's what the code looks like (ctrl.arrayOfObjects is ctrl.jokes):
var JokeWidgetSeries = {
controller: function() {
var ctrl = this;
ctrl.jokes = [{
text: "I'm a joke",
votes: 3
}, {
text: "This is another joke",
votes: 1
}, {
text: "A third joke",
votes: 1
}]
},
view: function(ctrl) {
return m('#jokes-container',
m('#jokes',
ctrl.jokes.sort(function(a, b) {
// Sort by number of votes.
return (a.votes < b.votes) ? 1 : (a.votes > b.votes) ? -1 : 0;
}).map(function(joke) {
// Create a 'JokeWidget' for each object in 'ctrl.jokes'.
// Share the 'joke' object with the child component.
return m.component(JokeWidget, joke);
})
)
);
}
};
var JokeWidget = {
controller: function(inherited) {
var ctrl = this;
ctrl.joke = inherited;
},
view: function(ctrl) {
return m('.joke', [
m('.joke-text', ctrl.joke.text),
m('.joke-vote-count', ctrl.joke.votes),
m('.thumb-up', {
onclick: function(e) {
// Here, the page shows that the vote count has changed,
// but the array of 'JokeWidget' DOM elements do not re-sort.
ctrl.joke.votes += 1;
}
})
]);
}
};
After triggering the onclick method of the JokeWidget component, the .joke-vote-count DOM element which displays ctrl.joke.votes does increase by 1. But it does not move up in the list/array of widgets on the page like it should.
However, if I combine these two components into one, the array does get re-sorted like I want it. Like this:
var JokeWidgetSeries = {
controller: function() {
var ctrl = this;
ctrl.jokes = [{
text: "I'm a joke",
votes: 3
}, {
text: "This is another joke",
votes: 1
}, {
text: "A third joke",
votes: 1
}]
},
view: function(ctrl) {
return m('#jokes-container',
m('#jokes',
ctrl.jokes.sort(function(a, b) {
// Sort by number of votes.
return (a.votes < b.votes) ? 1 : (a.votes > b.votes) ? -1 : 0;
}).map(function(joke) {
// Create a 'JokeWidget' component instance for each object in 'ctrl.jokes'.
// Share the 'joke' object with the child component.
return m('.joke', [
m('.joke-text', joke.text),
m('.joke-vote-count', joke.votes),
m('.thumb-up', {
onclick: function(e) {
// Here, the array of 'JokeWidget' DOM elements
// DO re-sort. Why?
joke.votes += 1;
}
})
]);
})
)
);
}
};
How can I separate these components and still get this re-sorting method to work?
The solution is simple. When sharing the object between the parent and child components, pass the object to the child component's view method rather than the controller method.
When creating components in Mithril, you can pass data to both the controller and the view methods of the component. This data will be the first parameter of the controller method and the second parameter of the view method.
Instead of setting the shared object to ctrl.joke inside of the JokeWidget's controller method and then referencing it inside of its view method, just pass the shared object directly to the view method. Like this:
var JokeWidget = {
// Do not pass the object this way.
// controller: function(joke) {
// var ctrl = this;
// ctrl.joke = joke;
// },
// Pass the shared object as a second parameter to the 'view' method.
view: function(ctrl, joke) {
return m('.joke', [
m('.joke-text', joke.text),
m('.joke-vote-count', joke.votes),
m('.thumb-up', {
onclick: function(e) {
// Now, the array of 'JokeWidget' DOM elements will re-sort.
joke.votes += 1;
}
})
]);
}
};
This also removes an extra layer of abstraction, making it easier to read and understand.

sencha apply a set of filters at once

I'm trying to apply several filters at once. Because there is a listener on the filters that will retrieve information from an online store when the filters are cleared. So I need them to be replaced at once.
The filter function is being called, log messages, but somehow the result is ignored. So all items are always shown whether includeStatus is true or false.
flipElements: function (includeStatus)
{
this.setFilters(
[
new Ext.util.Filter(
{
property: 'userName',
value: 'me'
}),
new Ext.util.Filter(
{
filterFn: function (record)
{
var result = includeStatus === (record.get('statusId') === 3);
console.log(result);
return result;
}
})
]);
}
The problem is not that the model does not have the property because when I simply set the filter with filterBy it does show/hide the items properly.
Edit: even when the log says false it still shows the items.
setFilters only changes the filters property of the store. In order to trigger filter actions call the function filter. This is normally used to add a filter in the style 'property,value' to the filterset, but when no arguments are given it filters the store.

KnockoutJS + KoGrid - observable array as child of ko.observable(object) does not trigger update

Context
I'm currently exploring KnockoutJS, combined with a KoGrid.
I have an object containing several fields (which are also bound to fields), and a list of child objects which I want to show in KoGrid.
jsfiddle
I've prepared a jsFiddle illustrating the issue I'm having:
http://jsfiddle.net/kPmAJ/4/
The issue:
My viewmodel contains an observable foo, containing the parent object.
In this parent object, there is a property children, which is an observableArray containing objects again.
The KoGrid is bound to $root.foo().children
This works if the array is filled before initializing the binding.
However, the object gets replaced afterwards (data is loaded through AJAX and can be re-loaded), and apparently the KoGrid items binding is lost.
I was thinking that, since the foo-object on my viewmodel is an observable, this would trigger KoGrid that is watching the array inside to update if foo gets replaced. This does work perfectly with a foreach-binding.
Apparently KoGrid doesn't trigger though.
--
Am I doing something wrong here, or have I hit an issue in KoGrid?
Code (for reference purposes. see fiddler ;))
var SampleObject = function (data) {
this.id = new ko.observable(data ? data.id : null);
this.children = new ko.observableArray([]);
if(data) {
var childrenMapped = [];
$(data.children).each(
function() {
childrenMapped.push(new SampleChildObject(this));
}
);
this.children(childrenMapped);
}
}
var SampleChildObject = function (data) {
this.name = new ko.observable(data ? data.name : null);
};
var vm = {
foo: new ko.observable('John Doe'),
bar: new ko.observable(
new SampleObject(
{
id: 1234,
children: []
})
)
};
ko.applyBindings(vm);
// Returns from an AJAX-call instead, so can't be before applyBindings
vm.bar(new SampleObject(
{
id: 1234,
children: [
{ name: 'test1' },
{ name: 'test2' },
{ name: 'test3' }]
}));
-
<div style="height: 500px;"
data-bind="koGrid: { data: bar().children }"></div>
<ul data-bind="foreach: bar().children">
<li data-bind="text: name"></li>
</ul>
Thanks!
What's happening is that the koGrid binding doesn't have an update handler, so it's not responding to any changes in bar.
The koGrid does watch the observable array children though, if you we're to replace the values in bar().children with those returned from your Ajax call the grid would update.
Like this:
function (data) {
var childrenMapped = [];
$(data.children).each(
function() {
childrenMapped.push(new SampleChildObject(this));
});
bar().children(childrenMapped);
bar().id(data.id);
});
You should also checkout the mapping plugin which is supposed to solve this problem.
ko mapping

Render jQuery object through Backbone View

I'm rather new to Backbone.js development, and have run into a bit of a roadblock while attempting to render a subview.
Currently, I have in place several views to render a custom dropdown-button, as well as other elements. I've taken this approach based on DocumentCloud's code
Here's what I have so far:
app.ui.SelectMenu = Backbone.View.extend({
className: 'btn-group group-item',
options: {
id: null,
standalone: false
},
events: {
"click .dropdown-menu a": "setLabel"
},
constructor: function (options) {
Backbone.View.call(this, options);
this.items = [];
this.content = JST['common-select_button'];
this.itemsContainer = $('.dropdown-menu', $(this.content.render()));
// Add any items that we may have added to the object params
if (options.items) {
this.addItems(options.items);
}
},
render: function () {
this.$el.html(this.content.render({
label: this.options.label,
items: this.itemsContainer
}));
this._label = this.$('.menu-label');
return this;
},
setLabel: function (label) {
$(this._label).text(label || this.options.label);
},
addItems: function (items) {
this.items = this.items.concat(items);
var elements = _(items).map(_.bind(function (item) {
var attrs = item.attrs || {};
_.extend(attrs, { 'class': 'menu_item' + (attrs['class'] || '') });
var el = this.make('li', attrs, item.title);
return el;
}, this));
$(this.itemsContainer).append(elements);
}
});
So far I have successfully rendered my button, as well as the appropriate label, but I cannot seem to populate the .dropdown-menu when calling the addItems function.
I'm assuming that when render hits, the items variable cannot be populated due to the fact that I am passing a jQuery object and not a string, yet whenever I use items: this.itemsContainer.html(), that simply pastes the html surrounded by quotes... I could simply replace the quotes but that just feels like a hack to me.
Any help would be much appreciated. Thanks!
jQuery's append doesn't take an array:
.append( content [, content] )
content: DOM element, HTML string, or jQuery object to insert at the end of each element in the set of matched elements.
content: One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert at the end of each element in the set of matched elements.
If you want to append multiple elements in one call, you have to supply them as separate arguments:
$(x).append(e1, e2, e3, ...);
so you'd have to use apply to convert your array to separate arguments:
var $i = $(this.itemsContainer);
$i.append.apply($i, elements);
That sort of chicanery really isn't necessary though, you can add them one by one as you create them:
addItems: function (items) {
this.items = this.items.concat(items);
_(items).each(function (item) {
var attrs = item.attrs || {};
_.extend(attrs, { 'class': 'menu_item' + (attrs['class'] || '') });
this.itemsContainer.append(this.make('li', attrs, item.title));
}, this);
}
Also note that _.each can take a context argument so you don't need a separate _.bind call. And I'm pretty sure that this.itemsContainer is already a jQuery object so you don't need to wrap it $() again.
You might have problems with your render as well:
render: function () {
this.$el.html(this.content.render({
label: this.options.label,
items: this.itemsContainer
}));
this._label = this.$('.menu-label');
return this;
}
I suspect that items: this.itemsContainer is going to end stringifying this.itemsContainer, you might have better luck with something like this:
this.$el.html(this.content.render({ label: this.options.label });
this.$el.find('some selector').append(this.itemsContainer);
where 'some selector' would, of course, depend on the HTML structure; you'll have to adjust the template for this as well.
Your Github link is broken so I don't know what code you're adapting. I do know that your use of constructor is non-standard. Why not use the standard initialize?
constructor / initialize new View([options])
[...] If the view defines an initialize function, it will be called when the view is first created.
You should probably do it this way:
app.ui.SelectMenu = Backbone.View.extend({
// No 'constructor' in here or anywhere...
initialize: function (options) {
this.items = [];
this.content = JST['common-select_button'];
this.itemsContainer = $('.dropdown-menu', $(this.content.render()));
// Add any items that we may have added to the object params
if (options.items) {
this.addItems(options.items);
}
},
//...
});

Categories

Resources