пятница, 26 сентября 2014 г.

Усовершенствование стандартного Array

Arr.js - это класс, унаследованный от стандартного Array. Отличительными особенностями являются: наличие события change для отслеживания любых изменений в массиве, и методы insert(), update(), remove(), set(), get() для упрощенной работы с массивом. Доступны все родные методы стандартного Array.



(function(rootScope) {

  var
    arrayPop = Array.prototype.pop,
    arrayPush = Array.prototype.push,
    arrayReverse = Array.prototype.reverse,
    arrayShift = Array.prototype.shift,
    arraySort = Array.prototype.sort,
    arraySplice = Array.prototype.splice,
    arrayUnshift = Array.prototype.unshift;

  /**
   *
   */
  var Arr = function() {
    arrayPush.apply(this, arguments);
 
    this.events = [];
  };

  Arr.prototype = [];

  /**
   *
   */
  Arr.prototype.events = [];
 
  /**
   *
   */
  Arr.prototype.get = function(index, defaultValue) {
    defaultValue = typeof defaultValue === 'undefined' ? undefined: defaultValue;
    return typeof this[index] === 'undefined' ? defaultValue: this[index];
  };
 
  /**
   *
   */
  Arr.prototype.on = function(eventName, handler) {
    if (! handler instanceof Function) {
      throw new Error('handler should be an Function');
    }
 
    this.events.push({
      name: eventName,
      handler: handler
    });
 
    return this;
  };
 
  /**
   *
   */
  Arr.prototype.trigger = function(eventName, args) {
    args = args || [];
 
    for (var i=0,len=this.events.length; i<len; i++) {
      if (this.events[i].name == eventName) {
        this.events[i].handler.apply(this, [args]);
      }
    }
 
    return this;
  };
 
  /**
   *
   */
  Arr.prototype.update = function(handler) {
    if (! handler instanceof Function) {
      throw new Error('handler should be an Function');
    }
 
    var oldValue, newValue;
    var result = [];
 
    for (var i=0,len=this.length; i<len; i++) {
      oldValue = this[i];
      newValue = handler.apply(this, [oldValue, i]);
   
      if (typeof newValue !== 'undefined') {
        this[i] = newValue;
        result.push(newValue);
      }
    }
 
    if (result.length > 0) {
      this.trigger('change', {
        type: 'update',
        items: result
      });
    }
 
    return this;
  };

  /**
   *
   */
  Arr.prototype.insert = function(items) {
    if (! items instanceof Array) {
      throw new Error('items should be an Array');
    }
 
    arrayPush.apply(this, items);
    this.trigger('change', {
      type: 'insert',
      items: items
    });
    return this;
  };

  /**
   *
   */
  Arr.prototype.remove = function(handler) {
    if (! handler instanceof Function) {
      throw new Error('handler should be an Function');
    }
 
    var result = [];
    var stay = [];

    for (var i=0, len=this.length; i<len; i++) {
      isRemove = handler.apply(this, [this[i], i]);
   
      if (isRemove === true) {
        result.push(this[i]);
      } else {
        stay.push(this[i]);
      }
    }

    arraySplice.apply(this, [0, this.length]);
    arrayPush.apply(this, stay);

    if (result.length > 0) {
      this.trigger('change', {
        type: 'remove',
        items: result
      });
    }
 
    return this;
  };

  /**
   *
   */
  Arr.prototype.set = function(index, value) {
    if (! index instanceof Number) {
      throw new Error('index should be an Number');
    }
 
    this[index] = value;
    this.trigger('change', {
      type: 'update',
      items: [this[index]]
    });
    return this;
  };
 
  /**
   * Removes the last element from an array and returns that element.
   */
  Arr.prototype.pop = function() {
    var result = arrayPop.apply(this, arguments);
    this.trigger('change', {
      type: 'remove',
      items: [result]
    });
    return result;
  };
 
  /**
   * Adds one or more elements to the end of an array and returns the new length of the array.
   */
  Arr.prototype.push = function() {
    var result = arrayPush.apply(this, arguments);
    this.trigger('change', {
      type: 'insert',
      items: Array.prototype.slice.call(arguments, 0)
    });
    return result;
  };
 
  /**
   * Reverses the order of the elements of an array — the first becomes the last, and the last becomes the first.
   */
  Arr.prototype.reverse = function() {
    var result = arrayReverse.apply(this, arguments);
    this.trigger('change', {
      type: 'update',
      items: result.slice(0)
    });
    return result;
  };
 
  /**
   * Removes the first element from an array and returns that element.
   */
  Arr.prototype.shift = function() {
    var result = arrayShift.apply(this, arguments);
    this.trigger('change', {
      type: 'remove',
      items: [result]
    });
    return result;
  };
 
  /**
   * Sorts the elements of an array in place and returns the array.
   */
  Arr.prototype.sort = function() {
    var result = arraySort.apply(this, arguments);
    this.trigger('change', {
      type: 'update',
      items: result
    });
    return result;
  };
 
  /**
   * Adds and/or removes elements from an array.
   */
  Arr.prototype.splice = function() {
    var items = this.slice(arguments[0], arguments[0]+arguments[1]);
    var result = arraySplice.apply(this, arguments);
    this.trigger('change', {
      type: 'remove',
      items: items
    });
    return result;
  };
 
  /**
   * Adds one or more elements to the front of an array and returns the new length of the array.
   */
  Arr.prototype.unshift = function() {
    var result = arrayUnshift.apply(this, arguments);
    this.trigger('change', {
      type: 'insert',
      items: [result]
    });
    return result;
  };

  // export

  if (typeof module !== 'undefined') {
    module.exports = Arr;
  } else {
    rootScope.Arr = Arr;
  }
})(this);

