Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow object iteration #115

Closed
gliese1337 opened this issue Aug 8, 2013 · 14 comments
Closed

Allow object iteration #115

gliese1337 opened this issue Aug 8, 2013 · 14 comments

Comments

@gliese1337
Copy link

I said (https://github.com/Rich-Harris/Ractive/issues/110#issuecomment-22333513) that I couldn't think of a reason to need to add properties to a Ractive model that aren't referenced in the template, but then just today I ran into one: dealing with a model that contains a key-value map rather than a regular array.

This is kind of a pie-in-the-sky feature request (it's fairly simply worked around, and it would make magic setter methods per #110 harder to implement in the absence of Object.observe or Proxy objects), but it would be kind of neat to have something like

{{#object:key}}Value:{{.}}{{/object}}

that would automatically iterate over object properties.

@gliese1337
Copy link
Author

Apparently, Handlebars has support for this already, via an 'each' tag (http://stackoverflow.com/questions/9058774/handlebars-mustache-is-there-a-built-in-way-to-loop-through-the-properties-of):

{{#each object}}
    {{@key}}:{{this}}
{{/each}}

But I'm not a fan of that syntax.

@Rich-Harris
Copy link
Member

This one has been on my radar for a while, I've had the same need from time to time - just struggled to figure out the best way to approach it. It's actually come up before - #24 - and I suspect it will come up again, particularly since it's quite common to get data from a back end structured in this form rather than as an array. So it's probably time to revisit it.

It gets tricky because there's no canonical way to sort object keys - different browsers do it differently (see this Stack Overflow answer and weep). And there's the question of what to do when new keys are added - do they get added to the bottom, or do they get inserted in the middle according to some sorting logic? And does there need to be a mechanism for specifying custom sort logic, rather than (for example) just sorting keys alphabetically?

There are some cases (particularly with SVG dataviz, for example) where sort order doesn't really matter. So maybe the right approach is to implement it without worrying too much about sorting, and see from there what the limitations are.

We agree on the syntax - I think that's the right way to approach it.

@codler
Copy link
Member

codler commented Aug 9, 2013

I think the easiest way is not to worry about sorting. If you want to sort restructure your object to an array with properties key & value [{key:'', value:''}...] http://stackoverflow.com/questions/14208651/javascript-sort-key-value-pair-object-based-on-value

@Rich-Harris
Copy link
Member

Okay, that's in. You can now do this sort of thing:

<ul>
  {{#countries:code}}
    <li>{{code}}: {{name}}</li>
  {{/countries}}
</ul>
ractive = new Ractive({
  el: container,
  template: template,
  data: {
    countries: {
      GBR: { name: 'United Kingdom' },
      USA: { name: 'United States' },
      FRA: { name: 'France' }
    }
  }
});

// would render
// <ul>
//   <li>GBR: United Kingdom</li>
//   <li>USA: United States</li>
//   <li>FRA: France</li>
// </ul>

Then of course you can do ractive.set( 'countries.BRA', { name: 'Brazil' }) and it will add another item.

It makes no attempt to maintain any sort order - items will initially be added in whatever order for ... in prefers, and new items will be added at the end. Also, it's experimental, and there may be some weird bugs I haven't thought of looking for.

Two ways to remove items:

delete ractive.get( 'countries' ).GBR;
ractive.update();

ractive.set( 'countries.GBR', undefined );

I did wonder about (where necessary) adding .add() and .remove() methods to objects, which would be similar to array mutator methods (except probably disabled by default) so that you could do things like...

countries.add( 'CHN', { name: 'China' });
countries.add({
  SWE: { name: 'Sweden' },
  GTM: { name: 'Guatemala' }
});

countries.remove( 'FRA' );

...in lieu of Object.observe(). Could possibly also implement sorting via a countries.sortKeys( comparator ) method as well. Too hacky?

One final note: there is a theoretical performance penalty to doing list sections based on objects - every time the object (or one of its properties, or sub-properties) changes, Ractive needs to do a for ... in on both the current map of section fragments, and the properties of the value the section is bound to. I don't see a way round that. In most situations we're talking about fractions of milliseconds though.

@gliese1337
Copy link
Author

I'd avoid adding add, remove, and sortKeys methods; they'd be sharing a namespace with actual properties of the object you might want to enumerate over. I suppose it's OK if there's a switch to add them as wanted (and leave them off by default), but if you really want sorting, you can always just restructure your data as an array.

The behaviors of add and remove seem pretty well covered already by ractive.set. Maybe instead of a method on objects, there could be a ractive.setSortKey(keypath, comparator), which would avoid property conflicts by storing object-comparator pairs in a Map (which is pretty easy to polyfill in non-Firefox browsers: https://github.com/BYU-ARCLITE/Polyfills/blob/master/Map.js); e.g. ractive._comparatorMap, to be checked for an entry whenever object iteration is about to happen. There is of course an additional performance penalty for doing comparator map lookups and the actual sorting, but probably still only fractions of a millisecond and developers may be properly warned to just use arrays if that's really a concern for them.
In that case, the sorting code would be something like this:

if(this._comparatorMap.has(obj)){
    var keys = Object.keys(obj).sort(this._comparatorMap(obj).bind(obj)); //Object.keys and .bind are easily polyfilled for older browsers
    iterateWithKeysForIndices(keys.map(function(key){ return obj[key]; }), keys)
}else{
    doRegularObjectIteration(obj);
}

@Rich-Harris
Copy link
Member

Closing this issue. Quite like your proposed sorting solution @gliese1337 but I haven't found it necessary to implement yet, and no-one else has asked for it, so will put it in the YAGNI pile for now

@der-david
Copy link

Object iteration seems to have stopped working: http://jsbin.com/uZavecob/3/edit?html,output (example from the docs http://docs.ractivejs.org/latest/mustaches) :(

@MartinKolarik
Copy link
Member

It doesn't work because of . in the key, it works with 'joe@example': {name: 'Joe'}

@Rich-Harris
Copy link
Member

Ah, whoops. Thanks for spotting it @der-david and identifying the problem @MartinKolarik. I'll leave this issue closed in favour of #396.

@svapreddy
Copy link

@Rich-Harris Please add methods add, remove and sortKeys. They are very helpful.

@constantx
Copy link

@Rich-Harris Please add methods add, remove and sortKeys. They are very helpful.

👍

Found myself need to delete a key and resort to this:

dom.ractive.set('list.' + id, undefined);

@svapreddy
Copy link

@Rich-Harris Any plans for adding a method to remove a key path instead of using using delete ractive.get('keypath'). I am confused every time about delete, whether it updates view and model or not. Else should I use ractive.set(path, undefined); ? Could you please give a note on this.

@zaus
Copy link

zaus commented Jan 30, 2015

(correct me if I should make another issue)

I see elsewhere (#1474) that "objects are first class citizens" and we can iterate objects in a couple different ways:

{{#obj:k,i}}<p>{{@key}} = {{k}} and {{@index}} = {{i}}, but value = {{.}}</p>{{/}}

But what if you want two-way binding on the key? ex) turn a dictionary/map into a table so you can edit both key/value.

@Rich-Harris
Copy link
Member

@zaus' comment led me back to this (long-since-abandoned!) issue - @svapreddy the delete thing came up recently in the form of #1649. It contains a useful code snippet for deleting data from the model. I closed the issue, but invited people to comment in support of adding ractive.unset(keypath) to the core API - feel free to weigh in!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants