diff --git a/README.md b/README.md index 8eb1e25..351707a 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,10 @@ arr.map(function(v) { // A work around is to use 'arr.length = 10' and then do 'arr[9] = "a"' ``` +## ReactiveArray +A reactive list implementation based on ReactiveArray. This implementation has a custom handlebars each helper extension. +Checkout the [api docs](http://boekkooi.github.io/reactive-extra/reactive-list.html). + ## Cake The current `cake` commands require the [wrench](https://github.com/ryanmcgrath/wrench-js) module, on windows the [which](https://github.com/isaacs/node-which) module is also required. diff --git a/docs/handlebars-list.html b/docs/handlebars-list.html new file mode 100644 index 0000000..ee9312b --- /dev/null +++ b/docs/handlebars-list.html @@ -0,0 +1,402 @@ + + + + + Handlebars each override + + + + + +
+
+ + + + +
+ + diff --git a/docs/reactive-array-test.html b/docs/reactive-array-test.html index e8d804e..d0b3c1e 100644 --- a/docs/reactive-array-test.html +++ b/docs/reactive-array-test.html @@ -19,6 +19,11 @@
+ + handlebars-list.coffee + + + reactive-array-test.coffee @@ -39,6 +44,16 @@ + + reactive-list-test.coffee + + + + + reactive-list.coffee + + + reactive-object-test.coffee @@ -82,7 +97,7 @@

reactive-array-test.coffee

-

Empty constructor

+

Empty constructor

@@ -99,7 +114,7 @@

reactive-array-test.coffee

-

length assign

+

length assign

@@ -124,7 +139,7 @@

reactive-array-test.coffee

-

push

+

push

@@ -144,7 +159,7 @@

reactive-array-test.coffee

-

pop

+

pop

@@ -161,7 +176,7 @@

reactive-array-test.coffee

-

shift

+

shift

@@ -178,7 +193,7 @@

reactive-array-test.coffee

-

unshift

+

unshift

@@ -194,7 +209,7 @@

reactive-array-test.coffee

-

splice

+

splice

@@ -211,7 +226,7 @@

reactive-array-test.coffee

-

bracket assign (index must exists)

+

bracket assign (index must exists)

@@ -229,7 +244,7 @@

reactive-array-test.coffee

-

sort

+

sort

@@ -249,7 +264,7 @@

reactive-array-test.coffee

-

length dep

+

length dep

@@ -268,7 +283,7 @@

reactive-array-test.coffee

-

indexOf dep

+

indexOf dep

@@ -287,7 +302,7 @@

reactive-array-test.coffee

-

lastIndexOf dep

+

lastIndexOf dep

@@ -306,7 +321,7 @@

reactive-array-test.coffee

-

add first item

+

add first item

@@ -325,7 +340,7 @@

reactive-array-test.coffee

-

bracket get dep

+

bracket get dep

@@ -344,7 +359,7 @@

reactive-array-test.coffee

-

add second item

+

add second item

@@ -364,7 +379,7 @@

reactive-array-test.coffee

-

add third item

+

add third item

@@ -384,7 +399,7 @@

reactive-array-test.coffee

-

add fourth item

+

add fourth item

@@ -404,7 +419,7 @@

reactive-array-test.coffee

-

change first

+

change first

@@ -424,7 +439,7 @@

reactive-array-test.coffee

-

sort

+

sort

@@ -444,7 +459,7 @@

reactive-array-test.coffee

-

shift

+

shift

@@ -462,7 +477,7 @@

reactive-array-test.coffee

-

unshift

+

unshift

diff --git a/docs/reactive-array.html b/docs/reactive-array.html index 385a623..ca2ec3d 100644 --- a/docs/reactive-array.html +++ b/docs/reactive-array.html @@ -19,6 +19,11 @@
+ + handlebars-list.coffee + + + reactive-array-test.coffee @@ -39,6 +44,16 @@ + + reactive-list-test.coffee + + + + + reactive-list.coffee + + + reactive-object-test.coffee @@ -65,15 +80,14 @@

reactive-array.coffee

  • -
    +
    -

    I wish i could use http://wiki.ecmascript.org/doku.php?id=harmony:proxies

    +

    Reactive Array

    -
    -class @ReactiveArray
    +            
    class @ReactiveArray
       constructor: () ->
  • @@ -85,7 +99,7 @@

    reactive-array.coffee

    -

    the actual array that we proxy to

    +

    the actual array that we proxy to

    @@ -100,7 +114,7 @@

    reactive-array.coffee

    -

    list of index dependencies

    +

    list of index dependencies

    @@ -115,7 +129,7 @@

    reactive-array.coffee

    -

    index dependency

    +

    index dependency

    @@ -130,7 +144,7 @@

    reactive-array.coffee

    -

    value dependency

    +

    value dependency

    @@ -145,7 +159,7 @@

    reactive-array.coffee

    -

    contains the current amount of properties linked with _list

    +

    contains the current amount of properties linked with _list

    @@ -160,7 +174,7 @@

    reactive-array.coffee

    -

    Define the length property proxy

    +

    Define the length property proxy

    @@ -185,7 +199,7 @@

    reactive-array.coffee

    -

    Assign arguments

    +

    Assign arguments

    @@ -202,8 +216,8 @@

    reactive-array.coffee

    -

    toArray

    -

    Returns a copy of the internal array

    +

    toArray

    +

    Returns a copy of the internal array

    @@ -221,14 +235,90 @@

    toArray

    -

    Accessor methods

    -
    -

    Optimized accessor methods for reactivity

    -

    indexOf

    -

    Returns the first index at which a given element can be found in the array, or -1 if it is not present. -Uses _.indexOf.

    -

    searchElement mixed as the element to locate in the array -fromIndex optional number as the index to start the search at

    +

    Mutator methods

    +
    +

    Optimized mutator methods for reactivity

    + + + +
      reverse: () ->
    + + + + +
  • +
    + +
    + +
    +

    Implement a custom array sort could be usefull based on http://jsperf.com/js-array-reverse-vs-while-loop/9 +but this works for small array's i still trust array.reverse a bit better

    + +
    + +
        Array.prototype.reverse.apply @_list
    +    `for (left = 0, right = this._list.length - 1; left < right; left += 1, right -= 1) {
    +      if (left === right) { continue; }
    +      if (this._listDeps[left]) { this._listDeps[left].changed(); }
    +      if (this._listDeps[right]) { this._listDeps[right].changed(); }
    +    }`
    +    @_listValueDep.changed()
    +    return @
    + +
  • + + +
  • +
    + +
    + +
    +

    sort

    +

    Sorts the elements of an array in place and returns the array.

    + +
    + +
      sort: () ->
    +    orgList = @_list.slice()
    +    Array.prototype.sort.apply @_list, arguments
    + +
  • + + +
  • +
    + +
    + +
    +

    Find the changed values and trigger there dependencies

    + +
    + +
        for dep, i in @_listDeps when dep && orgList[i] != @_list[i]
    +      dep.changed()
    +    @_listValueDep.changed()
    +    return @
    + +
  • + + +
  • +
    + +
    + +
    +

    Accessor methods

    +
    +

    Optimized accessor methods for reactivity

    +

    indexOf

    +

    Returns the first index at which a given element can be found in the array, or -1 if it is not present. +Uses _.indexOf.

    +

    searchElement mixed as the element to locate in the array +fromIndex optional number as the index to start the search at

    @@ -247,18 +337,18 @@

    indexOf

  • -
  • +
  • - +
    -

    lastIndexOf

    -

    Returns the last index at which a given element can be found in the array, or -1 if it is not present. -The array is searched backwards, starting at fromIndex. -Uses _.lastIndexOf.

    -

    searchElement mixed as the element to locate in the array -fromIndex optional number as the index at which to start searching backward

    +

    lastIndexOf

    +

    Returns the last index at which a given element can be found in the array, or -1 if it is not present. +The array is searched backwards, starting at fromIndex. +Uses _.lastIndexOf.

    +

    searchElement mixed as the element to locate in the array +fromIndex optional number as the index at which to start searching backward

    @@ -276,19 +366,19 @@

    lastIndexOf

  • -
  • +
  • - +
    -

    Iteration methods

    -
    -

    Optimized iteration methods for reactivity

    -

    forEach

    -

    Executes a provided function once per array element.

    -

    iterator function to execute for each element. -thisArg object to use as this when executing callback.

    +

    Iteration methods

    +
    +

    Optimized iteration methods for reactivity

    +

    forEach

    +

    Executes a provided function once per array element.

    +

    iterator function to execute for each element. +thisArg object to use as this when executing callback.

    @@ -300,16 +390,16 @@

    forEach

  • -
  • +
  • - +
    -

    every

    -

    Tests whether all elements in the array pass the test implemented by the provided function.

    -

    iterator function to test for each element. -thisArg object to use as this when executing callback.

    +

    every

    +

    Tests whether all elements in the array pass the test implemented by the provided function.

    +

    iterator function to test for each element. +thisArg object to use as this when executing callback.

    @@ -327,16 +417,16 @@

    every

  • -
  • +
  • - +
    -

    some

    -

    Tests whether some element in the array passes the test implemented by the provided function.

    -

    iterator function to test for each element. -thisArg object to use as this when executing callback.

    +

    some

    +

    Tests whether some element in the array passes the test implemented by the provided function.

    +

    iterator function to test for each element. +thisArg object to use as this when executing callback.

    @@ -354,34 +444,34 @@

    some

  • -
  • +
  • - +
    -

    EJSON Functions

    -
    -

    clone

    -

    EJSON::clone

    +

    EJSON Functions

    +
    +

    clone

    +

    EJSON::clone

      clone: () ->
    -    ReactiveArray.wrap @_list
    + @constructor.wrap @_list
  • -
  • +
  • - +
    -

    equals

    -

    EJSON::equals

    -

    obj object to compare

    +

    equals

    +

    EJSON::equals

    +

    obj object to compare

    @@ -393,14 +483,14 @@

    equals

  • -
  • +
  • - +
    -

    typeName

    -

    EJSON::typeName

    +

    typeName

    +

    EJSON::typeName

    @@ -410,14 +500,14 @@

    typeName

  • -
  • +
  • - +
    -

    toJSONValue

    -

    EJSON::toJSONValue

    +

    toJSONValue

    +

    EJSON::toJSONValue

    @@ -427,17 +517,17 @@

    toJSONValue

  • -
  • +
  • - +
    -

    Internal Functions

    -
    -

    _syncIndexProxies

    -

    Create proxy properties between the real array and the ReactiveArray

    -

    suppress boolean that indicates that changed should not be called

    +

    Internal Functions

    +
    +

    _syncIndexProxies

    +

    Create proxy properties between the real array and the ReactiveArray

    +

    suppress boolean that indicates that changed should not be called

    @@ -460,13 +550,13 @@

    _syncIndexProxies

  • -
  • +
  • - +
    -

    _defineIndexProperty

    +

    _defineIndexProperty

    @@ -480,13 +570,13 @@

    _defineIndexProperty

  • -
  • +
  • - +
    -

    _indexGet

    +

    _indexGet

    @@ -498,35 +588,36 @@

    _indexGet

  • -
  • +
  • - +
    -

    _indexSet

    +

    _indexSet

      _indexSet: (i, val) ->
    -    @_list[i] = val
    -    @_listDeps[i]?.changed()
    -    @_listValueDep.changed()
    +    if @_list[i] != val
    +      @_list[i] = val
    +      @_listDeps[i]?.changed()
    +      @_listValueDep.changed()
         val
  • -
  • +
  • - +
    -

    _definePrivateProperty

    -

    Create a configurable, writable, none enumerable property

    -

    name string as the name of the property -value mixed as the value to assign

    +

    _definePrivateProperty

    +

    Create a configurable, writable, none enumerable property

    +

    name string as the name of the property +value mixed as the value to assign

    @@ -540,28 +631,28 @@

    _definePrivateProperty

  • -
  • +
  • - +
    -

    Array Proxies

    -
    +

    Array Proxies

    +
  • -
  • +
  • - +
    -

    Mutator methods

    -

    Create proxy methods

    +

    Mutator methods

    +

    Create proxy methods

    @@ -577,41 +668,19 @@

    Mutator methods

    rtn = Array.prototype[m].apply @_list, arguments @_syncIndexProxies() dep.changed() for dep, i in @_listDeps when dep && orgList[i] != @_list[i] - rtn - -_.each ['reverse','sort'], (m) -> - ReactiveArray.prototype[m] = () -> - orgList = @_list.slice() - Array.prototype[m].apply @_list, arguments - -
  • - - -
  • -
    - -
    - -
    -

    Find the changed values and trigger there dependencies

    - -
    - -
        dep.changed() for dep, i in @_listDeps when dep && orgList[i] != @_list[i]
    -    @_listValueDep.changed()
    -    return @
    + rtn
  • -
  • +
  • - +
    -

    Accessor methods

    -

    Create Accessor proxy methods

    +

    Accessor methods

    +

    Create Accessor proxy methods

    @@ -620,7 +689,7 @@

    Accessor methods

    rtn = Array.prototype[m].apply @_list, arguments @_listLengthDep.depend() @_listValueDep.depend() - ReactiveArray.wrap rtn + @constructor.wrap rtn _.each ['join','toString'], (m) -> ReactiveArray.prototype[m] = () -> @@ -632,14 +701,14 @@

    Accessor methods

  • -
  • +
  • - +
    -

    Iteration methods

    -

    Create iteration proxy methods for filter, map that are using underscore.js

    +

    Iteration methods

    +

    Create iteration proxy methods for filter, map that are using underscore.js

    @@ -654,18 +723,18 @@

    Iteration methods

    @_listLengthDep.depend() @_listValueDep.depend() - ReactiveArray.wrap rtn + @constructor.wrap rtn
  • -
  • +
  • - +
    -

    Create iteration proxy methods for reduce, reduceRight that are using underscore.js

    +

    Create iteration proxy methods for reduce, reduceRight that are using underscore.js

    @@ -688,16 +757,16 @@

    Iteration methods

  • -
  • +
  • - +
    -

    Helper methods

    -
    -

    wrap

    -

    Method for wrapping a array

    +

    Helper methods

    +
    +

    wrap

    +

    Method for wrapping a array

    @@ -710,14 +779,14 @@

    wrap

  • -
  • +
  • - +
    -

    EJSON add ReactiveArray

    -

    EJSON.addType

    +

    EJSON add ReactiveArray

    +

    EJSON.addType

    diff --git a/docs/reactive-dictionary-test.html b/docs/reactive-dictionary-test.html index 90a8655..07fc699 100644 --- a/docs/reactive-dictionary-test.html +++ b/docs/reactive-dictionary-test.html @@ -19,6 +19,11 @@
    + + handlebars-list.coffee + + + reactive-array-test.coffee @@ -39,6 +44,16 @@ + + reactive-list-test.coffee + + + + + reactive-list.coffee + + + reactive-object-test.coffee diff --git a/docs/reactive-dictionary.html b/docs/reactive-dictionary.html index c718624..7a0b7c4 100644 --- a/docs/reactive-dictionary.html +++ b/docs/reactive-dictionary.html @@ -19,6 +19,11 @@
    + + handlebars-list.coffee + + + reactive-array-test.coffee @@ -39,6 +44,16 @@ + + reactive-list-test.coffee + + + + + reactive-list.coffee + + + reactive-object-test.coffee diff --git a/docs/reactive-list-test.html b/docs/reactive-list-test.html new file mode 100644 index 0000000..341f06c --- /dev/null +++ b/docs/reactive-list-test.html @@ -0,0 +1,514 @@ + + + + + reactive-list-test.coffee + + + + + +
    +
    + + + +
      + +
    • +
      +

      reactive-list-test.coffee

      +
      +
    • + + + +
    • +
      + +
      + +
      + +
      + +
      Tinytest.add "ReactiveList - added/addedAt", (test) ->
      +  invalidX = 0
      +  invalidCall = () ->
      +    invalidX++
      +
      +  addedAtX = 0
      +  addedX = 0
      +  addedVal = 'init'
      +  addedIdx = 0
      +  callbacks =
      +    added: (val) ->
      +      test.equal val, addedVal
      +      addedX++
      +    addedAt: (val, idx) ->
      +      test.equal val, addedVal
      +      test.equal idx, addedIdx
      +      addedAtX++
      +    changed: invalidCall
      +    changedAt: invalidCall
      +    removed: invalidCall
      +    removedAt: invalidCall
      +    movedTo: invalidCall
      +
      +  list = new ReactiveList('init')
      + +
    • + + +
    • +
      + +
      + +
      +

      observe: test initial added event's

      + +
      + +
        handle = list.observe callbacks
      +  test.equal invalidX, 0
      +  test.equal addedX, 1
      +  test.equal addedAtX, 1
      + +
    • + + +
    • +
      + +
      + +
      +

      push: test added

      + +
      + +
        addedVal = 'push'
      +  addedIdx = 1
      +  list.push addedVal
      +  test.equal invalidX, 0
      +  test.equal addedX, 2
      +  test.equal addedAtX, 2
      + +
    • + + +
    • +
      + +
      + +
      +

      unshift: test added

      + +
      + +
        addedVal = 'unshift'
      +  addedIdx = 0
      +  list.unshift addedVal
      +  test.equal invalidX, 0
      +  test.equal addedX, 3
      +  test.equal addedAtX, 3
      + +
    • + + +
    • +
      + +
      + +
      +

      splice: test added

      + +
      + +
        addedVal = 'splice1'
      +  addedIdx = 1
      +  list.splice 1, 0, 'splice1'
      +  test.equal invalidX, 0
      +  test.equal addedX, 4
      +  test.equal addedAtX, 4
      +
      +  addedVal = 'splice2'
      +  addedIdx = 3
      +  list.splice -1, 0, 'splice2'
      +  test.equal invalidX, 0
      +  test.equal addedX, 5
      +  test.equal addedAtX, 5
      +
      +Tinytest.add "ReactiveList - remove/removeAt", (test) ->
      +  invalidX = -14
      +  invalidCall = () ->
      +    invalidX++
      +
      +  removedAtX = 0
      +  removedX = 0
      +  removedVal = null
      +  removedIdx = 0
      +  callbacks =
      +    removed: (val) ->
      +      test.equal val, removedVal
      +      removedX++
      +    removedAt: (val, idx) ->
      +      test.equal val, removedVal
      +      test.equal idx, removedIdx
      +      removedAtX++
      +    changed: invalidCall
      +    changedAt: invalidCall
      +    added: invalidCall
      +    addedAt: invalidCall
      +    movedTo: invalidCall
      +
      +  list = new ReactiveList('shift','keep1','splice1','keep2','splice2','keep3','pop')
      +
      +  handle = list.observe callbacks
      +  test.equal invalidX, 0, 'observer'
      +  test.equal removedX, 0, 'observer'
      +  test.equal removedAtX, 0, 'observer'
      + +
    • + + +
    • +
      + +
      + +
      +

      pop

      + +
      + +
        removedVal = 'pop'
      +  removedIdx = 6
      +  list.pop()
      +  test.equal invalidX, 0, 'pop'
      +  test.equal removedX, 1, 'pop'
      +  test.equal removedAtX, 1, 'pop'
      + +
    • + + +
    • +
      + +
      + +
      +

      shift

      + +
      + +
        removedVal = 'shift'
      +  removedIdx = 0
      +  list.shift()
      +  test.equal invalidX, 0
      +  test.equal removedX, 2
      +  test.equal removedAtX, 2
      + +
    • + + +
    • +
      + +
      + +
      +

      splice: test added

      + +
      + +
        removedVal = 'splice1'
      +  removedIdx = 1
      +  list.splice 1, 1
      +  test.equal invalidX, 0
      +  test.equal removedX, 3
      +  test.equal removedAtX, 3
      +
      +  removedVal = 'splice2'
      +  removedIdx = 2
      +  list.splice -2, 1
      +  test.equal invalidX, 0
      +  test.equal removedX, 4
      +  test.equal removedAtX, 4
      +
      +Tinytest.add "ReactiveList - splice", (test) ->
      +  addedX = addedAtX = -7
      +  changedX = changedAtX = 0
      +  removedX = removedAtX = 0
      +  movedX = 0
      +
      +  changedRun = [
      +    { val: '2', newVal: 'replacement1', idx: 1 } # run 1
      +    { val: '4', newVal: 'replacement2', idx: 3 } # run 2
      +    { val: '5', newVal: 'replacement3', idx: 4 }
      +
      +    { val: '3', newVal: 'replacement4', idx: 2 } # run 3
      +    { val: 'replacement4', newVal: 'replacement5', idx: 2 } # run 4
      +  ]
      +  addedRun = [
      +    { val: '3.5', idx: 3 } # run 3
      +  ]
      +  removedRun = [
      +    { val: '3.5', idx: 3 } # run 4
      +  ]
      +  movedRun = []
      +
      +  callbacks =
      +    added: (val) ->
      +      if addedX >= 0
      +        eq = addedRun[addedX]
      +        test.equal val, eq.val, 'added - val: ' + addedX
      +      addedX++
      +    addedAt: (val, idx) ->
      +      if addedAtX >= 0
      +        eq = addedRun[addedAtX]
      +        test.equal val, eq.val, 'addedAt - val: ' + addedAtX
      +        test.equal idx, eq.idx, 'addedAt - idx: ' + addedAtX
      +      addedAtX++
      +    changed: (val, oldVal) ->
      +      eq = changedRun[changedX]
      +      test.equal val, eq.newVal, 'changed - val: ' + changedX
      +      test.equal oldVal, eq.val, 'changed - oldVal: ' + changedX
      +      changedX++
      +    changedAt: (val, oldVal, idx) ->
      +      eq = changedRun[changedAtX]
      +      test.equal val, eq.newVal, 'changedAt - val: ' + changedAtX
      +      test.equal oldVal, eq.val, 'changedAt - oldVal: ' + changedAtX
      +      test.equal idx, eq.idx, 'changedAt - idx: ' + changedAtX
      +      changedAtX++
      +    removed:  (val) ->
      +      eq = removedRun[removedX]
      +      test.equal val, eq.val, 'removed - val: ' + removedX
      +      removedX++
      +    removedAt: (val, idx) ->
      +      eq = removedRun[removedAtX]
      +      test.equal val, eq.val, 'removedAt - val: ' + removedAtX
      +      test.equal idx, eq.idx, 'removedAt - idx: ' + removedAtX
      +      removedAtX++
      +    movedTo: (val, idx) ->
      +      eq = movedRun[movedX]
      +      test.equal val, eq.val, 'movedTo - val: ' + movedX
      +      test.equal idx, eq.idx, 'movedTo - idx: ' + movedX
      +      movedX++
      +
      +  list = new ReactiveList('1','2','3','4','5','6','7')
      + +
    • + + +
    • +
      + +
      + +
      +

      Attach callbacks

      + +
      + +
        list.observe callbacks
      +  test.equal addedX, 0, 'added: observe'
      +  test.equal addedAtX, 0, 'addedAt: observe'
      +  test.equal removedX, 0, 'removedAt: observe'
      +  test.equal removedAtX, 0, 'removedAt: observe'
      +  test.equal changedX, 0, 'changed: observe'
      +  test.equal changedAtX, 0, 'changedAt: observe'
      +  test.equal movedX, 0, 'moved: observe'
      + +
    • + + +
    • +
      + +
      + +
      +

      splice

      + +
      + +
        list.splice 1, 1, changedRun[0].newVal
      +  test.equal addedX, 0, 'added: run 1'
      +  test.equal addedAtX, 0, 'addedAt: run 1'
      +  test.equal removedX, 0, 'removedAt: run 1'
      +  test.equal removedAtX, 0, 'removedAt: run 1'
      +  test.equal changedX, 1, 'changed: run 1'
      +  test.equal changedAtX, 1, 'changedAt: run 1'
      +  test.equal movedX, 0, 'moved: run 1'
      + +
    • + + +
    • +
      + +
      + +
      +

      splice

      + +
      + +
        list.splice 3, 2, changedRun[1].newVal, changedRun[2].newVal
      +  test.equal addedX, 0, 'added: run 2'
      +  test.equal addedAtX, 0, 'addedAt: run 2'
      +  test.equal removedX, 0, 'removedAt: run 2'
      +  test.equal removedAtX, 0, 'removedAt: run 2'
      +  test.equal changedX, 3, 'changed: run 2'
      +  test.equal changedAtX, 3, 'changedAt: run 2'
      +  test.equal movedX, 0, 'moved: run 2'
      + +
    • + + +
    • +
      + +
      + +
      +

      splice

      + +
      + +
        list.splice 2, 1, changedRun[3].newVal, addedRun[0].val
      +  test.equal addedX, 1, 'added: run 3'
      +  test.equal addedAtX, 1, 'addedAt: run 3'
      +  test.equal removedX, 0, 'removedAt: run 3'
      +  test.equal removedAtX, 0, 'removedAt: run 3'
      +  test.equal changedX, 4, 'changed: run 3'
      +  test.equal changedAtX, 4, 'changedAt: run 3'
      +  test.equal movedX, 0, 'moved: run 3'
      + +
    • + + +
    • +
      + +
      + +
      +

      splice

      + +
      + +
        list.splice 2, 2, changedRun[4].newVal
      +  test.equal addedX, 1, 'added: run 4'
      +  test.equal addedAtX, 1, 'addedAt: run 4'
      +  test.equal removedX, 1, 'removedAt: run 4'
      +  test.equal removedAtX, 1, 'removedAt: run 4'
      +  test.equal changedX, 5, 'changed: run 4'
      +  test.equal changedAtX, 5, 'changedAt: run 4'
      +  test.equal movedX, 0, 'moved: run 4'
      +
      +Tinytest.add "ReactiveList - reverse", (test) ->
      +  arr = ['1','2','3','4','5','6','7']
      +  arrReversed = arr.slice().reverse()
      +  list = ReactiveList.wrap(arr)
      +
      +  list.observe
      +    movedTo: (val, fromIdx, toIdx) ->
      +      arr.splice fromIdx, 1
      +      arr.splice toIdx, 0, val
      +
      +  test.equal list.reverse(), ReactiveList.wrap(arrReversed)
      +  test.equal arr, arrReversed
      +
      +  list.pop()
      +  arr.pop()
      +  arrReversed.pop()
      +  arrReversed.reverse()
      +  test.equal list.reverse(), ReactiveList.wrap(arrReversed)
      +  test.equal arr, arrReversed
      +
      +Tinytest.add "ReactiveList - sort", (test) ->
      +  arrs = [
      +    ['1','7','3','4','2','6','8','5']
      +    ['1','2','3','4','5','6','7'].reverse()
      +    ['1','7','3','7','4','2','6','8','5']
      +    ['1','7','3','7','7','4','7','2','6','8','5'],
      +    ['d', 'a', 'c', 'b', 'z', 'y', 'y']
      +  ]
      +  for arr in arrs
      +    arrSorted = arr.slice().sort()
      +    list = ReactiveList.wrap(arr)
      +
      +    list.observe
      +      movedTo: (docVal, fromIdx, toIdx) ->
      +        val = (arr.splice fromIdx, 1)[0]
      +        test.equal val, docVal
      +        arr.splice toIdx, 0, val
      +
      +    test.equal list.sort(), ReactiveList.wrap(arrSorted)
      +    test.equal arr, arrSorted
      + +
    • + +
    +
    + + diff --git a/docs/reactive-list.html b/docs/reactive-list.html new file mode 100644 index 0000000..716b776 --- /dev/null +++ b/docs/reactive-list.html @@ -0,0 +1,640 @@ + + + + + reactive-list.coffee + + + + + +
    +
    + + + +
      + +
    • +
      +

      reactive-list.coffee

      +
      +
    • + + + +
    • +
      + +
      + +
      +

      Reactive List

      +

      Represents a reactive list extended from a ReactiveArray. +A ReactiveList adds the observe function.

      +

      Example

      +
      list = new ReactiveList('first');
      +list.observer({
      +  added: function(newDoc) { console.log("added", newDoc) }
      +});
      +list.push('second');
      + +
      + +
      class @ReactiveList extends ReactiveArray
      +  constructor: () ->
      + +
    • + + +
    • +
      + +
      + +
      +

      A array of active lineHandlers

      + +
      + +
          @_definePrivateProperty '_handlers', []
      +
      +    super
      + +
    • + + +
    • +
      + +
      + +
      +

      observer

      +

      Based on cursor.observe

      + +
      + +
        observe: (callbacks) ->
      +    handle = new LiveHandler callbacks
      +    @_handlers.push handle
      +    @_trigger 'added', @_list[i], i for i in [0...@_list.length] by 1
      +    handle
      + +
    • + + +
    • +
      + +
      + +
      +

      Mutator methods

      +
      +

      Optimized mutator methods for reactivity

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      pop

      +

      Removes the last element from an array and returns that element.

      + +
      + +
        pop: () ->
      +    rtn = super
      +    @_trigger 'removed', rtn, @_list.length
      +    rtn
      + +
    • + + +
    • +
      + +
      + +
      +

      #

      +

      Adds one or more elements to the end of an array and returns the new length of the array.

      + +
      + +
        push: () ->
      +    orgLength = @_list.length
      +    rtn = super
      + +
    • + + +
    • +
      + +
      + +
      +

      Fire event, added, addedAt

      + +
      + +
          @_trigger 'added', @_list[i], i for i in [orgLength...@_list.length] by 1
      +    rtn
      + +
    • + + +
    • +
      + +
      + +
      +

      shift

      +

      Removes the first element from an array and returns that element.

      + +
      + +
        shift: () ->
      +    rtn = super
      +    @_trigger 'removed', rtn, 0
      +    rtn
      + +
    • + + +
    • +
      + +
      + +
      +

      unshift

      +

      Adds one or more elements to the front of an array and returns the new length of the array.

      + +
      + +
        unshift: () ->
      +    orgLength = @_list.length
      +    rtn = super
      + +
    • + + +
    • +
      + +
      + +
      +

      Fire event, added, addedAt

      + +
      + +
          @_trigger 'added', @_list[i], i for i in [0...(@_list.length-orgLength)] by 1
      +    rtn
      + +
    • + + +
    • +
      + +
      + +
      +

      splice

      +

      Adds and/or removes elements from an array.

      + +
      + +
        splice: () ->
      +    orgList = @_list.slice()
      +    rtn = super
      + +
    • + + +
    • +
      + +
      + +
      +

      start index

      + +
      + +
          idx = arguments[0]
      +    idx = orgList.length + idx if idx < 0
      +    rmAmount = if arguments.length > 1 then arguments[1] else orgList.length - idx
      + +
    • + + +
    • +
      + +
      + +
      +

      Elements where added/changed

      + +
      + +
          if arguments.length > 2
      +      addAmount = arguments.length - 2
      +      if rmAmount > 0
      +        changedAmount = if rmAmount > addAmount then addAmount else rmAmount
      +        for i in [0...changedAmount] by 1
      +          @_trigger 'changed', @_list[idx], orgList[idx], idx
      +          idx++
      +        addAmount = addAmount - changedAmount
      +        rmAmount = rmAmount - changedAmount
      +
      +      if (rmAmount-addAmount) > 0
      +        @_trigger 'removed', orgList[idx + i], idx + i for i in [0...rmAmount-addAmount] by 1
      +      else if (rmAmount-addAmount) < 0
      + +
    • + + +
    • +
      + +
      + +
      +

      only elements where added

      + +
      + +
              @_trigger 'added', @_list[idx + i], idx + i for i in [0...addAmount-rmAmount] by 1
      +    else if rmAmount > 0
      +      @_trigger 'removed', orgList[idx + i], idx + i for i in [0...rmAmount] by 1
      +
      +    rtn
      + +
    • + + +
    • +
      + +
      + +
      +

      reverse

      +

      Reverses an array in place. The first array element becomes the last and the last becomes the first.

      + +
      + +
        reverse: () ->
      +    super
      +    array = @_list;
      +    length = @_list.length
      +    `for (left = 0, right = length - 1; left < right; left += 1, right -= 1) {
      +      if (right === left) { continue; }
      +      this._trigger('movedTo', array[left], right, left);
      +      this._trigger('movedTo', array[right], left+1, right);
      +    }`
      +    return @
      + +
    • + + +
    • +
      + +
      + +
      +

      sort

      +

      Sorts the elements of an array in place and returns the array.

      + +
      + +
        sort: () ->
      +    org = @_list.slice()
      +    super
      +
      +    return @ if !@_hasActiveTrigger 'movedTo'
      + +
    • + + +
    • +
      + +
      + +
      +

      Create a list of moves that results in @_list

      + +
      + +
          length = @_list.length
      +    moves = []
      +    currentPosition = 0
      +    while currentPosition < length
      +      finalPosition = @_list.indexOf(org[currentPosition])
      + +
    • + + +
    • +
      + +
      + +
      +

      A movement of one won't happen

      + +
      + +
            if currentPosition+1 == finalPosition
      + +
    • + + +
    • +
      + +
      + +
      +

      Look forward maybe we have a group here

      + +
      + +
              while org[currentPosition+1] == @_list[finalPosition+1]
      +          finalPosition++
      +          currentPosition++
      +        if org[currentPosition] == @_list[finalPosition]
      +          finalPosition++
      +          currentPosition++
      +        finalPosition = @_list.indexOf(org[currentPosition])
      + +
    • + + +
    • +
      + +
      + +
      +

      This is my evil way of detecting duplicates

      + +
      + +
            if org[currentPosition] == org[currentPosition+1]
      +        while org[currentPosition-1] == @_list[finalPosition]
      +          finalPosition++
      +        if org[currentPosition] == @_list[finalPosition]
      +          finalPosition++
      +        finalPosition = @_list.indexOf(org[currentPosition], finalPosition)
      +
      +      move =
      +        from: currentPosition
      +        to: finalPosition
      +      skip = finalPosition == -1 || lastMove && lastMove.to == move.to && lastMove.from == move.from
      +
      +      if !skip && finalPosition != currentPosition
      +        moves.push move
      +        lastMove = move
      +        org.splice move.to, 0, (org.splice move.from, 1)[0]
      +
      +        if finalPosition < currentPosition
      +          currentPosition = finalPosition
      +        else
      +          currentPosition--
      +      currentPosition++
      +
      +    for move in moves
      +      this._trigger 'movedTo', @_list[move.to], move.from, move.to
      +
      +    return @
      + +
    • + + +
    • +
      + +
      + +
      +

      EJSON Functions

      +
      +

      These are overrides from ReactiveArray

      +

      typeName

      +

      EJSON::typeName

      + +
      + +
        typeName: () ->
      +    'reactive-list'
      + +
    • + + +
    • +
      + +
      + +
      +

      equals

      +

      EJSON::equals

      +

      obj object to compare

      + +
      + +
        equals: (obj) ->
      +    return obj? &&
      +      obj instanceof ReactiveList &&
      +      _.isEqual obj._list, @_list
      + +
    • + + +
    • +
      + +
      + +
      +

      Internal Functions

      +
      +

      _trigger

      +

      trigger a event to all observeables

      + +
      + +
        _trigger: (evt) ->
      +    self = @
      +    args = _.toArray(arguments).slice(1)
      +    if evt == 'movedTo'
      +      trigger = (callbacks) ->
      +        callbacks[evt].apply  self, args if evt of callbacks
      +    else
      +      evtArgs = args.slice(0, -1)
      +      evtAt = evt + 'At'
      +      evtAtArgs = args
      +      trigger = (callbacks) ->
      +        callbacks[evt].apply  self, evtArgs if evt of callbacks
      +        callbacks[evtAt].apply self, evtAtArgs if evtAt of callbacks
      +
      +    for i, handler of @_handlers when i of @_handlers
      +      if handler.stopped
      +        delete @_handlers[i]
      +        continue
      +      trigger handler.callbacks
      +    return
      +
      +  _hasActiveTrigger: (evt) ->
      +    _.any @_handlers, (handler) ->
      +      return !handler.stopped && evt of handler.callbacks
      + +
    • + + +
    • +
      + +
      + +
      +

      _indexSet

      + +
      + +
        _indexSet: (idx, val) ->
      +    rtn = val
      +    if @_list[idx] != val
      +      org = list[idx]
      +      rtn = super
      +      @_trigger 'changed', @_list[idx], org, idx
      +    rtn
      + +
    • + + +
    • +
      + +
      + +
      +

      Helper methods

      +
      +

      wrap

      +

      Method for wrapping a array

      + +
      + +
      ReactiveList.wrap = (arr) ->
      +  obj = new ReactiveList
      +  obj._list = _.toArray arr
      +  obj._syncIndexProxies(true)
      +  obj
      + +
    • + + +
    • +
      + +
      + +
      +

      EJSON add ReactiveArray

      +

      EJSON.addType

      + +
      + +
      EJSON.addType 'reactive-list', (jsonObj) ->
      +  ReactiveList.wrap jsonObj
      +
      +class LiveHandler
      +  constructor: (callbacks) ->
      +    self = @
      +    @stopped = false
      +    @callbacks = callbacks
      +
      +    if (Deps.active)
      +      Deps.onInvalidate () ->
      +        self.stop()
      +
      +  stop: () ->
      +    @stopped = true
      + +
    • + +
    +
    + + diff --git a/docs/reactive-object-test.html b/docs/reactive-object-test.html index e9a011e..9dc0943 100644 --- a/docs/reactive-object-test.html +++ b/docs/reactive-object-test.html @@ -19,6 +19,11 @@
    + + handlebars-list.coffee + + + reactive-array-test.coffee @@ -39,6 +44,16 @@ + + reactive-list-test.coffee + + + + + reactive-list.coffee + + + reactive-object-test.coffee diff --git a/docs/reactive-object.html b/docs/reactive-object.html index 8e83405..9480799 100644 --- a/docs/reactive-object.html +++ b/docs/reactive-object.html @@ -19,6 +19,11 @@
    + + handlebars-list.coffee + + + reactive-array-test.coffee @@ -39,6 +44,16 @@ + + reactive-list-test.coffee + + + + + reactive-list.coffee + + + reactive-object-test.coffee diff --git a/example/client/main.html b/example/client/main.html new file mode 100644 index 0000000..3812e64 --- /dev/null +++ b/example/client/main.html @@ -0,0 +1,21 @@ + + Reactive Extra Examples + + + + {{> list }} + + + \ No newline at end of file diff --git a/example/client/main.js b/example/client/main.js new file mode 100644 index 0000000..15b9686 --- /dev/null +++ b/example/client/main.js @@ -0,0 +1,24 @@ +MyList = new ReactiveList('a', 'b', 'c'); + +Template.list.list = function () { + return MyList; +}; + +Template.list.events({ + 'click #list-reverse': function(event) { + event.preventDefault(); + MyList.reverse(); + }, + 'click #list-add': function(event) { + event.preventDefault(); + MyList.push((new Date()).toLocaleTimeString()); + }, + 'click #list-remove': function(event) { + event.preventDefault(); + MyList.pop(); + }, + 'click #list-sort': function(event) { + event.preventDefault(); + MyList.sort(); + } +}); \ No newline at end of file diff --git a/example/packages/reactive-extra/lib/handlebars-list.js b/example/packages/reactive-extra/lib/handlebars-list.js new file mode 100644 index 0000000..fd62e42 --- /dev/null +++ b/example/packages/reactive-extra/lib/handlebars-list.js @@ -0,0 +1,185 @@ +// Generated by CoffeeScript 1.6.2 +(function() { + var HandlebarsEach, SparkListObserve, findParentOfType, makeRange; + + HandlebarsEach = Handlebars._default_helpers.each; + + Handlebars._default_helpers.each = function(arg, options) { + var elseFunc, itemFunc; + + if (!(arg && arg instanceof ReactiveList)) { + return HandlebarsEach.call(this, arg, options); + } + itemFunc = function(item) { + return Spark.labelBranch((item && item._id) || Spark.UNIQUE_LABEL, function() { + return Spark.setDataContext(item, Spark.isolate(_.bind(options.fn, null, item))); + }); + }; + elseFunc = function() { + if (options.inverse) { + return Spark.isolate(options.inverse); + } else { + return ''; + } + }; + return SparkListObserve(arg, itemFunc, elseFunc); + }; + + Spark._ANNOTATION_LIST_OBSERVE = "list_observe"; + + Spark._ANNOTATION_LIST_OBSERVE_ITEM = "list_observe_item"; + + SparkListObserve = function(observable, itemFunc, elseFunc) { + var callbacks, cleanup, handle, html, itemArr, later, maybeAnnotate, notifyParentsRendered, observerCallbacks, outerRange, renderer, stopped; + + elseFunc = elseFunc || function() { + return ''; + }; + callbacks = {}; + observerCallbacks = {}; + _.each(["addedAt", "changedAt", "removedAt", "movedTo"], function(name) { + return observerCallbacks[name] = function() { + return callbacks[name].apply(null, arguments); + }; + }); + itemArr = []; + _.extend(callbacks, { + addedAt: function(val, idx) { + return itemArr[idx] = { + liveRange: null, + value: val + }; + } + }); + handle = observable.observe(observerCallbacks); + renderer = Spark._currentRenderer.get(); + maybeAnnotate = renderer ? _.bind(renderer.annotate, renderer) : function(html) { + return html; + }; + html = ''; + outerRange = null; + if (itemArr.length === 0) { + html = elseFunc(); + } else { + _.each(itemArr, function(elt) { + return html += maybeAnnotate(itemFunc(elt.value), Spark._ANNOTATION_LIST_OBSERVE_ITEM, function(range) { + elt.liveRange = range; + }); + }); + } + stopped = false; + cleanup = function() { + handle.stop(); + return stopped = true; + }; + html = maybeAnnotate(html, Spark._ANNOTATION_LIST_OBSERVE, function(range) { + if (!range) { + cleanup(); + return; + } + outerRange = range; + outerRange.finalize = cleanup; + }); + if (!renderer) { + cleanup(); + return html; + } + notifyParentsRendered = function() { + var walk; + + walk = outerRange; + while ((walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk))) { + walk.rendered.call(walk.landmark); + } + }; + later = function(func) { + Deps.afterFlush(function() { + if (!stopped) { + func(); + } + }); + }; + _.extend(callbacks, { + addedAt: function(val, idx) { + return later(function() { + var frag, range; + + frag = Spark.render(_.bind(itemFunc, null, val)); + DomUtils.wrapFragmentForContainer(frag, outerRange.containerNode()); + range = makeRange(Spark._ANNOTATION_LIST_ITEM, frag); + if (itemArr.length === 0) { + Spark.finalize(outerRange.replaceContents(frag)); + } else { + itemArr[idx - 1].liveRange.insertAfter(frag); + } + return itemArr[idx] = { + liveRange: range, + value: val + }; + }); + }, + removedAt: function(val, idx) { + return later(function() { + var frag; + + if (itemArr.length === 1) { + frag = Spark.render(elseFunc); + DomUtils.wrapFragmentForContainer(frag, outerRange.containerNode()); + Spark.finalize(outerRange.replaceContents(frag)); + } else { + Spark.finalize(itemArr[idx].liveRange.extract()); + } + itemArr.splice(idx, 1); + return notifyParentsRendered(); + }); + }, + movedTo: function(val, fromIdx, toIdx) { + return later(function() { + var elt, frag; + + elt = (itemArr.splice(fromIdx, 1))[0]; + frag = elt.liveRange.extract(); + if (toIdx in itemArr) { + itemArr[toIdx].liveRange.insertBefore(frag); + } else { + itemArr[toIdx - 1].liveRange.insertAfter(frag); + } + itemArr.splice(toIdx, 0, elt); + return notifyParentsRendered(); + }); + }, + changedAt: function(val, idx) { + return later(function() { + var elt; + + elt = itemArr[idx]; + if (!elt) { + throw new Error("Unknown item at index: " + idx); + } + elt.value = val; + return Spark.renderToRange(elt.liveRange, _.bind(itemFunc, null, elt.value)); + }); + } + }); + return html; + }; + + findParentOfType = function(type, range) { + while (true) { + range = range.findParent(); + if (!(range && range.type !== type)) { + break; + } + } + return range; + }; + + makeRange = function(type, start, end, inner) { + var range; + + range = new LiveRange(Spark._TAG, start, end, inner); + range.type = type; + return range; + }; + +}).call(this); diff --git a/example/packages/reactive-extra/lib/reactive-array.js b/example/packages/reactive-extra/lib/reactive-array.js index 8531cf5..7603587 100644 --- a/example/packages/reactive-extra/lib/reactive-array.js +++ b/example/packages/reactive-extra/lib/reactive-array.js @@ -35,6 +35,33 @@ return this._list.slice(); }; + ReactiveArray.prototype.reverse = function() { + Array.prototype.reverse.apply(this._list); + for (left = 0, right = this._list.length - 1; left < right; left += 1, right -= 1) { + if (left === right) { continue; } + if (this._listDeps[left]) { this._listDeps[left].changed(); } + if (this._listDeps[right]) { this._listDeps[right].changed(); } + }; + this._listValueDep.changed(); + return this; + }; + + ReactiveArray.prototype.sort = function() { + var dep, i, orgList, _i, _len, _ref; + + orgList = this._list.slice(); + Array.prototype.sort.apply(this._list, arguments); + _ref = this._listDeps; + for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { + dep = _ref[i]; + if (dep && orgList[i] !== this._list[i]) { + dep.changed(); + } + } + this._listValueDep.changed(); + return this; + }; + ReactiveArray.prototype.indexOf = function(searchElement, fromIndex) { var i, idx, _base, _i, _ref, _ref1; @@ -115,7 +142,7 @@ }; ReactiveArray.prototype.clone = function() { - return ReactiveArray.wrap(this._list); + return this.constructor.wrap(this._list); }; ReactiveArray.prototype.equals = function(obj) { @@ -177,11 +204,13 @@ ReactiveArray.prototype._indexSet = function(i, val) { var _ref; - this._list[i] = val; - if ((_ref = this._listDeps[i]) != null) { - _ref.changed(); + if (this._list[i] !== val) { + this._list[i] = val; + if ((_ref = this._listDeps[i]) != null) { + _ref.changed(); + } + this._listValueDep.changed(); } - this._listValueDep.changed(); return val; }; @@ -226,24 +255,6 @@ }; }); - _.each(['reverse', 'sort'], function(m) { - return ReactiveArray.prototype[m] = function() { - var dep, i, orgList, _i, _len, _ref; - - orgList = this._list.slice(); - Array.prototype[m].apply(this._list, arguments); - _ref = this._listDeps; - for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { - dep = _ref[i]; - if (dep && orgList[i] !== this._list[i]) { - dep.changed(); - } - } - this._listValueDep.changed(); - return this; - }; - }); - _.each(['concat', 'slice'], function(m) { return ReactiveArray.prototype[m] = function() { var rtn; @@ -251,7 +262,7 @@ rtn = Array.prototype[m].apply(this._list, arguments); this._listLengthDep.depend(); this._listValueDep.depend(); - return ReactiveArray.wrap(rtn); + return this.constructor.wrap(rtn); }; }); @@ -277,7 +288,7 @@ rtn = _[m].call(null, this._list, iteratorProxy, thisArg); this._listLengthDep.depend(); this._listValueDep.depend(); - return ReactiveArray.wrap(rtn); + return this.constructor.wrap(rtn); }; }); diff --git a/example/packages/reactive-extra/lib/reactive-list-test.js b/example/packages/reactive-extra/lib/reactive-list-test.js new file mode 100644 index 0000000..a155a38 --- /dev/null +++ b/example/packages/reactive-extra/lib/reactive-list-test.js @@ -0,0 +1,312 @@ +// Generated by CoffeeScript 1.6.2 +(function() { + Tinytest.add("ReactiveList - added/addedAt", function(test) { + var addedAtX, addedIdx, addedVal, addedX, callbacks, handle, invalidCall, invalidX, list; + + invalidX = 0; + invalidCall = function() { + return invalidX++; + }; + addedAtX = 0; + addedX = 0; + addedVal = 'init'; + addedIdx = 0; + callbacks = { + added: function(val) { + test.equal(val, addedVal); + return addedX++; + }, + addedAt: function(val, idx) { + test.equal(val, addedVal); + test.equal(idx, addedIdx); + return addedAtX++; + }, + changed: invalidCall, + changedAt: invalidCall, + removed: invalidCall, + removedAt: invalidCall, + movedTo: invalidCall + }; + list = new ReactiveList('init'); + handle = list.observe(callbacks); + test.equal(invalidX, 0); + test.equal(addedX, 1); + test.equal(addedAtX, 1); + addedVal = 'push'; + addedIdx = 1; + list.push(addedVal); + test.equal(invalidX, 0); + test.equal(addedX, 2); + test.equal(addedAtX, 2); + addedVal = 'unshift'; + addedIdx = 0; + list.unshift(addedVal); + test.equal(invalidX, 0); + test.equal(addedX, 3); + test.equal(addedAtX, 3); + addedVal = 'splice1'; + addedIdx = 1; + list.splice(1, 0, 'splice1'); + test.equal(invalidX, 0); + test.equal(addedX, 4); + test.equal(addedAtX, 4); + addedVal = 'splice2'; + addedIdx = 3; + list.splice(-1, 0, 'splice2'); + test.equal(invalidX, 0); + test.equal(addedX, 5); + return test.equal(addedAtX, 5); + }); + + Tinytest.add("ReactiveList - remove/removeAt", function(test) { + var callbacks, handle, invalidCall, invalidX, list, removedAtX, removedIdx, removedVal, removedX; + + invalidX = -14; + invalidCall = function() { + return invalidX++; + }; + removedAtX = 0; + removedX = 0; + removedVal = null; + removedIdx = 0; + callbacks = { + removed: function(val) { + test.equal(val, removedVal); + return removedX++; + }, + removedAt: function(val, idx) { + test.equal(val, removedVal); + test.equal(idx, removedIdx); + return removedAtX++; + }, + changed: invalidCall, + changedAt: invalidCall, + added: invalidCall, + addedAt: invalidCall, + movedTo: invalidCall + }; + list = new ReactiveList('shift', 'keep1', 'splice1', 'keep2', 'splice2', 'keep3', 'pop'); + handle = list.observe(callbacks); + test.equal(invalidX, 0, 'observer'); + test.equal(removedX, 0, 'observer'); + test.equal(removedAtX, 0, 'observer'); + removedVal = 'pop'; + removedIdx = 6; + list.pop(); + test.equal(invalidX, 0, 'pop'); + test.equal(removedX, 1, 'pop'); + test.equal(removedAtX, 1, 'pop'); + removedVal = 'shift'; + removedIdx = 0; + list.shift(); + test.equal(invalidX, 0); + test.equal(removedX, 2); + test.equal(removedAtX, 2); + removedVal = 'splice1'; + removedIdx = 1; + list.splice(1, 1); + test.equal(invalidX, 0); + test.equal(removedX, 3); + test.equal(removedAtX, 3); + removedVal = 'splice2'; + removedIdx = 2; + list.splice(-2, 1); + test.equal(invalidX, 0); + test.equal(removedX, 4); + return test.equal(removedAtX, 4); + }); + + Tinytest.add("ReactiveList - splice", function(test) { + var addedAtX, addedRun, addedX, callbacks, changedAtX, changedRun, changedX, list, movedRun, movedX, removedAtX, removedRun, removedX; + + addedX = addedAtX = -7; + changedX = changedAtX = 0; + removedX = removedAtX = 0; + movedX = 0; + changedRun = [ + { + val: '2', + newVal: 'replacement1', + idx: 1 + }, { + val: '4', + newVal: 'replacement2', + idx: 3 + }, { + val: '5', + newVal: 'replacement3', + idx: 4 + }, { + val: '3', + newVal: 'replacement4', + idx: 2 + }, { + val: 'replacement4', + newVal: 'replacement5', + idx: 2 + } + ]; + addedRun = [ + { + val: '3.5', + idx: 3 + } + ]; + removedRun = [ + { + val: '3.5', + idx: 3 + } + ]; + movedRun = []; + callbacks = { + added: function(val) { + var eq; + + if (addedX >= 0) { + eq = addedRun[addedX]; + test.equal(val, eq.val, 'added - val: ' + addedX); + } + return addedX++; + }, + addedAt: function(val, idx) { + var eq; + + if (addedAtX >= 0) { + eq = addedRun[addedAtX]; + test.equal(val, eq.val, 'addedAt - val: ' + addedAtX); + test.equal(idx, eq.idx, 'addedAt - idx: ' + addedAtX); + } + return addedAtX++; + }, + changed: function(val, oldVal) { + var eq; + + eq = changedRun[changedX]; + test.equal(val, eq.newVal, 'changed - val: ' + changedX); + test.equal(oldVal, eq.val, 'changed - oldVal: ' + changedX); + return changedX++; + }, + changedAt: function(val, oldVal, idx) { + var eq; + + eq = changedRun[changedAtX]; + test.equal(val, eq.newVal, 'changedAt - val: ' + changedAtX); + test.equal(oldVal, eq.val, 'changedAt - oldVal: ' + changedAtX); + test.equal(idx, eq.idx, 'changedAt - idx: ' + changedAtX); + return changedAtX++; + }, + removed: function(val) { + var eq; + + eq = removedRun[removedX]; + test.equal(val, eq.val, 'removed - val: ' + removedX); + return removedX++; + }, + removedAt: function(val, idx) { + var eq; + + eq = removedRun[removedAtX]; + test.equal(val, eq.val, 'removedAt - val: ' + removedAtX); + test.equal(idx, eq.idx, 'removedAt - idx: ' + removedAtX); + return removedAtX++; + }, + movedTo: function(val, idx) { + var eq; + + eq = movedRun[movedX]; + test.equal(val, eq.val, 'movedTo - val: ' + movedX); + test.equal(idx, eq.idx, 'movedTo - idx: ' + movedX); + return movedX++; + } + }; + list = new ReactiveList('1', '2', '3', '4', '5', '6', '7'); + list.observe(callbacks); + test.equal(addedX, 0, 'added: observe'); + test.equal(addedAtX, 0, 'addedAt: observe'); + test.equal(removedX, 0, 'removedAt: observe'); + test.equal(removedAtX, 0, 'removedAt: observe'); + test.equal(changedX, 0, 'changed: observe'); + test.equal(changedAtX, 0, 'changedAt: observe'); + test.equal(movedX, 0, 'moved: observe'); + list.splice(1, 1, changedRun[0].newVal); + test.equal(addedX, 0, 'added: run 1'); + test.equal(addedAtX, 0, 'addedAt: run 1'); + test.equal(removedX, 0, 'removedAt: run 1'); + test.equal(removedAtX, 0, 'removedAt: run 1'); + test.equal(changedX, 1, 'changed: run 1'); + test.equal(changedAtX, 1, 'changedAt: run 1'); + test.equal(movedX, 0, 'moved: run 1'); + list.splice(3, 2, changedRun[1].newVal, changedRun[2].newVal); + test.equal(addedX, 0, 'added: run 2'); + test.equal(addedAtX, 0, 'addedAt: run 2'); + test.equal(removedX, 0, 'removedAt: run 2'); + test.equal(removedAtX, 0, 'removedAt: run 2'); + test.equal(changedX, 3, 'changed: run 2'); + test.equal(changedAtX, 3, 'changedAt: run 2'); + test.equal(movedX, 0, 'moved: run 2'); + list.splice(2, 1, changedRun[3].newVal, addedRun[0].val); + test.equal(addedX, 1, 'added: run 3'); + test.equal(addedAtX, 1, 'addedAt: run 3'); + test.equal(removedX, 0, 'removedAt: run 3'); + test.equal(removedAtX, 0, 'removedAt: run 3'); + test.equal(changedX, 4, 'changed: run 3'); + test.equal(changedAtX, 4, 'changedAt: run 3'); + test.equal(movedX, 0, 'moved: run 3'); + list.splice(2, 2, changedRun[4].newVal); + test.equal(addedX, 1, 'added: run 4'); + test.equal(addedAtX, 1, 'addedAt: run 4'); + test.equal(removedX, 1, 'removedAt: run 4'); + test.equal(removedAtX, 1, 'removedAt: run 4'); + test.equal(changedX, 5, 'changed: run 4'); + test.equal(changedAtX, 5, 'changedAt: run 4'); + return test.equal(movedX, 0, 'moved: run 4'); + }); + + Tinytest.add("ReactiveList - reverse", function(test) { + var arr, arrReversed, list; + + arr = ['1', '2', '3', '4', '5', '6', '7']; + arrReversed = arr.slice().reverse(); + list = ReactiveList.wrap(arr); + list.observe({ + movedTo: function(val, fromIdx, toIdx) { + arr.splice(fromIdx, 1); + return arr.splice(toIdx, 0, val); + } + }); + test.equal(list.reverse(), ReactiveList.wrap(arrReversed)); + test.equal(arr, arrReversed); + list.pop(); + arr.pop(); + arrReversed.pop(); + arrReversed.reverse(); + test.equal(list.reverse(), ReactiveList.wrap(arrReversed)); + return test.equal(arr, arrReversed); + }); + + Tinytest.add("ReactiveList - sort", function(test) { + var arr, arrSorted, arrs, list, _i, _len, _results; + + arrs = [['1', '7', '3', '4', '2', '6', '8', '5'], ['1', '2', '3', '4', '5', '6', '7'].reverse(), ['1', '7', '3', '7', '4', '2', '6', '8', '5'], ['1', '7', '3', '7', '7', '4', '7', '2', '6', '8', '5'], ['d', 'a', 'c', 'b', 'z', 'y', 'y']]; + _results = []; + for (_i = 0, _len = arrs.length; _i < _len; _i++) { + arr = arrs[_i]; + arrSorted = arr.slice().sort(); + list = ReactiveList.wrap(arr); + list.observe({ + movedTo: function(docVal, fromIdx, toIdx) { + var val; + + val = (arr.splice(fromIdx, 1))[0]; + test.equal(val, docVal); + return arr.splice(toIdx, 0, val); + } + }); + test.equal(list.sort(), ReactiveList.wrap(arrSorted)); + _results.push(test.equal(arr, arrSorted)); + } + return _results; + }); + +}).call(this); diff --git a/example/packages/reactive-extra/lib/reactive-list.js b/example/packages/reactive-extra/lib/reactive-list.js new file mode 100644 index 0000000..097368b --- /dev/null +++ b/example/packages/reactive-extra/lib/reactive-list.js @@ -0,0 +1,276 @@ +// Generated by CoffeeScript 1.6.2 +(function() { + var LiveHandler, + __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; + + this.ReactiveList = (function(_super) { + __extends(ReactiveList, _super); + + function ReactiveList() { + this._definePrivateProperty('_handlers', []); + ReactiveList.__super__.constructor.apply(this, arguments); + } + + ReactiveList.prototype.observe = function(callbacks) { + var handle, i, _i, _ref; + + handle = new LiveHandler(callbacks); + this._handlers.push(handle); + for (i = _i = 0, _ref = this._list.length; _i < _ref; i = _i += 1) { + this._trigger('added', this._list[i], i); + } + return handle; + }; + + ReactiveList.prototype.pop = function() { + var rtn; + + rtn = ReactiveList.__super__.pop.apply(this, arguments); + this._trigger('removed', rtn, this._list.length); + return rtn; + }; + + ReactiveList.prototype.push = function() { + var i, orgLength, rtn, _i, _ref; + + orgLength = this._list.length; + rtn = ReactiveList.__super__.push.apply(this, arguments); + for (i = _i = orgLength, _ref = this._list.length; _i < _ref; i = _i += 1) { + this._trigger('added', this._list[i], i); + } + return rtn; + }; + + ReactiveList.prototype.shift = function() { + var rtn; + + rtn = ReactiveList.__super__.shift.apply(this, arguments); + this._trigger('removed', rtn, 0); + return rtn; + }; + + ReactiveList.prototype.unshift = function() { + var i, orgLength, rtn, _i, _ref; + + orgLength = this._list.length; + rtn = ReactiveList.__super__.unshift.apply(this, arguments); + for (i = _i = 0, _ref = this._list.length - orgLength; _i < _ref; i = _i += 1) { + this._trigger('added', this._list[i], i); + } + return rtn; + }; + + ReactiveList.prototype.splice = function() { + var addAmount, changedAmount, i, idx, orgList, rmAmount, rtn, _i, _j, _k, _l, _ref, _ref1; + + orgList = this._list.slice(); + rtn = ReactiveList.__super__.splice.apply(this, arguments); + idx = arguments[0]; + if (idx < 0) { + idx = orgList.length + idx; + } + rmAmount = arguments.length > 1 ? arguments[1] : orgList.length - idx; + if (arguments.length > 2) { + addAmount = arguments.length - 2; + if (rmAmount > 0) { + changedAmount = rmAmount > addAmount ? addAmount : rmAmount; + for (i = _i = 0; _i < changedAmount; i = _i += 1) { + this._trigger('changed', this._list[idx], orgList[idx], idx); + idx++; + } + addAmount = addAmount - changedAmount; + rmAmount = rmAmount - changedAmount; + } + if ((rmAmount - addAmount) > 0) { + for (i = _j = 0, _ref = rmAmount - addAmount; _j < _ref; i = _j += 1) { + this._trigger('removed', orgList[idx + i], idx + i); + } + } else if ((rmAmount - addAmount) < 0) { + for (i = _k = 0, _ref1 = addAmount - rmAmount; _k < _ref1; i = _k += 1) { + this._trigger('added', this._list[idx + i], idx + i); + } + } + } else if (rmAmount > 0) { + for (i = _l = 0; _l < rmAmount; i = _l += 1) { + this._trigger('removed', orgList[idx + i], idx + i); + } + } + return rtn; + }; + + ReactiveList.prototype.reverse = function() { + var array, length; + + ReactiveList.__super__.reverse.apply(this, arguments); + array = this._list; + length = this._list.length; + for (left = 0, right = length - 1; left < right; left += 1, right -= 1) { + if (right === left) { continue; } + this._trigger('movedTo', array[left], right, left); + this._trigger('movedTo', array[right], left+1, right); + }; + return this; + }; + + ReactiveList.prototype.sort = function() { + var currentPosition, finalPosition, lastMove, length, move, moves, org, skip, _i, _len; + + org = this._list.slice(); + ReactiveList.__super__.sort.apply(this, arguments); + if (!this._hasActiveTrigger('movedTo')) { + return this; + } + length = this._list.length; + moves = []; + currentPosition = 0; + while (currentPosition < length) { + finalPosition = this._list.indexOf(org[currentPosition]); + if (currentPosition + 1 === finalPosition) { + while (org[currentPosition + 1] === this._list[finalPosition + 1]) { + finalPosition++; + currentPosition++; + } + if (org[currentPosition] === this._list[finalPosition]) { + finalPosition++; + currentPosition++; + } + finalPosition = this._list.indexOf(org[currentPosition]); + } + if (org[currentPosition] === org[currentPosition + 1]) { + while (org[currentPosition - 1] === this._list[finalPosition]) { + finalPosition++; + } + if (org[currentPosition] === this._list[finalPosition]) { + finalPosition++; + } + finalPosition = this._list.indexOf(org[currentPosition], finalPosition); + } + move = { + from: currentPosition, + to: finalPosition + }; + skip = finalPosition === -1 || lastMove && lastMove.to === move.to && lastMove.from === move.from; + if (!skip && finalPosition !== currentPosition) { + moves.push(move); + lastMove = move; + org.splice(move.to, 0, (org.splice(move.from, 1))[0]); + if (finalPosition < currentPosition) { + currentPosition = finalPosition; + } else { + currentPosition--; + } + } + currentPosition++; + } + for (_i = 0, _len = moves.length; _i < _len; _i++) { + move = moves[_i]; + this._trigger('movedTo', this._list[move.to], move.from, move.to); + } + return this; + }; + + ReactiveList.prototype.typeName = function() { + return 'reactive-list'; + }; + + ReactiveList.prototype.equals = function(obj) { + return (obj != null) && obj instanceof ReactiveList && _.isEqual(obj._list, this._list); + }; + + ReactiveList.prototype._trigger = function(evt) { + var args, evtArgs, evtAt, evtAtArgs, handler, i, self, trigger, _ref; + + self = this; + args = _.toArray(arguments).slice(1); + if (evt === 'movedTo') { + trigger = function(callbacks) { + if (evt in callbacks) { + return callbacks[evt].apply(self, args); + } + }; + } else { + evtArgs = args.slice(0, -1); + evtAt = evt + 'At'; + evtAtArgs = args; + trigger = function(callbacks) { + if (evt in callbacks) { + callbacks[evt].apply(self, evtArgs); + } + if (evtAt in callbacks) { + return callbacks[evtAt].apply(self, evtAtArgs); + } + }; + } + _ref = this._handlers; + for (i in _ref) { + handler = _ref[i]; + if (!(i in this._handlers)) { + continue; + } + if (handler.stopped) { + delete this._handlers[i]; + continue; + } + trigger(handler.callbacks); + } + }; + + ReactiveList.prototype._hasActiveTrigger = function(evt) { + return _.any(this._handlers, function(handler) { + return !handler.stopped && evt in handler.callbacks; + }); + }; + + ReactiveList.prototype._indexSet = function(idx, val) { + var org, rtn; + + rtn = val; + if (this._list[idx] !== val) { + org = list[idx]; + rtn = ReactiveList.__super__._indexSet.apply(this, arguments); + this._trigger('changed', this._list[idx], org, idx); + } + return rtn; + }; + + return ReactiveList; + + })(ReactiveArray); + + ReactiveList.wrap = function(arr) { + var obj; + + obj = new ReactiveList; + obj._list = _.toArray(arr); + obj._syncIndexProxies(true); + return obj; + }; + + EJSON.addType('reactive-list', function(jsonObj) { + return ReactiveList.wrap(jsonObj); + }); + + LiveHandler = (function() { + function LiveHandler(callbacks) { + var self; + + self = this; + this.stopped = false; + this.callbacks = callbacks; + if (Deps.active) { + Deps.onInvalidate(function() { + return self.stop(); + }); + } + } + + LiveHandler.prototype.stop = function() { + return this.stopped = true; + }; + + return LiveHandler; + + })(); + +}).call(this); diff --git a/example/packages/reactive-extra/package.js b/example/packages/reactive-extra/package.js index c808342..6f1f32d 100644 --- a/example/packages/reactive-extra/package.js +++ b/example/packages/reactive-extra/package.js @@ -6,13 +6,20 @@ var path = Npm.require("path"); Package.on_use(function(api) { // Required packages api.use(["deps", "ejson", "underscore"], ["client", "server"]); + api.use(["templating"], ["client"]); // Server and client side code api.add_files([ path.join("lib","reactive-object.js"), path.join("lib","reactive-dictionary.js"), - path.join("lib","reactive-array.js") + path.join("lib","reactive-array.js"), + path.join("lib","reactive-list.js") ], ["client", "server"]); + + // Client side code + api.add_files([ + path.join("lib","handlebars-list.js") + ], ["client"]); }); Package.on_test(function(api) { @@ -23,6 +30,7 @@ Package.on_test(function(api) { api.add_files([ path.join("lib","reactive-dictionary-test.js"), path.join("lib","reactive-object-test.js"), - path.join("lib","reactive-array-test.js") + path.join("lib","reactive-array-test.js"), + path.join("lib","reactive-list-test.js") ], ["client", "server"]); }); diff --git a/lib/handlebars-list.js b/lib/handlebars-list.js new file mode 100644 index 0000000..fd62e42 --- /dev/null +++ b/lib/handlebars-list.js @@ -0,0 +1,185 @@ +// Generated by CoffeeScript 1.6.2 +(function() { + var HandlebarsEach, SparkListObserve, findParentOfType, makeRange; + + HandlebarsEach = Handlebars._default_helpers.each; + + Handlebars._default_helpers.each = function(arg, options) { + var elseFunc, itemFunc; + + if (!(arg && arg instanceof ReactiveList)) { + return HandlebarsEach.call(this, arg, options); + } + itemFunc = function(item) { + return Spark.labelBranch((item && item._id) || Spark.UNIQUE_LABEL, function() { + return Spark.setDataContext(item, Spark.isolate(_.bind(options.fn, null, item))); + }); + }; + elseFunc = function() { + if (options.inverse) { + return Spark.isolate(options.inverse); + } else { + return ''; + } + }; + return SparkListObserve(arg, itemFunc, elseFunc); + }; + + Spark._ANNOTATION_LIST_OBSERVE = "list_observe"; + + Spark._ANNOTATION_LIST_OBSERVE_ITEM = "list_observe_item"; + + SparkListObserve = function(observable, itemFunc, elseFunc) { + var callbacks, cleanup, handle, html, itemArr, later, maybeAnnotate, notifyParentsRendered, observerCallbacks, outerRange, renderer, stopped; + + elseFunc = elseFunc || function() { + return ''; + }; + callbacks = {}; + observerCallbacks = {}; + _.each(["addedAt", "changedAt", "removedAt", "movedTo"], function(name) { + return observerCallbacks[name] = function() { + return callbacks[name].apply(null, arguments); + }; + }); + itemArr = []; + _.extend(callbacks, { + addedAt: function(val, idx) { + return itemArr[idx] = { + liveRange: null, + value: val + }; + } + }); + handle = observable.observe(observerCallbacks); + renderer = Spark._currentRenderer.get(); + maybeAnnotate = renderer ? _.bind(renderer.annotate, renderer) : function(html) { + return html; + }; + html = ''; + outerRange = null; + if (itemArr.length === 0) { + html = elseFunc(); + } else { + _.each(itemArr, function(elt) { + return html += maybeAnnotate(itemFunc(elt.value), Spark._ANNOTATION_LIST_OBSERVE_ITEM, function(range) { + elt.liveRange = range; + }); + }); + } + stopped = false; + cleanup = function() { + handle.stop(); + return stopped = true; + }; + html = maybeAnnotate(html, Spark._ANNOTATION_LIST_OBSERVE, function(range) { + if (!range) { + cleanup(); + return; + } + outerRange = range; + outerRange.finalize = cleanup; + }); + if (!renderer) { + cleanup(); + return html; + } + notifyParentsRendered = function() { + var walk; + + walk = outerRange; + while ((walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk))) { + walk.rendered.call(walk.landmark); + } + }; + later = function(func) { + Deps.afterFlush(function() { + if (!stopped) { + func(); + } + }); + }; + _.extend(callbacks, { + addedAt: function(val, idx) { + return later(function() { + var frag, range; + + frag = Spark.render(_.bind(itemFunc, null, val)); + DomUtils.wrapFragmentForContainer(frag, outerRange.containerNode()); + range = makeRange(Spark._ANNOTATION_LIST_ITEM, frag); + if (itemArr.length === 0) { + Spark.finalize(outerRange.replaceContents(frag)); + } else { + itemArr[idx - 1].liveRange.insertAfter(frag); + } + return itemArr[idx] = { + liveRange: range, + value: val + }; + }); + }, + removedAt: function(val, idx) { + return later(function() { + var frag; + + if (itemArr.length === 1) { + frag = Spark.render(elseFunc); + DomUtils.wrapFragmentForContainer(frag, outerRange.containerNode()); + Spark.finalize(outerRange.replaceContents(frag)); + } else { + Spark.finalize(itemArr[idx].liveRange.extract()); + } + itemArr.splice(idx, 1); + return notifyParentsRendered(); + }); + }, + movedTo: function(val, fromIdx, toIdx) { + return later(function() { + var elt, frag; + + elt = (itemArr.splice(fromIdx, 1))[0]; + frag = elt.liveRange.extract(); + if (toIdx in itemArr) { + itemArr[toIdx].liveRange.insertBefore(frag); + } else { + itemArr[toIdx - 1].liveRange.insertAfter(frag); + } + itemArr.splice(toIdx, 0, elt); + return notifyParentsRendered(); + }); + }, + changedAt: function(val, idx) { + return later(function() { + var elt; + + elt = itemArr[idx]; + if (!elt) { + throw new Error("Unknown item at index: " + idx); + } + elt.value = val; + return Spark.renderToRange(elt.liveRange, _.bind(itemFunc, null, elt.value)); + }); + } + }); + return html; + }; + + findParentOfType = function(type, range) { + while (true) { + range = range.findParent(); + if (!(range && range.type !== type)) { + break; + } + } + return range; + }; + + makeRange = function(type, start, end, inner) { + var range; + + range = new LiveRange(Spark._TAG, start, end, inner); + range.type = type; + return range; + }; + +}).call(this); diff --git a/lib/reactive-array-test.js b/lib/reactive-array-test.js index 4b88eac..5142f2c 100644 --- a/lib/reactive-array-test.js +++ b/lib/reactive-array-test.js @@ -107,7 +107,15 @@ test.equal(bracketX, 3); test.equal(lengthX, 5); test.equal(indexOfX, 5, 'sort changes all (not really but i\'m lazy)'); - return test.equal(lastIndexOfX, 6, 'sort changes all (not really but i\'m lazy)'); + test.equal(lastIndexOfX, 6, 'sort changes all (not really but i\'m lazy)'); + arr.unshift('drink'); + Deps.flush(); + test.equal(lengthX, 6); + test.equal(bracketX, 4); + test.equal(arr.shift(), 'drink'); + Deps.flush(); + test.equal(lengthX, 7); + return test.equal(bracketX, 5); }); Tinytest.add("ReactiveArray - Sort/Reverse", function(test) { diff --git a/lib/reactive-array.js b/lib/reactive-array.js index ed54788..7603587 100644 --- a/lib/reactive-array.js +++ b/lib/reactive-array.js @@ -35,6 +35,33 @@ return this._list.slice(); }; + ReactiveArray.prototype.reverse = function() { + Array.prototype.reverse.apply(this._list); + for (left = 0, right = this._list.length - 1; left < right; left += 1, right -= 1) { + if (left === right) { continue; } + if (this._listDeps[left]) { this._listDeps[left].changed(); } + if (this._listDeps[right]) { this._listDeps[right].changed(); } + }; + this._listValueDep.changed(); + return this; + }; + + ReactiveArray.prototype.sort = function() { + var dep, i, orgList, _i, _len, _ref; + + orgList = this._list.slice(); + Array.prototype.sort.apply(this._list, arguments); + _ref = this._listDeps; + for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { + dep = _ref[i]; + if (dep && orgList[i] !== this._list[i]) { + dep.changed(); + } + } + this._listValueDep.changed(); + return this; + }; + ReactiveArray.prototype.indexOf = function(searchElement, fromIndex) { var i, idx, _base, _i, _ref, _ref1; @@ -115,7 +142,7 @@ }; ReactiveArray.prototype.clone = function() { - return ReactiveArray.wrap(this._list); + return this.constructor.wrap(this._list); }; ReactiveArray.prototype.equals = function(obj) { @@ -177,11 +204,13 @@ ReactiveArray.prototype._indexSet = function(i, val) { var _ref; - this._list[i] = val; - if ((_ref = this._listDeps[i]) != null) { - _ref.changed(); + if (this._list[i] !== val) { + this._list[i] = val; + if ((_ref = this._listDeps[i]) != null) { + _ref.changed(); + } + this._listValueDep.changed(); } - this._listValueDep.changed(); return val; }; @@ -198,7 +227,7 @@ })(); - _.each(['pop', 'push', 'shift', 'splice', 'unshift'], function(m) { + _.each(['pop', 'push'], function(m) { return ReactiveArray.prototype[m] = function() { var rtn; @@ -208,12 +237,13 @@ }; }); - _.each(['reverse', 'sort'], function(m) { + _.each(['shift', 'splice', 'unshift'], function(m) { return ReactiveArray.prototype[m] = function() { - var dep, i, orgList, _i, _len, _ref; + var dep, i, orgList, rtn, _i, _len, _ref; orgList = this._list.slice(); - Array.prototype[m].apply(this._list, arguments); + rtn = Array.prototype[m].apply(this._list, arguments); + this._syncIndexProxies(); _ref = this._listDeps; for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { dep = _ref[i]; @@ -221,8 +251,7 @@ dep.changed(); } } - this._listValueDep.changed(); - return this; + return rtn; }; }); @@ -233,7 +262,7 @@ rtn = Array.prototype[m].apply(this._list, arguments); this._listLengthDep.depend(); this._listValueDep.depend(); - return ReactiveArray.wrap(rtn); + return this.constructor.wrap(rtn); }; }); @@ -259,7 +288,7 @@ rtn = _[m].call(null, this._list, iteratorProxy, thisArg); this._listLengthDep.depend(); this._listValueDep.depend(); - return ReactiveArray.wrap(rtn); + return this.constructor.wrap(rtn); }; }); diff --git a/lib/reactive-list-test.js b/lib/reactive-list-test.js new file mode 100644 index 0000000..a155a38 --- /dev/null +++ b/lib/reactive-list-test.js @@ -0,0 +1,312 @@ +// Generated by CoffeeScript 1.6.2 +(function() { + Tinytest.add("ReactiveList - added/addedAt", function(test) { + var addedAtX, addedIdx, addedVal, addedX, callbacks, handle, invalidCall, invalidX, list; + + invalidX = 0; + invalidCall = function() { + return invalidX++; + }; + addedAtX = 0; + addedX = 0; + addedVal = 'init'; + addedIdx = 0; + callbacks = { + added: function(val) { + test.equal(val, addedVal); + return addedX++; + }, + addedAt: function(val, idx) { + test.equal(val, addedVal); + test.equal(idx, addedIdx); + return addedAtX++; + }, + changed: invalidCall, + changedAt: invalidCall, + removed: invalidCall, + removedAt: invalidCall, + movedTo: invalidCall + }; + list = new ReactiveList('init'); + handle = list.observe(callbacks); + test.equal(invalidX, 0); + test.equal(addedX, 1); + test.equal(addedAtX, 1); + addedVal = 'push'; + addedIdx = 1; + list.push(addedVal); + test.equal(invalidX, 0); + test.equal(addedX, 2); + test.equal(addedAtX, 2); + addedVal = 'unshift'; + addedIdx = 0; + list.unshift(addedVal); + test.equal(invalidX, 0); + test.equal(addedX, 3); + test.equal(addedAtX, 3); + addedVal = 'splice1'; + addedIdx = 1; + list.splice(1, 0, 'splice1'); + test.equal(invalidX, 0); + test.equal(addedX, 4); + test.equal(addedAtX, 4); + addedVal = 'splice2'; + addedIdx = 3; + list.splice(-1, 0, 'splice2'); + test.equal(invalidX, 0); + test.equal(addedX, 5); + return test.equal(addedAtX, 5); + }); + + Tinytest.add("ReactiveList - remove/removeAt", function(test) { + var callbacks, handle, invalidCall, invalidX, list, removedAtX, removedIdx, removedVal, removedX; + + invalidX = -14; + invalidCall = function() { + return invalidX++; + }; + removedAtX = 0; + removedX = 0; + removedVal = null; + removedIdx = 0; + callbacks = { + removed: function(val) { + test.equal(val, removedVal); + return removedX++; + }, + removedAt: function(val, idx) { + test.equal(val, removedVal); + test.equal(idx, removedIdx); + return removedAtX++; + }, + changed: invalidCall, + changedAt: invalidCall, + added: invalidCall, + addedAt: invalidCall, + movedTo: invalidCall + }; + list = new ReactiveList('shift', 'keep1', 'splice1', 'keep2', 'splice2', 'keep3', 'pop'); + handle = list.observe(callbacks); + test.equal(invalidX, 0, 'observer'); + test.equal(removedX, 0, 'observer'); + test.equal(removedAtX, 0, 'observer'); + removedVal = 'pop'; + removedIdx = 6; + list.pop(); + test.equal(invalidX, 0, 'pop'); + test.equal(removedX, 1, 'pop'); + test.equal(removedAtX, 1, 'pop'); + removedVal = 'shift'; + removedIdx = 0; + list.shift(); + test.equal(invalidX, 0); + test.equal(removedX, 2); + test.equal(removedAtX, 2); + removedVal = 'splice1'; + removedIdx = 1; + list.splice(1, 1); + test.equal(invalidX, 0); + test.equal(removedX, 3); + test.equal(removedAtX, 3); + removedVal = 'splice2'; + removedIdx = 2; + list.splice(-2, 1); + test.equal(invalidX, 0); + test.equal(removedX, 4); + return test.equal(removedAtX, 4); + }); + + Tinytest.add("ReactiveList - splice", function(test) { + var addedAtX, addedRun, addedX, callbacks, changedAtX, changedRun, changedX, list, movedRun, movedX, removedAtX, removedRun, removedX; + + addedX = addedAtX = -7; + changedX = changedAtX = 0; + removedX = removedAtX = 0; + movedX = 0; + changedRun = [ + { + val: '2', + newVal: 'replacement1', + idx: 1 + }, { + val: '4', + newVal: 'replacement2', + idx: 3 + }, { + val: '5', + newVal: 'replacement3', + idx: 4 + }, { + val: '3', + newVal: 'replacement4', + idx: 2 + }, { + val: 'replacement4', + newVal: 'replacement5', + idx: 2 + } + ]; + addedRun = [ + { + val: '3.5', + idx: 3 + } + ]; + removedRun = [ + { + val: '3.5', + idx: 3 + } + ]; + movedRun = []; + callbacks = { + added: function(val) { + var eq; + + if (addedX >= 0) { + eq = addedRun[addedX]; + test.equal(val, eq.val, 'added - val: ' + addedX); + } + return addedX++; + }, + addedAt: function(val, idx) { + var eq; + + if (addedAtX >= 0) { + eq = addedRun[addedAtX]; + test.equal(val, eq.val, 'addedAt - val: ' + addedAtX); + test.equal(idx, eq.idx, 'addedAt - idx: ' + addedAtX); + } + return addedAtX++; + }, + changed: function(val, oldVal) { + var eq; + + eq = changedRun[changedX]; + test.equal(val, eq.newVal, 'changed - val: ' + changedX); + test.equal(oldVal, eq.val, 'changed - oldVal: ' + changedX); + return changedX++; + }, + changedAt: function(val, oldVal, idx) { + var eq; + + eq = changedRun[changedAtX]; + test.equal(val, eq.newVal, 'changedAt - val: ' + changedAtX); + test.equal(oldVal, eq.val, 'changedAt - oldVal: ' + changedAtX); + test.equal(idx, eq.idx, 'changedAt - idx: ' + changedAtX); + return changedAtX++; + }, + removed: function(val) { + var eq; + + eq = removedRun[removedX]; + test.equal(val, eq.val, 'removed - val: ' + removedX); + return removedX++; + }, + removedAt: function(val, idx) { + var eq; + + eq = removedRun[removedAtX]; + test.equal(val, eq.val, 'removedAt - val: ' + removedAtX); + test.equal(idx, eq.idx, 'removedAt - idx: ' + removedAtX); + return removedAtX++; + }, + movedTo: function(val, idx) { + var eq; + + eq = movedRun[movedX]; + test.equal(val, eq.val, 'movedTo - val: ' + movedX); + test.equal(idx, eq.idx, 'movedTo - idx: ' + movedX); + return movedX++; + } + }; + list = new ReactiveList('1', '2', '3', '4', '5', '6', '7'); + list.observe(callbacks); + test.equal(addedX, 0, 'added: observe'); + test.equal(addedAtX, 0, 'addedAt: observe'); + test.equal(removedX, 0, 'removedAt: observe'); + test.equal(removedAtX, 0, 'removedAt: observe'); + test.equal(changedX, 0, 'changed: observe'); + test.equal(changedAtX, 0, 'changedAt: observe'); + test.equal(movedX, 0, 'moved: observe'); + list.splice(1, 1, changedRun[0].newVal); + test.equal(addedX, 0, 'added: run 1'); + test.equal(addedAtX, 0, 'addedAt: run 1'); + test.equal(removedX, 0, 'removedAt: run 1'); + test.equal(removedAtX, 0, 'removedAt: run 1'); + test.equal(changedX, 1, 'changed: run 1'); + test.equal(changedAtX, 1, 'changedAt: run 1'); + test.equal(movedX, 0, 'moved: run 1'); + list.splice(3, 2, changedRun[1].newVal, changedRun[2].newVal); + test.equal(addedX, 0, 'added: run 2'); + test.equal(addedAtX, 0, 'addedAt: run 2'); + test.equal(removedX, 0, 'removedAt: run 2'); + test.equal(removedAtX, 0, 'removedAt: run 2'); + test.equal(changedX, 3, 'changed: run 2'); + test.equal(changedAtX, 3, 'changedAt: run 2'); + test.equal(movedX, 0, 'moved: run 2'); + list.splice(2, 1, changedRun[3].newVal, addedRun[0].val); + test.equal(addedX, 1, 'added: run 3'); + test.equal(addedAtX, 1, 'addedAt: run 3'); + test.equal(removedX, 0, 'removedAt: run 3'); + test.equal(removedAtX, 0, 'removedAt: run 3'); + test.equal(changedX, 4, 'changed: run 3'); + test.equal(changedAtX, 4, 'changedAt: run 3'); + test.equal(movedX, 0, 'moved: run 3'); + list.splice(2, 2, changedRun[4].newVal); + test.equal(addedX, 1, 'added: run 4'); + test.equal(addedAtX, 1, 'addedAt: run 4'); + test.equal(removedX, 1, 'removedAt: run 4'); + test.equal(removedAtX, 1, 'removedAt: run 4'); + test.equal(changedX, 5, 'changed: run 4'); + test.equal(changedAtX, 5, 'changedAt: run 4'); + return test.equal(movedX, 0, 'moved: run 4'); + }); + + Tinytest.add("ReactiveList - reverse", function(test) { + var arr, arrReversed, list; + + arr = ['1', '2', '3', '4', '5', '6', '7']; + arrReversed = arr.slice().reverse(); + list = ReactiveList.wrap(arr); + list.observe({ + movedTo: function(val, fromIdx, toIdx) { + arr.splice(fromIdx, 1); + return arr.splice(toIdx, 0, val); + } + }); + test.equal(list.reverse(), ReactiveList.wrap(arrReversed)); + test.equal(arr, arrReversed); + list.pop(); + arr.pop(); + arrReversed.pop(); + arrReversed.reverse(); + test.equal(list.reverse(), ReactiveList.wrap(arrReversed)); + return test.equal(arr, arrReversed); + }); + + Tinytest.add("ReactiveList - sort", function(test) { + var arr, arrSorted, arrs, list, _i, _len, _results; + + arrs = [['1', '7', '3', '4', '2', '6', '8', '5'], ['1', '2', '3', '4', '5', '6', '7'].reverse(), ['1', '7', '3', '7', '4', '2', '6', '8', '5'], ['1', '7', '3', '7', '7', '4', '7', '2', '6', '8', '5'], ['d', 'a', 'c', 'b', 'z', 'y', 'y']]; + _results = []; + for (_i = 0, _len = arrs.length; _i < _len; _i++) { + arr = arrs[_i]; + arrSorted = arr.slice().sort(); + list = ReactiveList.wrap(arr); + list.observe({ + movedTo: function(docVal, fromIdx, toIdx) { + var val; + + val = (arr.splice(fromIdx, 1))[0]; + test.equal(val, docVal); + return arr.splice(toIdx, 0, val); + } + }); + test.equal(list.sort(), ReactiveList.wrap(arrSorted)); + _results.push(test.equal(arr, arrSorted)); + } + return _results; + }); + +}).call(this); diff --git a/lib/reactive-list.js b/lib/reactive-list.js new file mode 100644 index 0000000..097368b --- /dev/null +++ b/lib/reactive-list.js @@ -0,0 +1,276 @@ +// Generated by CoffeeScript 1.6.2 +(function() { + var LiveHandler, + __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; + + this.ReactiveList = (function(_super) { + __extends(ReactiveList, _super); + + function ReactiveList() { + this._definePrivateProperty('_handlers', []); + ReactiveList.__super__.constructor.apply(this, arguments); + } + + ReactiveList.prototype.observe = function(callbacks) { + var handle, i, _i, _ref; + + handle = new LiveHandler(callbacks); + this._handlers.push(handle); + for (i = _i = 0, _ref = this._list.length; _i < _ref; i = _i += 1) { + this._trigger('added', this._list[i], i); + } + return handle; + }; + + ReactiveList.prototype.pop = function() { + var rtn; + + rtn = ReactiveList.__super__.pop.apply(this, arguments); + this._trigger('removed', rtn, this._list.length); + return rtn; + }; + + ReactiveList.prototype.push = function() { + var i, orgLength, rtn, _i, _ref; + + orgLength = this._list.length; + rtn = ReactiveList.__super__.push.apply(this, arguments); + for (i = _i = orgLength, _ref = this._list.length; _i < _ref; i = _i += 1) { + this._trigger('added', this._list[i], i); + } + return rtn; + }; + + ReactiveList.prototype.shift = function() { + var rtn; + + rtn = ReactiveList.__super__.shift.apply(this, arguments); + this._trigger('removed', rtn, 0); + return rtn; + }; + + ReactiveList.prototype.unshift = function() { + var i, orgLength, rtn, _i, _ref; + + orgLength = this._list.length; + rtn = ReactiveList.__super__.unshift.apply(this, arguments); + for (i = _i = 0, _ref = this._list.length - orgLength; _i < _ref; i = _i += 1) { + this._trigger('added', this._list[i], i); + } + return rtn; + }; + + ReactiveList.prototype.splice = function() { + var addAmount, changedAmount, i, idx, orgList, rmAmount, rtn, _i, _j, _k, _l, _ref, _ref1; + + orgList = this._list.slice(); + rtn = ReactiveList.__super__.splice.apply(this, arguments); + idx = arguments[0]; + if (idx < 0) { + idx = orgList.length + idx; + } + rmAmount = arguments.length > 1 ? arguments[1] : orgList.length - idx; + if (arguments.length > 2) { + addAmount = arguments.length - 2; + if (rmAmount > 0) { + changedAmount = rmAmount > addAmount ? addAmount : rmAmount; + for (i = _i = 0; _i < changedAmount; i = _i += 1) { + this._trigger('changed', this._list[idx], orgList[idx], idx); + idx++; + } + addAmount = addAmount - changedAmount; + rmAmount = rmAmount - changedAmount; + } + if ((rmAmount - addAmount) > 0) { + for (i = _j = 0, _ref = rmAmount - addAmount; _j < _ref; i = _j += 1) { + this._trigger('removed', orgList[idx + i], idx + i); + } + } else if ((rmAmount - addAmount) < 0) { + for (i = _k = 0, _ref1 = addAmount - rmAmount; _k < _ref1; i = _k += 1) { + this._trigger('added', this._list[idx + i], idx + i); + } + } + } else if (rmAmount > 0) { + for (i = _l = 0; _l < rmAmount; i = _l += 1) { + this._trigger('removed', orgList[idx + i], idx + i); + } + } + return rtn; + }; + + ReactiveList.prototype.reverse = function() { + var array, length; + + ReactiveList.__super__.reverse.apply(this, arguments); + array = this._list; + length = this._list.length; + for (left = 0, right = length - 1; left < right; left += 1, right -= 1) { + if (right === left) { continue; } + this._trigger('movedTo', array[left], right, left); + this._trigger('movedTo', array[right], left+1, right); + }; + return this; + }; + + ReactiveList.prototype.sort = function() { + var currentPosition, finalPosition, lastMove, length, move, moves, org, skip, _i, _len; + + org = this._list.slice(); + ReactiveList.__super__.sort.apply(this, arguments); + if (!this._hasActiveTrigger('movedTo')) { + return this; + } + length = this._list.length; + moves = []; + currentPosition = 0; + while (currentPosition < length) { + finalPosition = this._list.indexOf(org[currentPosition]); + if (currentPosition + 1 === finalPosition) { + while (org[currentPosition + 1] === this._list[finalPosition + 1]) { + finalPosition++; + currentPosition++; + } + if (org[currentPosition] === this._list[finalPosition]) { + finalPosition++; + currentPosition++; + } + finalPosition = this._list.indexOf(org[currentPosition]); + } + if (org[currentPosition] === org[currentPosition + 1]) { + while (org[currentPosition - 1] === this._list[finalPosition]) { + finalPosition++; + } + if (org[currentPosition] === this._list[finalPosition]) { + finalPosition++; + } + finalPosition = this._list.indexOf(org[currentPosition], finalPosition); + } + move = { + from: currentPosition, + to: finalPosition + }; + skip = finalPosition === -1 || lastMove && lastMove.to === move.to && lastMove.from === move.from; + if (!skip && finalPosition !== currentPosition) { + moves.push(move); + lastMove = move; + org.splice(move.to, 0, (org.splice(move.from, 1))[0]); + if (finalPosition < currentPosition) { + currentPosition = finalPosition; + } else { + currentPosition--; + } + } + currentPosition++; + } + for (_i = 0, _len = moves.length; _i < _len; _i++) { + move = moves[_i]; + this._trigger('movedTo', this._list[move.to], move.from, move.to); + } + return this; + }; + + ReactiveList.prototype.typeName = function() { + return 'reactive-list'; + }; + + ReactiveList.prototype.equals = function(obj) { + return (obj != null) && obj instanceof ReactiveList && _.isEqual(obj._list, this._list); + }; + + ReactiveList.prototype._trigger = function(evt) { + var args, evtArgs, evtAt, evtAtArgs, handler, i, self, trigger, _ref; + + self = this; + args = _.toArray(arguments).slice(1); + if (evt === 'movedTo') { + trigger = function(callbacks) { + if (evt in callbacks) { + return callbacks[evt].apply(self, args); + } + }; + } else { + evtArgs = args.slice(0, -1); + evtAt = evt + 'At'; + evtAtArgs = args; + trigger = function(callbacks) { + if (evt in callbacks) { + callbacks[evt].apply(self, evtArgs); + } + if (evtAt in callbacks) { + return callbacks[evtAt].apply(self, evtAtArgs); + } + }; + } + _ref = this._handlers; + for (i in _ref) { + handler = _ref[i]; + if (!(i in this._handlers)) { + continue; + } + if (handler.stopped) { + delete this._handlers[i]; + continue; + } + trigger(handler.callbacks); + } + }; + + ReactiveList.prototype._hasActiveTrigger = function(evt) { + return _.any(this._handlers, function(handler) { + return !handler.stopped && evt in handler.callbacks; + }); + }; + + ReactiveList.prototype._indexSet = function(idx, val) { + var org, rtn; + + rtn = val; + if (this._list[idx] !== val) { + org = list[idx]; + rtn = ReactiveList.__super__._indexSet.apply(this, arguments); + this._trigger('changed', this._list[idx], org, idx); + } + return rtn; + }; + + return ReactiveList; + + })(ReactiveArray); + + ReactiveList.wrap = function(arr) { + var obj; + + obj = new ReactiveList; + obj._list = _.toArray(arr); + obj._syncIndexProxies(true); + return obj; + }; + + EJSON.addType('reactive-list', function(jsonObj) { + return ReactiveList.wrap(jsonObj); + }); + + LiveHandler = (function() { + function LiveHandler(callbacks) { + var self; + + self = this; + this.stopped = false; + this.callbacks = callbacks; + if (Deps.active) { + Deps.onInvalidate(function() { + return self.stop(); + }); + } + } + + LiveHandler.prototype.stop = function() { + return this.stopped = true; + }; + + return LiveHandler; + + })(); + +}).call(this); diff --git a/package.js b/package.js index c808342..6f1f32d 100644 --- a/package.js +++ b/package.js @@ -6,13 +6,20 @@ var path = Npm.require("path"); Package.on_use(function(api) { // Required packages api.use(["deps", "ejson", "underscore"], ["client", "server"]); + api.use(["templating"], ["client"]); // Server and client side code api.add_files([ path.join("lib","reactive-object.js"), path.join("lib","reactive-dictionary.js"), - path.join("lib","reactive-array.js") + path.join("lib","reactive-array.js"), + path.join("lib","reactive-list.js") ], ["client", "server"]); + + // Client side code + api.add_files([ + path.join("lib","handlebars-list.js") + ], ["client"]); }); Package.on_test(function(api) { @@ -23,6 +30,7 @@ Package.on_test(function(api) { api.add_files([ path.join("lib","reactive-dictionary-test.js"), path.join("lib","reactive-object-test.js"), - path.join("lib","reactive-array-test.js") + path.join("lib","reactive-array-test.js"), + path.join("lib","reactive-list-test.js") ], ["client", "server"]); }); diff --git a/smart.json b/smart.json index 8212d5b..c3ec1b8 100644 --- a/smart.json +++ b/smart.json @@ -3,7 +3,7 @@ "description": "Providing reactive classes", "homepage": "https://github.com/boekkooi/reactive-extra", "author": "Warnar Boekkooi ", - "version": "0.0.2", + "version": "0.0.3", "git": "https://github.com/boekkooi/reactive-extra.git", "packages": {} } \ No newline at end of file diff --git a/src/handlebars-list.coffee b/src/handlebars-list.coffee new file mode 100644 index 0000000..4dd52bc --- /dev/null +++ b/src/handlebars-list.coffee @@ -0,0 +1,143 @@ +# # Handlebars each override +HandlebarsEach = Handlebars._default_helpers.each +Handlebars._default_helpers.each = (arg, options) -> + # Only use our implementation when the arg is a ReactiveList + return HandlebarsEach.call(this, arg, options) unless arg and arg instanceof ReactiveList + + # Item & else functions (stolen from [templating/deftemplate.js](https://github.com/meteor/meteor/blob/master/packages/templating/deftemplate.js) + itemFunc = (item) -> + Spark.labelBranch (item && item._id) || Spark.UNIQUE_LABEL, () -> + Spark.setDataContext item, Spark.isolate(_.bind(options.fn, null, item)) + elseFunc = () -> if options.inverse then Spark.isolate(options.inverse) else '' + + # Call our curstom observe based SparkArrayList + SparkListObserve arg, itemFunc, elseFunc + +# # Spark listObserve +Spark._ANNOTATION_LIST_OBSERVE = "list_observe"; +Spark._ANNOTATION_LIST_OBSERVE_ITEM = "list_observe_item"; + +# Render a object with a observe function using Spark +# *Most of this code is ripped from [Spark.list](https://github.com/meteor/meteor/blob/master/packages/spark/spark.js#L899)* +SparkListObserve = (observable, itemFunc, elseFunc) -> + elseFunc = elseFunc || () -> return '' + + # Create a level of indirection around our observable callbacks so we can change them later + callbacks = {} + observerCallbacks = {} + _.each ["addedAt", "changedAt", "removedAt", "movedTo"], (name) -> + observerCallbacks[name] = -> + callbacks[name].apply null, arguments + + # Create liverange stubs for the current contents of the observable + itemArr = [] + _.extend callbacks, + addedAt: (val, idx) -> + itemArr[idx] = { liveRange: null, value: val } + + handle = observable.observe observerCallbacks + + # Get the renderer, if any + renderer = Spark._currentRenderer.get(); + maybeAnnotate = if renderer then _.bind(renderer.annotate, renderer) else (html) -> html + + # Render the initial contents. + # If we have a renderer, create a range around each item as well as around the list, and save them off for later. + html = '' + outerRange = null + if itemArr.length == 0 + html = elseFunc() + else + _.each itemArr, (elt) -> + html += maybeAnnotate itemFunc(elt.value), Spark._ANNOTATION_LIST_OBSERVE_ITEM, (range) -> + elt.liveRange = range + return + + stopped = false + cleanup = () -> + handle.stop() + stopped = true + + html = maybeAnnotate html, Spark._ANNOTATION_LIST_OBSERVE, (range) -> + if !range + cleanup() + return + outerRange = range + outerRange.finalize = cleanup + return + + # No renderer? Then we have no way to update the returned html and we can close the observer. + if !renderer + cleanup() + return html + + notifyParentsRendered = -> + walk = outerRange + walk.rendered.call walk.landmark while (walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk)) + return + + later = (func) -> + Deps.afterFlush () -> + # Spark uses withEventGuard let's just have this won't brake + func() unless stopped + return + return + + # The DOM update callbacks. + _.extend callbacks, + addedAt: (val, idx) -> + later -> + frag = Spark.render(_.bind(itemFunc, null, val)) + DomUtils.wrapFragmentForContainer frag, outerRange.containerNode() + range = makeRange(Spark._ANNOTATION_LIST_ITEM, frag) + if itemArr.length == 0 + Spark.finalize outerRange.replaceContents(frag) + else + itemArr[idx-1].liveRange.insertAfter frag + itemArr[idx] = { liveRange: range, value: val } + + removedAt: (val, idx) -> + later -> + if itemArr.length == 1 + frag = Spark.render(elseFunc) + DomUtils.wrapFragmentForContainer frag, outerRange.containerNode() + Spark.finalize outerRange.replaceContents(frag) + else + Spark.finalize itemArr[idx].liveRange.extract() + itemArr.splice idx, 1 + notifyParentsRendered() + + movedTo: (val, fromIdx, toIdx) -> + later -> + elt = (itemArr.splice fromIdx, 1)[0] + frag = elt.liveRange.extract() + if toIdx of itemArr + itemArr[toIdx].liveRange.insertBefore frag + else + itemArr[toIdx-1].liveRange.insertAfter frag + itemArr.splice toIdx, 0, elt + notifyParentsRendered() + + changedAt: (val, idx) -> + later -> + elt = itemArr[idx] + throw new Error("Unknown item at index: " + idx) unless elt + elt.value = val + Spark.renderToRange elt.liveRange, _.bind(itemFunc, null, elt.value) + + return html + +# ## findParentOfType +# Ripped from [Spark.list](https://github.com/meteor/meteor/blob/master/packages/spark/spark.js#L77)* +findParentOfType = (type, range) -> + loop + range = range.findParent() + break unless range and range.type isnt type + range + +# ## makeRange +# Ripped from [Spark.list](https://github.com/meteor/meteor/blob/master/packages/spark/spark.js#L63)* +makeRange = (type, start, end, inner) -> + range = new LiveRange(Spark._TAG, start, end, inner) + range.type = type + range \ No newline at end of file diff --git a/src/reactive-array.coffee b/src/reactive-array.coffee index ac955d2..ab90a2d 100644 --- a/src/reactive-array.coffee +++ b/src/reactive-array.coffee @@ -1,5 +1,4 @@ -# I wish i could use http://wiki.ecmascript.org/doku.php?id=harmony:proxies - +# ## *Reactive Array* class @ReactiveArray constructor: () -> # the actual array that we proxy to @@ -38,6 +37,33 @@ class @ReactiveArray @_listValueDep.depend() return @_list.slice() + # ### *Mutator methods* + # --------------------------------------- + # Optimized mutator methods for reactivity + reverse: () -> + # Implement a custom array sort could be usefull based on http://jsperf.com/js-array-reverse-vs-while-loop/9 + # but this works for small array's i still trust array.reverse a bit better + Array.prototype.reverse.apply @_list + `for (left = 0, right = this._list.length - 1; left < right; left += 1, right -= 1) { + if (left === right) { continue; } + if (this._listDeps[left]) { this._listDeps[left].changed(); } + if (this._listDeps[right]) { this._listDeps[right].changed(); } + }` + @_listValueDep.changed() + return @ + + # #### sort + # Sorts the elements of an array in place and returns the array. + sort: () -> + orgList = @_list.slice() + Array.prototype.sort.apply @_list, arguments + + # Find the changed values and trigger there dependencies + for dep, i in @_listDeps when dep && orgList[i] != @_list[i] + dep.changed() + @_listValueDep.changed() + return @ + # ### *Accessor methods* # --------------------------------------- # Optimized accessor methods for reactivity @@ -129,7 +155,7 @@ class @ReactiveArray # #### clone # *[EJSON::clone](http://docs.meteor.com/#ejson_type_clone)* clone: () -> - ReactiveArray.wrap @_list + @constructor.wrap @_list # #### equals # *[EJSON::equals](http://docs.meteor.com/#ejson_type_equals)* @@ -188,9 +214,10 @@ class @ReactiveArray # ### _indexSet _indexSet: (i, val) -> - @_list[i] = val - @_listDeps[i]?.changed() - @_listValueDep.changed() + if @_list[i] != val + @_list[i] = val + @_listDeps[i]?.changed() + @_listValueDep.changed() val # #### _definePrivateProperty @@ -224,16 +251,6 @@ _.each ['shift', 'splice', 'unshift'], (m) -> dep.changed() for dep, i in @_listDeps when dep && orgList[i] != @_list[i] rtn -_.each ['reverse','sort'], (m) -> - ReactiveArray.prototype[m] = () -> - orgList = @_list.slice() - Array.prototype[m].apply @_list, arguments - - # Find the changed values and trigger there dependencies - dep.changed() for dep, i in @_listDeps when dep && orgList[i] != @_list[i] - @_listValueDep.changed() - return @ - # #### *Accessor methods* # Create Accessor proxy methods _.each ['concat','slice'], (m) -> @@ -241,7 +258,7 @@ _.each ['concat','slice'], (m) -> rtn = Array.prototype[m].apply @_list, arguments @_listLengthDep.depend() @_listValueDep.depend() - ReactiveArray.wrap rtn + @constructor.wrap rtn _.each ['join','toString'], (m) -> ReactiveArray.prototype[m] = () -> @@ -263,7 +280,7 @@ _.each ['filter', 'map'], (m) -> @_listLengthDep.depend() @_listValueDep.depend() - ReactiveArray.wrap rtn + @constructor.wrap rtn # Create iteration proxy methods for `reduce`, `reduceRight` that are using [underscore.js](http://underscorejs.org/) _.each ['reduce', 'reduceRight'], (m) -> diff --git a/src/reactive-list-test.coffee b/src/reactive-list-test.coffee new file mode 100644 index 0000000..0cf885d --- /dev/null +++ b/src/reactive-list-test.coffee @@ -0,0 +1,275 @@ +Tinytest.add "ReactiveList - added/addedAt", (test) -> + invalidX = 0 + invalidCall = () -> + invalidX++ + + addedAtX = 0 + addedX = 0 + addedVal = 'init' + addedIdx = 0 + callbacks = + added: (val) -> + test.equal val, addedVal + addedX++ + addedAt: (val, idx) -> + test.equal val, addedVal + test.equal idx, addedIdx + addedAtX++ + changed: invalidCall + changedAt: invalidCall + removed: invalidCall + removedAt: invalidCall + movedTo: invalidCall + + list = new ReactiveList('init') + + # observe: test initial added event's + handle = list.observe callbacks + test.equal invalidX, 0 + test.equal addedX, 1 + test.equal addedAtX, 1 + + # push: test added + addedVal = 'push' + addedIdx = 1 + list.push addedVal + test.equal invalidX, 0 + test.equal addedX, 2 + test.equal addedAtX, 2 + + # unshift: test added + addedVal = 'unshift' + addedIdx = 0 + list.unshift addedVal + test.equal invalidX, 0 + test.equal addedX, 3 + test.equal addedAtX, 3 + + # splice: test added + addedVal = 'splice1' + addedIdx = 1 + list.splice 1, 0, 'splice1' + test.equal invalidX, 0 + test.equal addedX, 4 + test.equal addedAtX, 4 + + addedVal = 'splice2' + addedIdx = 3 + list.splice -1, 0, 'splice2' + test.equal invalidX, 0 + test.equal addedX, 5 + test.equal addedAtX, 5 + +Tinytest.add "ReactiveList - remove/removeAt", (test) -> + invalidX = -14 + invalidCall = () -> + invalidX++ + + removedAtX = 0 + removedX = 0 + removedVal = null + removedIdx = 0 + callbacks = + removed: (val) -> + test.equal val, removedVal + removedX++ + removedAt: (val, idx) -> + test.equal val, removedVal + test.equal idx, removedIdx + removedAtX++ + changed: invalidCall + changedAt: invalidCall + added: invalidCall + addedAt: invalidCall + movedTo: invalidCall + + list = new ReactiveList('shift','keep1','splice1','keep2','splice2','keep3','pop') + + handle = list.observe callbacks + test.equal invalidX, 0, 'observer' + test.equal removedX, 0, 'observer' + test.equal removedAtX, 0, 'observer' + + # pop + removedVal = 'pop' + removedIdx = 6 + list.pop() + test.equal invalidX, 0, 'pop' + test.equal removedX, 1, 'pop' + test.equal removedAtX, 1, 'pop' + + # shift + removedVal = 'shift' + removedIdx = 0 + list.shift() + test.equal invalidX, 0 + test.equal removedX, 2 + test.equal removedAtX, 2 + + # splice: test added + removedVal = 'splice1' + removedIdx = 1 + list.splice 1, 1 + test.equal invalidX, 0 + test.equal removedX, 3 + test.equal removedAtX, 3 + + removedVal = 'splice2' + removedIdx = 2 + list.splice -2, 1 + test.equal invalidX, 0 + test.equal removedX, 4 + test.equal removedAtX, 4 + +Tinytest.add "ReactiveList - splice", (test) -> + addedX = addedAtX = -7 + changedX = changedAtX = 0 + removedX = removedAtX = 0 + movedX = 0 + + changedRun = [ + { val: '2', newVal: 'replacement1', idx: 1 } # run 1 + { val: '4', newVal: 'replacement2', idx: 3 } # run 2 + { val: '5', newVal: 'replacement3', idx: 4 } + + { val: '3', newVal: 'replacement4', idx: 2 } # run 3 + { val: 'replacement4', newVal: 'replacement5', idx: 2 } # run 4 + ] + addedRun = [ + { val: '3.5', idx: 3 } # run 3 + ] + removedRun = [ + { val: '3.5', idx: 3 } # run 4 + ] + movedRun = [] + + callbacks = + added: (val) -> + if addedX >= 0 + eq = addedRun[addedX] + test.equal val, eq.val, 'added - val: ' + addedX + addedX++ + addedAt: (val, idx) -> + if addedAtX >= 0 + eq = addedRun[addedAtX] + test.equal val, eq.val, 'addedAt - val: ' + addedAtX + test.equal idx, eq.idx, 'addedAt - idx: ' + addedAtX + addedAtX++ + changed: (val, oldVal) -> + eq = changedRun[changedX] + test.equal val, eq.newVal, 'changed - val: ' + changedX + test.equal oldVal, eq.val, 'changed - oldVal: ' + changedX + changedX++ + changedAt: (val, oldVal, idx) -> + eq = changedRun[changedAtX] + test.equal val, eq.newVal, 'changedAt - val: ' + changedAtX + test.equal oldVal, eq.val, 'changedAt - oldVal: ' + changedAtX + test.equal idx, eq.idx, 'changedAt - idx: ' + changedAtX + changedAtX++ + removed: (val) -> + eq = removedRun[removedX] + test.equal val, eq.val, 'removed - val: ' + removedX + removedX++ + removedAt: (val, idx) -> + eq = removedRun[removedAtX] + test.equal val, eq.val, 'removedAt - val: ' + removedAtX + test.equal idx, eq.idx, 'removedAt - idx: ' + removedAtX + removedAtX++ + movedTo: (val, idx) -> + eq = movedRun[movedX] + test.equal val, eq.val, 'movedTo - val: ' + movedX + test.equal idx, eq.idx, 'movedTo - idx: ' + movedX + movedX++ + + list = new ReactiveList('1','2','3','4','5','6','7') + + # Attach callbacks + list.observe callbacks + test.equal addedX, 0, 'added: observe' + test.equal addedAtX, 0, 'addedAt: observe' + test.equal removedX, 0, 'removedAt: observe' + test.equal removedAtX, 0, 'removedAt: observe' + test.equal changedX, 0, 'changed: observe' + test.equal changedAtX, 0, 'changedAt: observe' + test.equal movedX, 0, 'moved: observe' + + # splice + list.splice 1, 1, changedRun[0].newVal + test.equal addedX, 0, 'added: run 1' + test.equal addedAtX, 0, 'addedAt: run 1' + test.equal removedX, 0, 'removedAt: run 1' + test.equal removedAtX, 0, 'removedAt: run 1' + test.equal changedX, 1, 'changed: run 1' + test.equal changedAtX, 1, 'changedAt: run 1' + test.equal movedX, 0, 'moved: run 1' + + # splice + list.splice 3, 2, changedRun[1].newVal, changedRun[2].newVal + test.equal addedX, 0, 'added: run 2' + test.equal addedAtX, 0, 'addedAt: run 2' + test.equal removedX, 0, 'removedAt: run 2' + test.equal removedAtX, 0, 'removedAt: run 2' + test.equal changedX, 3, 'changed: run 2' + test.equal changedAtX, 3, 'changedAt: run 2' + test.equal movedX, 0, 'moved: run 2' + + # splice + list.splice 2, 1, changedRun[3].newVal, addedRun[0].val + test.equal addedX, 1, 'added: run 3' + test.equal addedAtX, 1, 'addedAt: run 3' + test.equal removedX, 0, 'removedAt: run 3' + test.equal removedAtX, 0, 'removedAt: run 3' + test.equal changedX, 4, 'changed: run 3' + test.equal changedAtX, 4, 'changedAt: run 3' + test.equal movedX, 0, 'moved: run 3' + + # splice + list.splice 2, 2, changedRun[4].newVal + test.equal addedX, 1, 'added: run 4' + test.equal addedAtX, 1, 'addedAt: run 4' + test.equal removedX, 1, 'removedAt: run 4' + test.equal removedAtX, 1, 'removedAt: run 4' + test.equal changedX, 5, 'changed: run 4' + test.equal changedAtX, 5, 'changedAt: run 4' + test.equal movedX, 0, 'moved: run 4' + +Tinytest.add "ReactiveList - reverse", (test) -> + arr = ['1','2','3','4','5','6','7'] + arrReversed = arr.slice().reverse() + list = ReactiveList.wrap(arr) + + list.observe + movedTo: (val, fromIdx, toIdx) -> + arr.splice fromIdx, 1 + arr.splice toIdx, 0, val + + test.equal list.reverse(), ReactiveList.wrap(arrReversed) + test.equal arr, arrReversed + + list.pop() + arr.pop() + arrReversed.pop() + arrReversed.reverse() + test.equal list.reverse(), ReactiveList.wrap(arrReversed) + test.equal arr, arrReversed + +Tinytest.add "ReactiveList - sort", (test) -> + arrs = [ + ['1','7','3','4','2','6','8','5'] + ['1','2','3','4','5','6','7'].reverse() + ['1','7','3','7','4','2','6','8','5'] + ['1','7','3','7','7','4','7','2','6','8','5'], + ['d', 'a', 'c', 'b', 'z', 'y', 'y'] + ] + for arr in arrs + arrSorted = arr.slice().sort() + list = ReactiveList.wrap(arr) + + list.observe + movedTo: (docVal, fromIdx, toIdx) -> + val = (arr.splice fromIdx, 1)[0] + test.equal val, docVal + arr.splice toIdx, 0, val + + test.equal list.sort(), ReactiveList.wrap(arrSorted) + test.equal arr, arrSorted diff --git a/src/reactive-list.coffee b/src/reactive-list.coffee new file mode 100644 index 0000000..28c9df3 --- /dev/null +++ b/src/reactive-list.coffee @@ -0,0 +1,246 @@ +# ## *Reactive List* +# Represents a reactive list extended from a ReactiveArray. +# A `ReactiveList` adds the observe function. +# +# Example +# +# ```javascript +# list = new ReactiveList('first'); +# list.observer({ +# added: function(newDoc) { console.log("added", newDoc) } +# }); +# list.push('second'); +# ``` +class @ReactiveList extends ReactiveArray + constructor: () -> + # A array of active lineHandlers + @_definePrivateProperty '_handlers', [] + + super + + # ### observer + # Based on [cursor.observe](http://docs.meteor.com/#observe) + observe: (callbacks) -> + handle = new LiveHandler callbacks + @_handlers.push handle + @_trigger 'added', @_list[i], i for i in [0...@_list.length] by 1 + handle + + # ### *Mutator methods* + # --------------------------------------- + # Optimized mutator methods for reactivity + + # #### pop + # Removes the last element from an array and returns that element. + pop: () -> + rtn = super + @_trigger 'removed', rtn, @_list.length + rtn + + # #### + # Adds one or more elements to the end of an array and returns the new length of the array. + push: () -> + orgLength = @_list.length + rtn = super + # Fire event, added, addedAt + @_trigger 'added', @_list[i], i for i in [orgLength...@_list.length] by 1 + rtn + + # #### shift + # Removes the first element from an array and returns that element. + shift: () -> + rtn = super + @_trigger 'removed', rtn, 0 + rtn + + # #### unshift + # Adds one or more elements to the front of an array and returns the new length of the array. + unshift: () -> + orgLength = @_list.length + rtn = super + # Fire event, added, addedAt + @_trigger 'added', @_list[i], i for i in [0...(@_list.length-orgLength)] by 1 + rtn + + # #### splice + # Adds and/or removes elements from an array. + splice: () -> + orgList = @_list.slice() + rtn = super + + # start index + idx = arguments[0] + idx = orgList.length + idx if idx < 0 + rmAmount = if arguments.length > 1 then arguments[1] else orgList.length - idx + # Elements where added/changed + if arguments.length > 2 + addAmount = arguments.length - 2 + if rmAmount > 0 + changedAmount = if rmAmount > addAmount then addAmount else rmAmount + for i in [0...changedAmount] by 1 + @_trigger 'changed', @_list[idx], orgList[idx], idx + idx++ + addAmount = addAmount - changedAmount + rmAmount = rmAmount - changedAmount + + if (rmAmount-addAmount) > 0 + @_trigger 'removed', orgList[idx + i], idx + i for i in [0...rmAmount-addAmount] by 1 + else if (rmAmount-addAmount) < 0 + # only elements where added + @_trigger 'added', @_list[idx + i], idx + i for i in [0...addAmount-rmAmount] by 1 + else if rmAmount > 0 + @_trigger 'removed', orgList[idx + i], idx + i for i in [0...rmAmount] by 1 + + rtn + + # #### reverse + # Reverses an array in place. The first array element becomes the last and the last becomes the first. + reverse: () -> + super + array = @_list; + length = @_list.length + `for (left = 0, right = length - 1; left < right; left += 1, right -= 1) { + if (right === left) { continue; } + this._trigger('movedTo', array[left], right, left); + this._trigger('movedTo', array[right], left+1, right); + }` + return @ + + # #### sort + # Sorts the elements of an array in place and returns the array. + sort: () -> + org = @_list.slice() + super + + return @ if !@_hasActiveTrigger 'movedTo' + + # Create a list of moves that results in @_list + length = @_list.length + moves = [] + currentPosition = 0 + while currentPosition < length + finalPosition = @_list.indexOf(org[currentPosition]) + + # A movement of one won't happen + if currentPosition+1 == finalPosition + # Look forward maybe we have a group here + while org[currentPosition+1] == @_list[finalPosition+1] + finalPosition++ + currentPosition++ + if org[currentPosition] == @_list[finalPosition] + finalPosition++ + currentPosition++ + finalPosition = @_list.indexOf(org[currentPosition]) + # This is my evil way of detecting duplicates + if org[currentPosition] == org[currentPosition+1] + while org[currentPosition-1] == @_list[finalPosition] + finalPosition++ + if org[currentPosition] == @_list[finalPosition] + finalPosition++ + finalPosition = @_list.indexOf(org[currentPosition], finalPosition) + + move = + from: currentPosition + to: finalPosition + skip = finalPosition == -1 || lastMove && lastMove.to == move.to && lastMove.from == move.from + + if !skip && finalPosition != currentPosition + moves.push move + lastMove = move + org.splice move.to, 0, (org.splice move.from, 1)[0] + + if finalPosition < currentPosition + currentPosition = finalPosition + else + currentPosition-- + currentPosition++ + + for move in moves + this._trigger 'movedTo', @_list[move.to], move.from, move.to + + return @ + + # ### *EJSON Functions* + # --------------------------------------- + # These are overrides from [ReactiveArray](reactive-array.html) + # + # #### typeName + # *[EJSON::typeName](http://docs.meteor.com/#ejson_type_typeName)* + typeName: () -> + 'reactive-list' + + # #### equals + # *[EJSON::equals](http://docs.meteor.com/#ejson_type_equals)* + # + # **obj** object to compare + equals: (obj) -> + return obj? && + obj instanceof ReactiveList && + _.isEqual obj._list, @_list + + # ### *Internal Functions* + # --------------------------------------- + # + # #### _trigger + # trigger a event to all observeables + _trigger: (evt) -> + self = @ + args = _.toArray(arguments).slice(1) + if evt == 'movedTo' + trigger = (callbacks) -> + callbacks[evt].apply self, args if evt of callbacks + else + evtArgs = args.slice(0, -1) + evtAt = evt + 'At' + evtAtArgs = args + trigger = (callbacks) -> + callbacks[evt].apply self, evtArgs if evt of callbacks + callbacks[evtAt].apply self, evtAtArgs if evtAt of callbacks + + for i, handler of @_handlers when i of @_handlers + if handler.stopped + delete @_handlers[i] + continue + trigger handler.callbacks + return + + _hasActiveTrigger: (evt) -> + _.any @_handlers, (handler) -> + return !handler.stopped && evt of handler.callbacks + + # ### _indexSet + _indexSet: (idx, val) -> + rtn = val + if @_list[idx] != val + org = list[idx] + rtn = super + @_trigger 'changed', @_list[idx], org, idx + rtn + +# ### *Helper methods* +# --------------------------------------- +# #### wrap +# Method for wrapping a array +ReactiveList.wrap = (arr) -> + obj = new ReactiveList + obj._list = _.toArray arr + obj._syncIndexProxies(true) + obj + +# ## EJSON add ReactiveArray +# *[EJSON.addType](https://docs.meteor.com/#ejson_add_type)* +EJSON.addType 'reactive-list', (jsonObj) -> + ReactiveList.wrap jsonObj + +class LiveHandler + constructor: (callbacks) -> + self = @ + @stopped = false + @callbacks = callbacks + + if (Deps.active) + Deps.onInvalidate () -> + self.stop() + + stop: () -> + @stopped = true