Примеры работы.

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.on('change', function() {
  alert('I changed fruits: ' + fruits.join(', '));
});

fruits.push('banana');

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.get(0);
// apple

fruits.get(10, 'lime'); // trying to get undefined element - return defaultValue
// lime

fruits.get(20); // trying to get undefined element
// null

fruits.set(1, 'nut');
// ['nut', 'orange', 'pineapple']

fruits.insert(['lime', 'banana', 'kivi']);
// ['nut', 'orange', 'pineapple', 'lime', 'banana', 'kivi']

fruits.remove(function(item, index) {
  if (item.indexOf('apple') !== -1) { // remove only items where word "apple" is founded
    return true;
  }
});
// ['nut', 'orange', 'lime', 'banana', 'kivi']

fruits.update(function(item, index) {
  if (item.indexOf('nut') !== -1) { // update "nut" to "apple"
    return 'apple';
  }
});
// ['apple', 'orange', 'lime', 'banana', 'kivi']

Зачем событие change и как с ними работать.

Наличие события позволяет сделать:
подобие FRP: когда изменение одних данных должно повлечь за собой изменение других данных и так далее
отложенный рендеринг: что то изменилось в массиве — обновили HTML (ala angular)
автоматическое сохранение данных на сервер при любых изменениях

Поддерживается одно событие — change.

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.on('change', function(event) { // handler
  console.log(event);
});

fruits.push('banana');
// { "type": "insert", "items": ["banana"] }

fruits.remove(function(item) { return item == 'banana'; });
// { "type": "remove", "items": ['banana"] }

Понять что произошло в массиве можно по передаваемому в handler объекту, event. Свойства объекта event: type может принимать значения: insert, update, remove. Свойство items позволяет узнать какие элементы массива были затронуты.

Наглядный пример

// Массив в котором планируем хранить данные о погоде
var weatherList = new Arr;

// При изменении в массиве - перересовываем список
weatherList.on('change', function() {
  var el = $('#weather');
  var celsius, maxCelsius, minCelsius, weather;

  el.html('');

  weatherList.forEach(function(item) {
    celsius = Math.floor(item.main.temp - 273);
    maxCelsius = Math.floor(item.main.temp_max - 273);
    minCelsius = Math.floor(item.main.temp_min - 273);
    weather = item.weather.pop().main;
    el.append('<li><b>' + item.name + '</b> ' + ' ' + celsius + ' (max: ' + maxCelsius + ', min: ' + minCelsius + ') ' + weather + '</li>');
  });
});

// Загрузка погоды из сервиса, обновление массива weatherList
function loadWeather(lat, lng) {
  $.get('http://api.openweathermap.org/data/2.5/find?lat=' + lat + '&lon=' + lng + '&cnt=10').done(function(data) {
    // clear weather list
    weatherList.remove(function() { return true; });

    // insert items
    weatherList.insert(data.list);
  });
}

// Погода в Киеве
loadWeather(50.4666537, 30.5844519);

Примеры работы основных методов.

new Arr([item1, item2, ..., itemN])
Initialize new array items: item1, item2, ..., itemN.

Properties
events

List of Attached Events.

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.on('change', function(event) {
  console.log('fruits list is changed.');
});

// fruits.events
// [{
//  "name": "change",
//  "handler": function() { ... }
// }]

fruits.push('mango');
// fruits list is changed.
// fruits
// ['apple', 'orange', 'pineapple', 'mango']
length

Standard property length.

Accessor methods
get(index [, defaultValue])

Example:

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.get(0);
// apple

fruits.get(10, 'lime'); // trying to get undefined element - return defaultValue
// lime

fruits.get(20); // trying to get undefined element
// undefined
Standard accessor methods are supported

concat() Returns a new array comprised of this array joined with other array(s) and/or value(s).
join() Joins all elements of an array into a string.
slice() Extracts a section of an array and returns a new array.
toString() Returns a string representing the array and its elements. Overrides the Object.prototype.toString() method.
toLocaleString() Returns a localized string representing the array and its elements. Overrides the Object.prototype.toLocaleString() method.
indexOf() Returns the first (least) index of an element within the array equal to the specified value, or -1 if none is found.
lastIndexOf() Returns the last (greatest) index of an element within the array equal to the specified value, or -1 if none is found.
and other methods.

