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

Iteration Binding needs to be more flexible with how it gets and iterates the collection #52

Closed
msheakoski opened this issue Aug 13, 2012 · 14 comments

Comments

@msheakoski
Copy link

I'm building an app with Stapes.js + Rivets.js and ran into a problem when doing an iteration binding. Rivets doesn't use an adapter to retrieve the collection and assumes that the attribute it's iterating over is an array. Stapes has specific methods for dealing with collections, mainly getAllAsArray and each.

The issue is with this piece of code:

# Sets the value for the binding. This Basically just runs the binding routine
# with the suplied value formatted.
set: (value) =>
  # ... snip ...
  else if @options.special is "iteration"
    @routine @el, value, @ # <--- assumes value is an array :(

I got around it by defining a method on my view, and calling it from the binding. It's not the best solution because it adds complexity to the view and also requires manually updating the binding when items are added/removed from the collection. I will have to think about this problem more and post a followup comment with ideas but just wanted to shed some light on this issue to get a discussion going.

Vehicle = Stapes.create()
Vehicles = Stapes.create()

ListVehiclesView = Stapes.create().extend
  init: ->
    @set "vehicles", Vehicles
    @set "vehiclesCollection", @get("vehicles").getAllAsArray()
    @render()
  render: ->
    @view = rivets.bind $("#list-vehicles"), view: @

v = Vehicle.create(); v.set modelYear: 1980, make: "Ford"
v2 = Vehicle.create(); v2.set modelYear: 2012, make: "Chevy"
v3 = Vehicle.create(); v3.set modelYear: 1993, make: "Toyota"
Vehicles.push [v, v2, v3]

ListVehiclesView.init()
<div id="list-vehicles">
  <h2>List of Vehicles here</h2>
  <ul>
    <li data-each-vehicle="view.vehiclesCollection">
      <span data-text="vehicle.make"></span>
    </li>
  </ul>
</div>
@mikeric
Copy link
Owner

mikeric commented Aug 14, 2012

Thanks for bringing this up. I've actually thought about this as well recently — since most frameworks do come with some sort of collection concept. A couple of ways to go about this that don't involve binding to the collection directly:

  1. Store a separate array of models that you want to iterate over in your view-model. To keep things in sync, have your view-model observe the actual collection for changes and set the models that you want displayed— this is also useful when you need to filter or limit the iterated set. More on this method below.
  2. If your collection complies with your adapter enough to be observed for changes, then you can define a filter that converts the collection into a plain array before any iteration starts: view.vehiclesCollection | asArray

Aside from those two options, I think the adapter interface would have to increase in complexity a bit. For example, there would have to be the current adapter for observing models and a secondary adapter for observing collections. I'm not convinced that it's completely necessary, but I'm definitely open to it if we can find a way that won't over-complicate things.

By coincidence, I started writing a simple todo list demo with Rivets.js and Stapes.js right before you opened this issue (I find Stapes.js to be one of the more flexible tools to use alongside Rivets.js). I did it MVVM style using the first method outlined above, and things seem to work pretty smoothly so far.

http://bl.ocks.org/3345763

@mattcreager
Copy link

I want to make sure I understand how this is suppose to work -

I pass the rivets.bind event an array of project models like such:

rivets.bind @el.select, { project: projects }

My view

<select data-each-project="projects" id="project-selection">
    <option data-value="project:description"></option>
</select>

My adapter subscribe method:

obj.on "change:" + keypath, callback

Now will each of the model objects in my projects array be passed through the subscribe method? Because when I log the object from the subscribe method it appears to be the entire array of project models?

Thanks in advance for the assist :)

Update: I've also asked this question on stackoverflow: http://stackoverflow.com/questions/13569470/iterating-through-models-for-a-select-field-with-rivet-js-coffeescript

Update 2: I've added an asArray filter as suggested but value isn't defined, so I'm assuming this is happening after the iterator?

    rivets.formatters.asArray = (value, ...) ->
        console.log arguments

