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.
Related
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.
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()));
})
])
]);
};
I am dynamically bringing in objects to an repeat, and when I get a certain type of object, I want to make a service call and replace it. My best guess was a filter on the repeat would be a good idea. So here is what I tried.
.filter('filterByType', [
vitModel.componentName,
function(myVitModel) {
return function(items) {
var newObj = _.map(items, function(item) {
if (!item.data) {
return item;
} else {
var newModel = new myVitModel(item.data);
newModel.getFullVitData().then(function() {
return newModel.data;
});
}
});
return newObj;
};
}
]);
This seems to have some problems in the logic, but the basic idea is if an item comes in that has .data it needs to call a new myVitModel on the data and replace it.
Everything seems sound until I drop an object in that does have the .data, then I am getting a
Error: $rootScope:infdig
Infinite $digest Loop
So I'm not sure where in this problem is being caused, possibly looping in on itself? Could use some help. Thanks!
I'm using Array.prototype.map.call to store in an array a bunch of node list objects:
function getListings() {
return Array.prototype.map.call(document.querySelectorAll('li.g'), function(e) {
return {
rectangle: e.getBoundingClientRect();
}
}
}
However, I also want to store the order in which this elements appear in the DOM, and I don't know how to do that.
I know that I'm storing this in an array, and the order would be the index of the array. For example:
var listings = getListings();
console.log(listings[0]); // rank #1
console.log(listings[1]); // rank #2
// etc...
but I'm inserting the json object in a database, and the easiest way to store the "rank" information is by creating a property "rank" in my object, but I don't know how to get the "index" of the current array.
Something like:
function getListings() {
return Array.prototype.map.call(document.querySelectorAll('li.g'), function(e) {
return {
rectangle: e.getBoundingClientRect(),
rank: magicFunctionThatReturnsCurrentIndex() // <-- magic happens
}
}
}
Any help pointing me to the right direction will be greatly appreciated! Thanks
The MDN documentation says:
callback is invoked with three arguments: the value of the element,
the index of the element, and the Array object being traversed.
So
function getListings() {
return Array.prototype.map.call(document.querySelectorAll('li.g'), function(e, rank) { // magic
return {
rectangle: e.getBoundingClientRect(),
rank: rank // <-- magic happens
}
}
}
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