I have an array whose items I want to group, and then display in this grouped fashion. It's all terribly confusing:
App.GroupedThings = Ember.ArrayProxy.extend({
init: function(modelToStartWith) {
this.set('content', Ember.A());
this.itemsByGroup = {};
modelToStartWith.addArrayObserver(this, {
willChange: function(array, offset, removeCount, addCount) {},
didChange: function(array, offset, removeCount, addCount) {
if (addCount > 0)
// Sort the newly added items into groups
this.add(array.slice(offset, offset + addCount))
}
});
},
add : function(things) {
var this$ = this;
// Group all passed things by day
things.forEach(function(thing) {
var groupKey = thing.get('date').clone().hours(0).minutes(0).seconds(0);
// Create data structure for new groups
if (!this$.itemsByGroup[groupKey]) {
var newArray = Ember.A();
this$.itemsByGroup[groupKey] = newArray;
this$.get('content').pushObject({'date': groupKey, 'items': newArray});
}
// Add to correct group
this$.itemsByGroup[groupKey].pushObject(thing);
});
}
});
App.ThingsRoute = Ember.Route.extend({
model: function() {
return new App.GroupedThings(this.store.find('thing'));
},
});
This only works if I use the following template:
{{#each model.content }}
These don't render anything (an ArrayController is used):
{{#each model }}
{{#each content }}
{{#each}}
Why? Shouldn't the ArrayController proxy to "model" (which is GroupedThings), which should proxy to "content"?
The reason this becomes a problem is that I then want to sort these groups, and as soon as I change the entire contents array (even using ArrayProxy.replaceContent()), the whole views rebuilt, even if only a single item is added to a single group.
Is there a different approach to this entirely?
I've tended to use ArrayProxies slightly differently when doing such things.
I'd probably get Ember to do all the heavy lifting, and for sorting get it to create ArrayProxies based around a content collection, that way you can sort them automatically:
(note I haven't run this code, but it should push you off in the right direction)
App.GroupedThings = Em.ArrayProxy.extend({
groupKey: null,
sortKey: null,
groupedContent: function() {
var content = this.get('content');
var groupKey = this.get('groupKey');
var sortKey = this.get('sortKey');
var groupedArrayProxies = content.reduce(function(previousValue, item) {
// previousValue is the reduced value - ie the 'memo' or 'sum' if you will
var itemGroupKeyValue = item.get('groupKey');
currentArrayProxyForGroupKeyValue = previousValue.get(itemGroupKeyValue);
// if there is no Array Proxy set up for this item's groupKey value, create one
if(Em.isEmpty(currentArrayProxyForGroupKeyValue)) {
var newArrayProxy = Em.ArrayProxy.createWithMixins(Em.SortableMixin, {sortProperties: [sortKey], content: Em.A()});
previousValue.set(itemGroupKeyValue, newArrayProxy);
currentArrayProxyForGroupKeyValue = newArrayProxy;
}
currentArrayProxyForGroupKeyValue.get('content').addObject(item);
return previousValue;
}, Em.Object.create());
return groupedArrayProxies;
}.property('content', 'groupKey', 'sortKey')
);
You'd then Create a GroupedThings instance like this:
var aGroupedThings = App.GroupedThings.create({content: someArrayOfItemsThatYouWantGroupedThatHaveBothSortKeyAndGroupKeyAttributes, sortKey: 'someSortKey', groupKey: 'someGroupKey'});
If you wanted to get the groupedContent, you'd just get your instance and get.('groupedContent'). Easy! :)
...and it'll just stay grouped and sorted (the power of computed properties)... if you want an 'add' convenience method you could add one to the Em.Object.Extend def above, but you can just as easily use the native ones in Em.ArrayProxy, which are better IMHO:
aGroupedThings.addObjects([some, set, of, objects]);
or
aGroupedThings.addObject(aSingleObject);
H2H
Related
I have a Feed List for posting comments in my UI5 xml view
<layout:content>
<m:FeedInput post="onFeedPost" class="sapUiSmallMarginTopBottom"/>
<m:List id="feedList" showSeparators="Inner" items="{path: '/table', sorter: {path: 'DATE', descending: true}}">
<m:FeedListItem sender="{MEMBERID}" timestamp="{DATE}" text="{COMMENT}" convertLinksToAnchorTags="All"/>
</m:List>
</layout:content>
I want to not display duplicate comments that have the same text and date, but keep them in the database. My idea was to in the controller iterate over over the items to do this, but I am not sure what to do with the resulting array
var results = [];
var comments = feed.getItems();
for (var n = 0; n < comments.length - 1; n++) {
var contained = false;
for (var m = n + 1; m < comments.length; m++) {
if (comments[n].getText() === comments[m].getText() &&
comments[n].getDate() === comments[m].getDate()) {
comments.pop(m);
contained = true;
if (!results.includes(comments[n])) {
results.push(comments[n]);
}
}
}
if (!contained && !results.includes(comments[n])) {
results.push(comments[n]);
}
}
// replace list items with results array
I can't figure out how to replace the feed list's items with the new array as there is a getItems function but not a setItems function. It occurs to me there is probably a simpler more idiomatic UI5 way to do this but I haven't found it yet.
First off, the correct way to handle this situation is in the OData service. The service should remove the duplicates before sending the data to the client. If we assume, however, that you can't do this server side, then you have some options.
1.) Do not bind the list items to anything. Instead, use the ODataModel to read the data, then filter out duplicates, create a new list item and add it to the list
Read the data using the ODataModel, then pass the results to a method that will filter and add them items to the list
oModel.read("/EntitySet", {
success: function(oResponse) {
this._addCommentsToList(oResponse.results)
}.bind(this)
})
In your method to handle the results, you'll need to do three things -- create a new FeedListItem, set the binding context of the list item, and then add the list item to the list
var aDistinctComments = //use your logic to filter out duplicates
aDistinctComments.forEach(function(oComment) {
//to set the binding context, you'll need the entity key/path
var sCommentKey = oModel.createKey("/EntitySet", oComment)
//create a new binding context
var oContext = oModel.createBindingContext(sCommentKey)
//create a new FeedListItem
var oItem = new FeedListItem({
sender: "{MemberId}",
...
});
//set the context of the item and add it to the list
oItem.setBindingContext(oContext);
oList.addItem(oItem);
})
2.) Bind the list directly to the OData entity set and then when the list receives the data, iterate over the items and hide the duplicates
<List items="{/EntitySet}" updateFinished="onListUpdateFinished"....>
----- onListUpdateFinished ---
var aItems = oList.getItems();
for (var m = n + 1; m < aItems.length; m++) {
//set a boolean, true if duplicate
var bDuplicate = aItems[m].getText() ==== aItems[n].getText() &&
aItems[m].getDate() === aItems[n].getDate();
//set the visibility of the item to true if it is not a duplicate
aItems[m].setVisible(!bDuplicate)
}
3.) Read the data manually, remove duplicates, and stash it in a JSON model, and bind the table to your JSON model path
oModel.read("/EntitySet", {
success: function(oResponse) {
this._addCommentsToJSONModel(oResponse.results)
}.bind(this)
})
You can stash an array of objects in your JSON model, and then bind the table items to that path
var aDistinctComments = // your logic to get distinct comments
oJSONModel.setProperty("/comments", aDistinctComments)
oList.setModel(oJSONModel);
-----
<List items="{/comments"}....>
4.) Bind your list items to your entity set, iterate over the items, and then remove duplicates from the list. I don't recommend this approach. Removing items manually from lists bound to an entity set can lead to trouble with duplicate IDs.
var oItem = //use your logic to find a duplicate list item
oList.removeItem(oItem)
I recommend first handling this server side in the OData service, and if that's not an option, then use option 1 above. This will give you the desired results and maintain the binding context of your list items. Options 2 and 3 will get you the desired results, but depending on your applicaiton, may make working with the list more difficult.
Here is one approach :
Do not directly bind the list to your oData.
You can create a JSON model which will be the resulting model after removing duplicate items.
Bind the JSON model to the List as such:
var oList = this.getView().byId("feedList");
oList.bindAggregation("items", "pathToJsonArray", template);
(The template is feedlistitem in this case).
Is it possible to parse out a list of keypaths used by a dynamic partial / component?
If I start with a completely empty data object - and dynamically add a partial / component. Is possible to step through the dynamically added partial's html and discover which keypaths are used.
My intent is to then apply this list of keypaths to my data object on the fly. I'm building a drag and drop wysiwyg ui - and want designers to be able to add templates without touching ractive...
Here is some pseudo code which I hope illustrates what I'm trying to achieve.
<script id="foo" type="text/ractive">
<p>{{blah}}</p>
<p>{{blahblah}}</p>
</script>
-
var ractive = new Ractive({
el: '#container',
template: '{{#items}}{{partial}}{{/items}}',
data: {
items : [] // empty to start with
}
});
ractive.on( 'addpartial', function ( event ) {
// partial gets added
// process the partial to find out what keypaths it contains.. put those keypaths into array
var partialDataArray = [{'blah':''},{'blahblah':''}]
this.push('items' , { 'partial' : 'foo', partialDataArray }
});
The other option would be to set each 'partial' as a component - but I'd be repeating myself loads (and I'm trying to be all DRY etc)
Cheers,
Rob
This code borrows heavily from an example on dynamic components given by Martydpx https://github.com/martypdx - although I am unable to find the post where I found it.
I've created a setup function that basically parses everything out for me. It means that I can supply a file with a long list of templates (to be used by components)
<div data-component="first_component">
<h1>{{h1}}</h1>
<p>{{p1}}</p>
</div>
<div data-component="second_component">
<p>{{p1}}</p>
</div>
-- and here is the JS. Edit - please see JavaScript regex - get string from within infinite number of curly braces for correct regex.
var htmlSnippets = [];
var setup = function() {
// load in our ractive templates - each one is wrapped in a div with
// a data attribute of component.
$.get("assets/snippets/snippets2.htm", function(data) {
var snippets = $.parseHTML(data);
// Each over each 'snippet / component template' parsing it out.
$.each(snippets, function(i, el) {
if ($(el).attr('data-component')) {
var componentName = $(el).attr('data-component')
// reg ex to look for curly braces {{ }} - used to get the names of each keypath
var re = /[^{]+(?=}})/g;
// returns an array containing each keypath within the snippet nb: ['foo' , 'bar']
var componentData = $(el).html().match(re);
// this is probably a bit messy... adding a value to each keypath...
// this returns an array of objects.
componentData = $.map( componentData, function( value ) {
return { [value] : 'Lorem ipsum dolor sit amet' };
});
// combine that array of objects - so its a single data object
componentData = componentData.reduce(function(result, currentObject) {
for(var key in currentObject) {
if (currentObject.hasOwnProperty(key)) {
result[key] = currentObject[key];
}
}
return result;
}, {});
// and add the component name to this data object
componentData['componentName'] = componentName;
// We need to use the data elsewhere - so hold it here...
htmlSnippets.push(componentData );
// and lastly set our component up using the html snippet as the template
Ractive.components[componentName] = Ractive.extend({
template: $(el).html()
});
}
});
Ractive.components.dynamic = Ractive.extend({
template: '<impl/>',
components: {
impl: function(){
return this.components[this.get('type')]
}
}
});
});
}();
var ractive = new Ractive({
el: '#container',
template: '#template',
data: {
widgets: htmlSnippets,
pageContent: []
}
});
I'm trying to create a simple display of NBA west leaders in order by seed using the following json file:
http://data.nba.com/data/v2014/json/mobile_teams/nba/2014/00_standings.json
Right now I have the following:
$(document).ready(function() {
$.getJSON('http://data.nba.com/data/v2014/json/mobile_teams/nba/2014/00_standings.json',function(info){
var eastHead = info.sta.co[0].val;
var divi = info.sta.co[0].di[0].val;
/*evaluate East*/
for(i=0;i < divi.length;i++){
var visTeam ='<li>' + divi + '</li>';
document.getElementById("eastHead").innerHTML=eastHead;
}
var seed = info.sta.co[0].di[0].t[0].see;
$.each(menuItems.data, function (i) {
var eastSeed ='<li>' + seed + '</li>';
console.log(eastSeed)
document.getElementById("eastSeed").innerHTML=eastSeed;
});//$.each(menuItems.data, function (i) {
});//getJSON
});//ready
I'm looking just to list out the leaders in order. So right now we have
Golden State 2. Memphis 3. Houston 4. Portland 5. L.A. Clippers 6. Dallas .... and so
forth.
This is based off of the "see" value which means seed in the west.
This issue is I'm getting a single value rather than an iteration.
Updated:
$(document).ready(function() {
$.getJSON('http://data.nba.com/data/v2014/json/mobile_teams/nba/2014/00_standings.json',function(info){
/**************************************************/
//Get info above here
var westDivision = info.sta.co[1].di;
westDivision.forEach(function (subdivision)
{
subdivision.t.forEach(function (team)
{
westTeams.push({
city: team.tc,
name: team.tn,
seed: team.see
});
});
});
function compare(a,b) {
if (a.see < b.see)
return -1;
if (a.see > b.see)
return 1;
return 0;
}
var sorted = westTeams.sort(compare);
sorted.forEach(function (el,i)
{
console.log(i+'. '+el.city+' '+el.name);
});
/**************************************************/
});//getJSON
});//ready
console output :
Portland Trail Blazers
Oklahoma City Thunder
Denver Nuggets
Utah Jazz
Minnesota Timberwolves
Golden State Warriors
Los Angeles Clippers
Phoenix Suns
Sacramento Kings
Los Angeles Lakers
Memphis Grizzlies
Houston Rockets
Dallas Mavericks
San Antonio Spurs
New Orleans Pelicans
I like to iterate with forEach. Rather then having to worry about indexes you can directly reference each item of the array.
Using this code you can put the data you want into an array.
//Get info above here
var westTeams = [];
var westDivision = info.sta.co[1].di;
westDivision.forEach(function (subdivision)
{
subdivision.t.forEach(function (team)
{
westTeams.push({
city: team.tc,
name: team.tn,
seed: team.see
});
});
});
Then you can sort them using obj.sort
function compare(a,b) {
if (a.seed < b.seed)
return -1;
if (a.seed > b.seed)
return 1;
return 0;
}
var sorted = westTeams.sort(compare);
Finally, you can print them in order.
sorted.forEach(function (el,i)
{
console.log((i+1)+'. '+el.city+' '+el.name);
});
Querying a large JavaScript object graph can be a tedious thing, especially if you want to have dynamic output. Implementing support for different filter criteria, sort orders, "top N" restrictions, paging can be difficult. And whatever you come up with tends to be inflexible.
To cover these cases you can (if you don't mind the learning curve) use linq.js (reference), a library that implements .NET's LINQ for JavaScript.
The following showcases what you can do with it. Long post, bear with me.
Preparation
Your NBA data object follows a parent-child hierarchy, but it misses a few essential things:
there are no parent references
the property that contains the children is called differently on every level (i.e. co, di, t)
In order to make the whole thing uniform (and therefore traversable), we first need to build a tree of nodes from it. A tree node would wrap objects from your input graph and would look like this:
{
obj: o, /* the original object, e.g. sta.co[1] */
parent: p, /* the parent tree node, e.g. the one that wraps sta */
children: [] /* array of tree nodes built from e.g. sta.co[1].di */
}
The building of this structure can be done recursively in one function:
function toNode(obj) {
var node = {
obj: obj,
parent: this === window ? null : this,
// we're interested in certain child arrays, either of:
children: obj.co || obj.di || obj.t || []
};
// recursive step (with reference to the parent node)
node.children = node.children.map(toNode, node);
// (*) explanation below
node.parents = Enumerable.Return(node.parent)
.CascadeDepthFirst("$ ? [$.parent] : []").TakeExceptLast(1);
return node;
}
(*) The node.parents property is a convenience facility. It contains an enumeration of all parent nodes except the last one (i.e. the root node, which is null). This enumeration can be used for filtering as shown below.
The result of this function is a nice-and-uniform interlinked tree of nodes. (Expand the code snippet, but unfortunately it currently does not run due to same-origin browser restrictions. Maybe there is something in the NBA REST API that needs to be turned on first.)
function toNode(obj) {
var node = {
obj: obj,
parent: this === window ? null : this,
children: obj.co || obj.di || obj.t || []
};
node.children = node.children.map(toNode, node);
node.parents = Enumerable.Return(node.parent)
.CascadeDepthFirst("$ ? [$.parent] : []").TakeExceptLast(1);
return node;
}
$(function () {
var standingsUrl = 'http://data.nba.com/data/v2014/json/mobile_teams/nba/2014/00_standings.json';
$.getJSON(standingsUrl, function(result) {
var sta = toNode(result.sta);
console.log(sta);
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Result
Now that we have a fully traversable tree of nodes, we can use LINQ queries to do complex things with only a few lines of code:
// first build our enumerable stats tree
var stats = Enumerable.Return(toNode(result.sta));
// then traverse into children; the ones with a tid are teams
var teams = stats.CascadeDepthFirst("$.children")
.Where("$.obj.tid");
OK, we have identified all teams, so we can...
// ...select all that have a parent with val 'West' and order them by 'see'
var westernTeams = teams.Where(function (node) {
return node.parents.Any("$.obj.val === 'West'");
})
.OrderByDescending("$.obj.see");
// ...insert the top 5 into our page as list items
westernTeams.Take(5).Do(function (node) {
$("<li></li>", {text: node.obj.tc + ' ' + node.obj.tn}).appendTo("#topFiveList");
});
// ...turn them as an array of names
var names = westernTeams.Select("$.obj.tc + ' ' + $.obj.tn").ToArray();
console.log(names);
Of course what I have done there in several steps could be done in one:
// request details for all Northwest and Southeast teams who have won more than one game (*)
var httpRequests = Enumerable.Return(toNode(result.sta))
.CascadeDepthFirst("$.children")
.Where("$.obj.tid")
.Where(function (node) {
var str = node.obj.str.split(" ");
return str[0] === "W" && str[1] > 1 &&
node.parents.Any("$.obj.val==='Northwest' || $.obj.val==='Southeast'");
})
.Select(function (node) {
return $.getJSON(detailsUrl, {tid: node.obj.tid});
})
.ToArray();
$.when.apply($, httpRequests).done(function () {
var results = [].slice.call(arguments);
// all detail requests have been fetched, do sth. with the results
});
(*) correct me if I'm wrong, I have no idea what the data in the JSON file actually means
I want to create a table that is sortable using the attributes of a collection.
So far i've being able to make the tab table sortable using two attributes, but i would like to be sortable based the value of the sort key attribute.
e.g when the "task_status = 'open'"
Here what i having working now
var TaskCollection = Backbone.Collection.extend({
//Model
model:Task,
//url
url:"./api/tasks",
//construct
initialize: function() {
this.sort_key = 'end';
this.fetch();
},
comparator: function(a,b) {
a = a.get(this.sort_key);
b = b.get(this.sort_key);
return a > b ? 1
: a < b ? -1
: 0;
},
sort_by_status: function() {
this.sort_key = 'task_status';
this.sort();
},
sort_by_task_tag: function() {
this.sort_key = 'task_group';
this.sort();
}
});
This sorts the collection but does not reverse the order, or allow me to sort by the particular value of an attribute. How can this be modified to work
In the comparator, have a state variable for "reversed" and have it take values 1 and negative 1. Multiply it by the previous return value. Setting the state variable on the collection and then again sorting should get things done.
I want to store objects in jquery. The objects are 'events' each event has a date, title and some text. They need to be stored like and array (maybe thats what they will be a multi-dimensional array) so I can iterate through them with a counter.
Edit,
I like the stores var as a way to group the info but how do I add multiple items and how do I index them?
var dates = new Array('12th Dec', '14th Jan', '6th May');
var event_title = new Array('My Birthday', 'Going to Beach', 'Holiday');
var event_text = new Array('One Year Older', 'Remember the suntan lotion', 'on the plane to spain');
I need to return by index alert(dates[2], event_title[2], event_text[2]);
you can try:
var store = {};
store = {
dates : dates,
titles: titles,
infotxt: infotxt
};
Then access:
store.dates or store.title or so..
You probably don't want to store separate related info in arrays as you're doing, and then attempt to access those groups by index.
JavaScript has a perfect, native object that allows you to group similar properties. Just use an object literal:
var events = [];
events.push({
data: someData,
title: someTitle,
text: someText
});
Now you have what's referred to oftentimes as a "collection".
You could take it a step further and make each event a "class":
function MyAwesomeEvent(data, title, text) {
this.data = data;
this.title = title;
this.text = text;
}
events.push(new MyAwesomeEvent(someData, someTitle, someText));
That approach is potentially heavy-handed depending on what your specific use-case is, but it's another option.