Repository: javve/list.js

Issue:
The list need to have at list one item on init otherwise you'll have to add a template.
_The list need to have at list one item on init otherwise you'll have to add a template._ error message gets thrown if there are no items in the list. 

I think the process should **not** throw an error.


Discussion:
@mkvlrn-cm42: Looks like whenever this error is thrown the other tables/lists after the one causing problems do not work anymore.
@klonos: ...besides, the "have at l**i**st one item" part of that message should have been "have at l**ea**st one item" 😉 


Solution patch:
diff --git a/docs/api.html b/docs/api.html
index 7b876af..3c14bea 100644
--- a/docs/api.html
+++ b/docs/api.html
@@ -106,7 +106,7 @@ var valueNames =  [
 </li>
 <li>
 <p><strong><a name="item" href="#item" class="anchor"></a>item</strong> <em class="docs-parameter-description">String, default: undefined</em><br>
-ID to item template element or a string of HTML.</p>
+ID to item template element or a string of HTML. Can also be a function which receives a <code>values</code> object and which must return the complete item's HTML as a string.</p>
 
 <pre><code class="javascript">var options = {
     item: "&lt;li&gt;&lt;span class='name'&gt;&lt;/span&gt;&lt;span class='city'&gt;&lt;/span&gt;&lt;/li&gt;"
@@ -115,6 +115,12 @@ ID to item template element or a string of HTML.</p>
 var options = {
     item: 'cool-item-id'
 };
+// or
+var options = {
+    item: function(values) {
+      return `&lt;li&gt;&lt;span class='name'&gt;${values.name}&lt;/span&gt;&lt;span class='city'&gt;${values.city}&lt;/span&gt;&lt;/li&gt;`;
+    }
+};
 </code></pre>
 </li>
 <li><p><strong><a name="listClass" href="#listClass" class="anchor"></a>listClass</strong> <em class="docs-parameter-description">String, default: "list"</em><br>
diff --git a/src/templater.js b/src/templater.js
index 83b9119..f40148c 100644
--- a/src/templater.js
+++ b/src/templater.js
@@ -1,126 +1,166 @@
-var Templater = function (list) {
-  var itemSource,
-    templater = this
-
-  var init = function () {
-    itemSource = templater.getItemSource(list.item)
-    if (itemSource) {
-      itemSource = templater.clearSourceItem(itemSource, list.valueNames)
+var Templater = function(list) {
+  var createItem,
+    templater = this;
+
+  var init = function() {
+    var itemSource;
+
+    if (typeof list.item === "function") {
+      createItem = function(values) {
+        var item = list.item(values);
+        return getItemSource(item);
+      }
+      return;
+    }
+
+    if (typeof list.item === "string") {
+      if (list.item.indexOf("<") === -1) {
+        itemSource = document.getElementById(list.item);
+      } else {
+        itemSource = getItemSource(list.item);
+      }
+    } else {
+      /* If item source does not exists, use the first item in list as
+      source for new items */
+      itemSource = getFirstListItem();
+    }
+
+    if (!itemSource) {
+      throw new Error(
+        "The list needs to have at least one item on init otherwise you'll have to add a template."
+      );
+    }
+
+    itemSource = createCleanTemplateItem(itemSource, list.valueNames);
+
+    createItem = function() {
+      return itemSource.cloneNode(true);
     }
   }
 
-  this.clearSourceItem = function (el, valueNames) {
+  var createCleanTemplateItem = function(templateNode, valueNames) {
+    var el = templateNode.cloneNode(true);
+    el.removeAttribute("id");
+
     for (var i = 0, il = valueNames.length; i < il; i++) {
-      var elm
-      if (valueNames[i].data) {
-        for (var j = 0, jl = valueNames[i].data.length; j < jl; j++) {
-          el.setAttribute('data-' + valueNames[i].data[j], '')
+      var elm = undefined,
+        valueName = valueNames[i];
+      if (valueName.data) {
+        for (var j = 0, jl = valueName.data.length; j < jl; j++) {
+          el.setAttribute("data-" + valueName.data[j], "");
         }
-      } else if (valueNames[i].attr && valueNames[i].name) {
-        elm = list.utils.getByClass(el, valueNames[i].name, true)
+      } else if (valueName.attr && valueName.name) {
+        elm = list.utils.getByClass(el, valueName.name, true);
         if (elm) {
-          elm.setAttribute(valueNames[i].attr, '')
+          elm.setAttribute(valueName.attr, "");
         }
       } else {
-        elm = list.utils.getByClass(el, valueNames[i], true)
+        elm = list.utils.getByClass(el, valueName, true);
         if (elm) {
           elm.innerHTML = ''
         }
       }
-      elm = undefined
     }
     return el
   }
 
-  this.getItemSource = function (item) {
-    if (item === undefined) {
-      var nodes = list.list.childNodes,
-        items = []
+  var getFirstListItem = function() {
+    var nodes = list.list.childNodes;
 
-      for (var i = 0, il = nodes.length; i < il; i++) {
-        // Only textnodes have a data attribute
-        if (nodes[i].data === undefined) {
-          return nodes[i].cloneNode(true)
-        }
-      }
-    } else if (/<tr[\s>]/g.exec(item)) {
-      var tbody = document.createElement('tbody')
-      tbody.innerHTML = item
-      return tbody.firstChild
-    } else if (item.indexOf('<') !== -1) {
-      var div = document.createElement('div')
-      div.innerHTML = item
-      return div.firstChild
-    } else {
-      var source = document.getElementById(list.item)
-      if (source) {
-        return source
+    for (var i = 0, il = nodes.length; i < il; i++) {
+      // Only textnodes have a data attribute
+      if (nodes[i].data === undefined) {
+        return nodes[i].cloneNode(true);
       }
     }
-    return undefined
+    return undefined;
   }
 
-  this.get = function (item, valueNames) {
-    templater.create(item)
-    var values = {}
-    for (var i = 0, il = valueNames.length; i < il; i++) {
-      var elm
-      if (valueNames[i].data) {
-        for (var j = 0, jl = valueNames[i].data.length; j < jl; j++) {
-          values[valueNames[i].data[j]] = list.utils.getAttribute(item.elm, 'data-' + valueNames[i].data[j])
-        }
-      } else if (valueNames[i].attr && valueNames[i].name) {
-        elm = list.utils.getByClass(item.elm, valueNames[i].name, true)
-        values[valueNames[i].name] = elm ? list.utils.getAttribute(elm, valueNames[i].attr) : ''
-      } else {
-        elm = list.utils.getByClass(item.elm, valueNames[i], true)
-        values[valueNames[i]] = elm ? elm.innerHTML : ''
-      }
-      elm = undefined
+  var getItemSource = function(itemHTML) {
+    if (/<tr[\s>]/g.exec(itemHTML)) {
+      var tbody = document.createElement("tbody");
+      tbody.innerHTML = itemHTML;
+      return tbody.firstElementChild;
+    } else if (itemHTML.indexOf("<") !== -1) {
+      var div = document.createElement("div");
+      div.innerHTML = itemHTML;
+      return div.firstElementChild;
     }
-    return values
-  }
+    return undefined;
+  };
 
-  this.set = function (item, values) {
-    var getValueName = function (name) {
-      for (var i = 0, il = list.valueNames.length; i < il; i++) {
-        if (list.valueNames[i].data) {
-          var data = list.valueNames[i].data
-          for (var j = 0, jl = data.length; j < jl; j++) {
-            if (data[j] === name) {
-              return { data: name }
-            }
+  var getValueName = function(name) {
+    for (var i = 0, il = list.valueNames.length; i < il; i++) {
+      var valueName = list.valueNames[i];
+      if (valueName.data) {
+        var data = valueName.data;
+        for (var j = 0, jl = data.length; j < jl; j++) {
+          if (data[j] === name) {
+            return { data: name };
           }
-        } else if (list.valueNames[i].attr && list.valueNames[i].name && list.valueNames[i].name == name) {
-          return list.valueNames[i]
-        } else if (list.valueNames[i] === name) {
-          return name
         }
+      } else if (
+        valueName.attr &&
+        valueName.name &&
+        valueName.name == name
+      ) {
+        return valueName;
+      } else if (valueName === name) {
+        return name;
       }
     }
-    var setValue = function (name, value) {
-      var elm
-      var valueName = getValueName(name)
-      if (!valueName) return
+  };
+
+  var setValue = function(item, name, value) {
+    var elm = undefined,
+      valueName = getValueName(name);
+    if (!valueName) return;
+    if (valueName.data) {
+      item.elm.setAttribute("data-" + valueName.data, value);
+    } else if (valueName.attr && valueName.name) {
+      elm = list.utils.getByClass(item.elm, valueName.name, true);
+      if (elm) {
+        elm.setAttribute(valueName.attr, value);
+      }
+    } else {
+      elm = list.utils.getByClass(item.elm, valueName, true);
+      if (elm) {
+        elm.innerHTML = value;
+      }
+    }
+  };
+
+  this.get = function(item, valueNames) {
+    templater.create(item);
+    var values = {};
+    for (var i = 0, il = valueNames.length; i < il; i++) {
+      var elm = undefined,
+        valueName = valueNames[i];
       if (valueName.data) {
-        item.elm.setAttribute('data-' + valueName.data, value)
-      } else if (valueName.attr && valueName.name) {
-        elm = list.utils.getByClass(item.elm, valueName.name, true)
-        if (elm) {
-          elm.setAttribute(valueName.attr, value)
+        for (var j = 0, jl = valueName.data.length; j < jl; j++) {
+          values[valueName.data[j]] = list.utils.getAttribute(
+            item.elm,
+            "data-" + valueName.data[j]
+          );
         }
+      } else if (valueName.attr && valueName.name) {
+        elm = list.utils.getByClass(item.elm, valueName.name, true);
+        values[valueName.name] = elm
+          ? list.utils.getAttribute(elm, valueName.attr)
+          : "";
       } else {
-        elm = list.utils.getByClass(item.elm, valueName, true)
-        if (elm) {
-          elm.innerHTML = value
-        }
+        elm = list.utils.getByClass(item.elm, valueName, true);
+        values[valueName] = elm ? elm.innerHTML : "";
       }
-      elm = undefined
     }
+    return values
+  }
+
+  this.set = function(item, values) {
     if (!templater.create(item)) {
       for (var v in values) {
         if (values.hasOwnProperty(v)) {
-          setValue(v, values[v])
+          setValue(item, v, values[v]);
         }
       }
     }
@@ -130,18 +170,11 @@ var Templater = function (list) {
     if (item.elm !== undefined) {
       return false
     }
-    if (itemSource === undefined) {
-      throw new Error("The list needs to have at least one item on init otherwise you'll have to add a template.")
-    }
-    /* If item source does not exists, use the first item in list as
-    source for new items */
-    var newItem = itemSource.cloneNode(true)
-    newItem.removeAttribute('id')
-    item.elm = newItem
-    templater.set(item, item.values())
-    return true
-  }
-  this.remove = function (item) {
+    item.elm = createItem(item.values());
+    templater.set(item, item.values());
+    return true;
+  };
+  this.remove = function(item) {
     if (item.elm.parentNode === list.list) {
       list.list.removeChild(item.elm)
     }



Test patch:
diff --git a/__test__/create.test.js b/__test__/create.test.js
index f25b6de..fbc1f38 100644
--- a/__test__/create.test.js
+++ b/__test__/create.test.js
@@ -36,90 +36,66 @@ describe('Create', function () {
 
     $(document.body).append(listEl)
 
-    var list = new List(
-      'list',
-      {
-        valueNames: ['name'],
-        item: '<li><span class="name"></span></li>',
-      },
-      [{ name: 'Jonny' }]
-    )
-
-    it('should contain one item', function () {
-      expect(list.items.length).toEqual(1)
-      expect(listEl.find('li').length).toEqual(1)
-    })
-
-    it('should contain two items', function () {
-      list.add({ name: 'Jonas' })
-      expect(list.items.length).toEqual(2)
-      expect(listEl.find('li').length).toEqual(2)
-    })
-
-    listEl.remove()
-  })
-
-  describe('Without items and with string template for table', function () {
+  describe('Without items and with template function', function() {
+    var listEl = $('<div id="list">\
+      <ul class="list"></ul>\
+    </div>');
+
+    $(document.body).append(listEl);
+
+    var list = new List('list', {
+      valueNames: ['name'],
+      item: function(values) {
+        return `<li data-template-fn-${values.name.toLowerCase()}><span class="name"></span></li>`;
+      }
+    }, [
+      { name: 'Jonny' }
+    ]);
+
+    it('should contain one item', function() {
+      expect(list.items.length).toEqual(1);
+      expect(listEl.find('li').size()).toEqual(1);
+    });
+
+    it('should contain two items', function() {
+      list.add({ name: 'Jonas' });
+      expect(list.items.length).toEqual(2);
+      expect(listEl.find('li').size()).toEqual(2);
+    });
+
+    it('should get values from items', function() {
+      list.add({ name: 'Egon' });
+      expect(listEl.find('li[data-template-fn-egon]').size()).toEqual(1);
+    });
+
+    listEl.remove();
+  });
+
+  describe('Without items and with string template for table', function() {
     var listEl = $('<div id="list">\
       <table class="list"></table>\
     </div>')
 
     $(document.body).append(listEl)
 
-    var list = new List(
-      'list',
-      {
-        valueNames: ['name'],
-        item: '<tr><span class="name"></span></tr>',
-      },
-      [{ name: 'Jonny' }]
-    )
-
-    it('should contain one item', function () {
-      expect(list.items.length).toEqual(1)
-      expect(listEl.find('tr').length).toEqual(1)
-    })
+  describe('without items and or template', function() {
 
-    it('should contain two items', function () {
-      list.add({ name: 'Jonas' })
-      expect(list.items.length).toEqual(2)
-      expect(listEl.find('tr').length).toEqual(2)
-    })
-
-    listEl.remove()
-  })
-
-  describe('without items and or template', function () {
-    it('should not throw error on init', function () {
+    it('should throw error on init', function() {
       var listEl = $('<div id="list">\
         <ul class="list"></ul>\
       </div>')
       $(document.body).append(listEl)
 
-      var list = new List('list', {
-        valueNames: ['name'],
-      })
-
-      listEl.remove()
-    })
-
-    it('should throw error when created items', function () {
-      var listEl = $('<div id="list">\
-        <ul class="list"></ul>\
-      </div>')
-      $(document.body).append(listEl)
-
-      var list = new List('list', {
-        valueNames: ['name'],
-      })
-      expect(function () {
-        list.add({ name: 'Jonas' })
-      }).toThrow()
-      listEl.remove()
-    })
-  })
+      expect(function() {
+        var list = new List('list', {
+          valueNames: ['name']
+        });
+      }).toThrow();
+      listEl.remove();
+    });
+  });
 
-  describe('Without items and with HTML template', function () {
+  describe('Without items and with HTML template', function() {
     var listEl = $('<div id="list">\
       <ul class="list"></ul>\
     </div>')