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

Optimize each routine #311

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 110 additions & 6 deletions spec/rivets/functional.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ describe('Functional', function() {
it('should set the text content of the element', function() {
el.setAttribute('data-text', 'data:foo');
rivets.bind(el, bindData);
debugger
expect(el.textContent || el.innerText).toBe(data.get('foo'));
});

Expand Down Expand Up @@ -99,7 +98,7 @@ describe('Functional', function() {
expect(input.value).toBe(data.get('foo'));
});
});

describe('Multiple', function() {
it('should bind a list of multiple elements', function() {
el.setAttribute('data-html', 'data:foo');
Expand Down Expand Up @@ -178,13 +177,118 @@ describe('Functional', function() {
expect(el.getElementsByTagName('li')[3]).toHaveTheTextContent('last');
});

it('should allow binding to the iterated element index', function() {
listItem.setAttribute('data-index', 'index');
it('should unbind and remove elements bound to items removed from the beginning of the collection', function () {
listItem.setAttribute('data-text', 'item.name');
rivets.bind(el, bindData);
expect(el.getElementsByTagName('li').length).toBe(2);
var collection = data.get('items').slice();
collection.shift();
data.set({items: collection});
// 2 -> 1
expect(el.getElementsByTagName('li').length).toBe(collection.length);
expect(el.getElementsByTagName('li')[0]).toHaveTheTextContent('b');

collection = collection.slice();
collection.push({name: 'c'}, {name: 'd'}, {name: 'e'});
data.set({items: collection});
expect(el.getElementsByTagName('li').length).toBe(collection.length);
expect(el.textContent).toBe('bcde');
collection = collection.slice();
collection.shift();
data.set({items: collection});
// 4 -> 3
expect(el.getElementsByTagName('li').length).toBe(collection.length);
expect(el.textContent).toBe('cde');
});

it('should unbind and remove elements bound to items removed from the end of the collection', function () {
listItem.setAttribute('data-text', 'item.name');
rivets.bind(el, bindData);
expect(el.getElementsByTagName('li')[0].getAttribute('index')).toBe('0')
expect(el.getElementsByTagName('li')[1].getAttribute('index')).toBe('1')
expect(el.getElementsByTagName('li').length).toBe(2);
var collection = data.get('items').slice();
collection.pop();
data.set({items: collection});
// 2 -> 1
expect(el.getElementsByTagName('li').length).toBe(collection.length);
expect(el.getElementsByTagName('li')[0]).toHaveTheTextContent('a');

collection = collection.slice();
collection.push({name: 'b'}, {name: 'c'}, {name: 'd'});
data.set({items: collection});
expect(el.getElementsByTagName('li').length).toBe(collection.length);
expect(el.textContent).toBe('abcd');
collection = collection.slice();
collection.pop();
data.set({items: collection});
// 4 -> 3
expect(el.getElementsByTagName('li').length).toBe(collection.length);
expect(el.textContent).toBe('abc');
});

it('should create a new element at the beginning of the container element when a new item is prepended to the collection', function () {
listItem.setAttribute('data-text', 'item.name');
rivets.bind(el, bindData);
expect(el.getElementsByTagName('li')[0]).toHaveTheTextContent('a');
var collection = data.get('items').slice();
collection.unshift({name: 'start'});
data.set({items: collection});
expect(el.getElementsByTagName('li')[0]).toHaveTheTextContent('start');
});

it('should create a new element at the end of the container element when a new item is appended to the collection', function () {
listItem.setAttribute('data-text', 'item.name');
rivets.bind(el, bindData);
var collection = data.get('items').slice();
collection.push({name: 'end'});
data.set({items: collection});
expect(el.getElementsByTagName('li').length).toBe(3);
expect(el.getElementsByTagName('li')[0]).toHaveTheTextContent('a');
expect(el.getElementsByTagName('li')[1]).toHaveTheTextContent('b');
expect(el.getElementsByTagName('li')[2]).toHaveTheTextContent('end');
});

it('should sort the bound elements according to order of items in the collection', function () {
listItem.setAttribute('data-text', 'item.name');
var collection = data.get('items').slice();
collection.push({name: 'e'}, {name: 'c'}, {name: 'd'});
data.set({items: collection});
rivets.bind(el, bindData);
expect(el.getElementsByTagName('li').length).toBe(collection.length);
expect(el.textContent).toBe('abecd');

// sort alphabetically
collection = collection.slice().sort(function (a, b) {
return a.name < b.name ? -1 : 1;
});
data.set({items: collection});
expect(el.textContent).toBe('abcde');

// reverse order
collection = collection.slice().reverse();
data.set({items: collection});
expect(el.textContent).toBe('edcba');

// move start to end
collection = collection.slice();
collection.push(collection.shift());
data.set({items: collection});
expect(el.textContent).toBe('dcbae');

// move end to start
collection = collection.slice();
collection.unshift(collection.pop());
data.set({items: collection});
expect(el.textContent).toBe('edcba');
});

it('should replace all bound elements to match the new collection and sort them according to order of items in the collection', function () {
listItem.setAttribute('data-text', 'item.name');
rivets.bind(el, bindData);
expect(el.textContent).toBe('ab');
data.set({items: [{name: 'e'}, {name: 'p'}, {name: 'r'}]});
expect(el.getElementsByTagName('li').length).toBe(3);
expect(el.textContent).toBe('epr');
});
});
});

Expand Down
135 changes: 108 additions & 27 deletions src/binders.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -164,25 +164,51 @@ Rivets.binders['each-*'] =
routine: (el, collection) ->
modelName = @args[0]
collection = collection or []

if @iterated.length > collection.length
for i in Array @iterated.length - collection.length
view = @iterated.pop()
view.unbind()
@marker.parentNode.removeChild view.els[0]
# mirror is a live copy of the list of all `modelName` models bound to this view
iterated_mirror = (iter.models[modelName] for iter in @iterated)
element_list = []

for model, index in collection
data = {index}
data[modelName] = model
iter_model = iterated_mirror[index]
# optimize the quickest check
if model is iter_model
continue

# see if we can find this model
iter_index = iterated_mirror.indexOf model
if ~ iter_index
# found it
# we already know that iter_index isnt index
# check if this item in @iterated was removed
if !~ collection.indexOf iter_model
# this item was removed
view = @iterated.splice(index, 1)[0]
view.unbind()
@marker.parentNode.removeChild view.els[0]
# reflect in the mirror
iterated_mirror.splice index, 1
# correct iter_index if needed
if index < iter_index
iter_index -= 1

# check that this index was not corrected by last removal
# or was removed itself from the views list
if iter_index isnt index and iterated_mirror[iter_index]
# move the view to the right index
view = @iterated.splice(iter_index, 1)[0]
@iterated.splice index, 0, view
# remove the view's element and queue it for inserting later
element_list.push { el: @marker.parentNode.removeChild(view.els[0]), idx: index }
# also fix the mirror's order
iterated_mirror.splice(index, 0, iterated_mirror.splice(iter_index, 1)[0])

if not @iterated[index]?
for key, model of @view.models
data[key] ?= model
else
# it's a new model in the collection, so bind it
data = {}
data[modelName] = model

previous = if @iterated.length
@iterated[@iterated.length - 1].els[0]
else
@marker
for key, view_model of @view.models
data[key] ?= view_model

options =
binders: @view.options.binders
Expand All @@ -196,24 +222,79 @@ Rivets.binders['each-*'] =
template = el.cloneNode true
view = new Rivets.View(template, data, options)
view.bind()
@iterated.push view

@marker.parentNode.insertBefore template, previous.nextSibling
else if @iterated[index].models[modelName] isnt model
@iterated[index].update data
@iterated.splice index, 0, view

# also add it to the mirror list so it mirrors indices properly
iterated_mirror.splice index, 0, model

element_list.push { el: template, idx: index }

# unbind views that no longer exist in collection
while @iterated.length > collection.length
view = @iterated.pop()
view.unbind()
@marker.parentNode.removeChild view.els[0]

# if we have elements in queue, put them back into the DOM
if element_list.length
element_list.sort (a, b) ->
if a.idx < b.idx then -1 else 1

buffers = []
last_buffer = null
# create buffers
for element in element_list
if not buffers.length
last_buffer = new ElementBuffer(element.el, element.idx)
buffers.push last_buffer
else
if not last_buffer.add element.el, element.idx
last_buffer = new ElementBuffer(element.el, element.idx)
buffers.push last_buffer

# insert buffers into DOM
for buffer in buffers
index = buffer.first_index
previous = if index and @iterated[index - 1]
@iterated[index - 1].els[0]
else
@marker
@marker.parentNode.insertBefore buffer.fragment, previous.nextSibling

if el.nodeName is 'OPTION'
for binding in @view.bindings
if binding.el is @marker.parentNode and binding.type is 'value'
binding.sync()

update: (models) ->
data = {}

for key, model of models
data[key] = model unless key is @args[0]

view.update data for view in @iterated
class ElementBuffer
constructor: (element, @last_index) ->
@fragment = element
@first_index = @last_index
@length = 1

add: (element, index) ->
if index is @first_index - 1
@insert element, true
@first_index = index
true
else if index is @last_index + 1
@insert element
@last_index = index
true
else
false

insert: (element, preppend) ->
if not @wrapped
fragment = document.createDocumentFragment()
fragment.appendChild @fragment
@fragment = fragment
@wrapped = true
if preppend
@fragment.insertBefore element, @fragment.firstChild
else
@fragment.appendChild element
@length += 1

# Adds or removes the class from the element when value is true or false.
Rivets.binders['class-*'] = (el, value) ->
Expand Down