Mutator methods
Notice: Traditional mutator arr[index] = value do not trigger event change. Use method set(index, value) instead arr[index] = value.

set(index, value)

Set value by index. Will be triggered event change.

Example:

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.set(0, 'banana');
// ['banana', 'orange', 'pineapple']

fruits.set(1, 'lime');
// ['banana', 'lime', 'pineapple']

fruits.set(3, 'nut');
// ['banana', 'lime', 'pineapple', 'nut']
insert([item1, item2, ..., itemN])

Insert array of items. Will be triggered event change.

Example:

var fruits = new Arr();

fruits.inser('apple', 'orange', 'pineapple');
// ['apple', 'orange', 'pineapple']
update(handler)

Update item if handler return true. Will be triggered event change if one or more items updated.

handler can recive data:

value (mixed) current value
index (number) current index
Example:

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.update(function(value, index) {
  if (index === 2) {
    return 'lime';  // "return" not undefined value for update item
  }
});
// ['apple', 'orange', 'lime']

fruits.update(function(value, index) {
  return index;
});
// [0, 1, 2]
remove(handler)

Remove item if handler return true. Will be triggered event change if one or more items removed.

Example:

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.remove(function(value, index) {
  if (value.indexOf('apple') !== -1) {
    return true;
  }
});
// ['orange']
Standard mutator methods are supported

Each mutator method throw event change. How? You can read in section Events.

pop() Removes the last element from an array and returns that element.
push() Adds one or more elements to the end of an array and returns the new length of the array.
reverse() Reverses the order of the elements of an array — the first becomes the last, and the last becomes the first.
shift() Removes the first element from an array and returns that element.
sort() Sorts the elements of an array in place and returns the array.
splice() Adds and/or removes elements from an array.
unshift() Adds one or more elements to the front of an array and returns the new length of the array.
and other methods.

Sometimes you need to push array of items to Arr. You can push array of items in this way (note: now you can use method insert()):

var fruits = new Arr;

fruits.push.apply(fruits, ['apple', 'orange', 'pineapple']);
// fruits
// ['apple', 'orange', 'pineapple']
For remove item(s) from Arr you can use traditional method splice() (note: now you can use method remove()).

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.splice(0, 1); // remove first item
// fruits
// ['orange', 'pineapple']
Iteration methods
forEach() Calls a function for each element in the array.
every() Returns true if every element in this array satisfies the provided testing function.
some() Returns true if at least one element in this array satisfies the provided testing function.
filter() Creates a new array with all of the elements of this array for which the provided filtering function returns true.
map() Creates a new array with the results of calling a provided function on every element in this array.
reduce() Apply a function against an accumulator and each value of the array (from left-to-right) as to reduce it to a single value.
reduceRight() Apply a function against an accumulator and each value of the array (from right-to-left) as to reduce it to a single value.
and other methods.

You can use method filter() for searching items by conditions.

Example:

var fruits = new Arr('apple', 'orange', 'pineapple');

var fruitsWithWordApple = fruits.filter(function(value, index) {
  if (value.indexOf('apple') !== -1) {
    return true;
  } else {
    return false;
  }
});
// fruitsWithWordApple
// ['apple', 'pineapple']
Events
Instance of Arr throw only one event change.

How to use events? You can use array events like events in Backbone Collection.

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.on('change', function(event) {
  // event
  // {
  //   "type": "insert",
  //   "items": ['mongo']
  // }

  console.log('fruits list is changed.');
});

// fruits.events
// [{
//  "name": "change",
//  "handler": function() { ... }
// }]

fruits.push('mango');
// fruits list is changed.
// fruits
// ['apple', 'orange', 'pineapple', 'mango']
Events Handling and Triggering
Notice: Traditional mutator arr[index] = value do not trigger event change. Use method set(index, value) instead arr[index] = value.

on(eventName, handler)

Attach event handler.

handler can recive event that have data:

type (string) can be insert, update, remove
items (array) are items that was inserted, updated or removed
Example:

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.on('change', function(event) {
  // event
  // {
  //   "type": "insert",
  //   "items": ['mango']
  // }

  console.log('fruits list is changed.');
});

fruits.push('mango');
trigger(eventName [, args])

Trigger some event.

var fruits = new Arr('apple', 'orange', 'pineapple');

fruits.on('change', function(event) {
  console.log('fruits list is changed.');
});

fruits.trigger('change');
// fruits list is changed.
Use Cases
Send server updates when Arr is changed.

var products = new Arr(
  {
    id: 1,
    name: 'apple'
  },
  {
    id: 2,
    name: 'orange'
  },
  {
    id: 3,
    name: 'pineapple'
  }
);

products.on('change', function(event) {
  // products are changed
  // you can use event.type == 'insert' or 'update' or 'remove' to detect items that you need to update on the server

  // $ is link on jQuery
  $.post('/products', products)
    .fail(function() {
      alert('error');
    })
    .done(function(updatedProducts) {
      alert('products are updated');
    });
});

products.push({
  name: 'lime'
});

Комментариев нет:

Отправить комментарий