@yocontra
Copy link
Contributor

@slukehart - If you did

<select data-each-project="projects" id="project-selection">
    <option data-value="project.description"></option>
</select>

Then your subscribe adapter would be called on each project in the array - make sure your project object is observable by the adapter

@mattcreager
Copy link

@contra

In that case - wouldn't the object argument in the subscribe function be called once per object in the array? Because it is only being called once and being passed the entire array...

Update: It doesn't even seem to matter if I populate the data-each-project attribute, the entire array gets passed to the adapter anyway.

@yocontra
Copy link
Contributor

@slukehart - You aren't using subscribe on any projects (you use : which is direct access - it bypasses the subscriber) use the code I pasted and you should see each project in the array get passed in

@mattcreager
Copy link

@contra - Your correct, I don't need the subscribe (.on) binding - but the subscribe function is still being passed the array - even using project:description

@yocontra
Copy link
Contributor

@slukehart - When you subscribe to an iteration binding rivets does two things

  1. Subscribes to the entire array so if it changes it will rerun the iteration
  2. Subscribe to all children of the array that need binding

Rivets isn't subscribing to your children because you are not using any bindings that require it.

project:description = non-subscribe binding
project.description = subscribe binding

If you don't want to subscribe to array changes (I think that's what you're asking for) you can do data-each-project=":projects"

@mattcreager
Copy link

@contra your my hero - do you have a stack overflow account? Copy & paste that answer over and I'll mark it as the correct answer 👍

@bitinn
Copy link

bitinn commented Feb 26, 2013

Sorry to jump on this thread, I have a similar question regarding this.

Given that Stapes.js currently support push with collection and set with array, which would be more appropriate to use with rivets each-* binding when appending is involved?

Consider one is to implement infinite scroll:

  1. Array would be 'natural' as it implies order but for Stapes we can only 'set' a new array, so we have to concat array manually;
  2. Collection does not guarantee order and as stated above adds complexity.

If we use the Array approach, how does rivets re-render upon change event? I would assume it re-render everything? Would it be more expensive than a Collection approach?

@yocontra
Copy link
Contributor

@bitinn - Rivets would re-render everything. You would want for a collection to emit a "reset" "add" and "remove events (like backbone or dermis) that would trigger custom crap if you didn't want it to re-render everything. The re-render is quick though you shouldn't even notice it

@bitinn
Copy link

bitinn commented Feb 26, 2013

@contra Thanks, the main problem with re-render is 1. it triggers scrollbar reset; 2. rendering takes about 1 seconds for about 200 items, and my case this can go to around 1000+.

I am think of custom event but that require adapter change right? Do you have an example that actual show rivets using more than the change event? I have a feeling this can get ugly fast...

@yocontra
Copy link
Contributor

@bitinn - Here is an example of a backbone one https://gist.github.com/wulftone/4751672 I don't think rivets supports incremental changes so a full re-render might be the only way - might want @mikeric to chime in on this

@bitinn
Copy link

bitinn commented Feb 27, 2013

@contra Thansk for the tip! coming from a knockout.js background I do enjoy its way of handling foreach loop (and on model side, a built-in observableArray to handle this use case, much like option 1 as @mikeric suggested.)

To quote:

Changes to the observableArray do not trigger the foreach binding directly (or the whole section would be re-rendered). Instead it triggers logic in a separate ko.computed that evaluates how the array has changed and makes the necessary incremental updates (add an item, remove an item, etc.).

http://stackoverflow.com/questions/14715724/how-to-use-custom-binding-with-ko-observablearray

Understand the idea of micro-framework is to keep things lite, but having this figure out on both stapes and rivets would improve dom performance a lot (minimize re-rendering).

@yocontra
Copy link
Contributor

@bitinn - I agree there does need to be more control over how an iteration binding renders changes. For large collections you can't really use rivets if it does a full re-render on a new element

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

5 participants