Repository: vuejs/vue
Pull request: Add iterable supports for v-for 

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [ ] Bugfix
- [x] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [x] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)

**Other information:**

Adds support for iterable object implementing [Symbol.iterator]()
It shouldn't have an impact for existing code because non iterable objects are not impacted.
Vue will have the same behaviour than a for...of expression for iterables. (actual interest for using iterables)
Allows us to pass an iterable directly to a template without giving internal properties or converting it to an array


I wrote 4 new tests. No existing tests needed changes.
All tests passed. I ran tests with PhantomJS (lack of Symbol support), Firefox and Chrome.



Changed files:
filename: src/core/instance/render-helpers/render-list.js
patch: @@ -1,6 +1,6 @@
 /* @flow */
 
-import { isObject, isDef } from 'core/util/index'
+import { isObject, isDef, hasSymbol } from 'core/util/index'
 
 /**
  * Runtime helper for rendering v-for lists.
@@ -25,11 +25,21 @@ export function renderList (
       ret[i] = render(i + 1, i)
     }
   } else if (isObject(val)) {
-    keys = Object.keys(val)
-    ret = new Array(keys.length)
-    for (i = 0, l = keys.length; i < l; i++) {
-      key = keys[i]
-      ret[i] = render(val[key], key, i)
+    if (hasSymbol && val[Symbol.iterator]) {
+      ret = []
+      const iterator: Iterator<any> = val[Symbol.iterator]()
+      let result = iterator.next()
+      while (!result.done) {
+        ret.push(render(result.value, ret.length))
+        result = iterator.next()
+      }
+    } else {
+      keys = Object.keys(val)
+      ret = new Array(keys.length)
+      for (i = 0, l = keys.length; i < l; i++) {
+        key = keys[i]
+        ret[i] = render(val[key], key, i)
+      }
     }
   }
   if (!isDef(ret)) {

filename: test/unit/features/directives/for.spec.js
patch: @@ -1,4 +1,5 @@
 import Vue from 'vue'
+import { hasSymbol } from 'core/util/env'
 
 describe('Directive v-for', () => {
   it('should render array of primitive values', done => {
@@ -123,6 +124,205 @@ describe('Directive v-for', () => {
     }).then(done)
   })
 
+  if (hasSymbol) {
+    it('should render iterable of primitive values', done => {
+      const iterable = {
+        models: ['a', 'b', 'c'],
+        index: 0,
+        [Symbol.iterator] () {
+          const iterator = {
+            index: 0,
+            models: this.models,
+            next () {
+              if (this.index < this.models.length) {
+                return { value: this.models[this.index++] }
+              } else {
+                return { done: true }
+              }
+            }
+          }
+          return iterator
+        }
+      }
+      const vm = new Vue({
+        template: `
+          <div>
+            <span v-for="item in list">{{item}}</span>
+          </div>
+        `,
+        data: {
+          list: iterable
+        }
+      }).$mount()
+      expect(vm.$el.innerHTML).toBe('<span>a</span><span>b</span><span>c</span>')
+      Vue.set(vm.list.models, 0, 'd')
+      waitForUpdate(() => {
+        expect(vm.$el.innerHTML).toBe('<span>d</span><span>b</span><span>c</span>')
+        vm.list.models.push('d')
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>d</span><span>b</span><span>c</span><span>d</span>')
+        vm.list.models.splice(1, 2)
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>d</span><span>d</span>')
+        vm.list.models = ['x', 'y']
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>x</span><span>y</span>')
+      }).then(done)
+    })
+
+    it('should render iterable of primitive values with index', done => {
+      const iterable = {
+        models: ['a', 'b', 'c'],
+        index: 0,
+        [Symbol.iterator] () {
+          const iterator = {
+            index: 0,
+            models: this.models,
+            next () {
+              if (this.index < this.models.length) {
+                return { value: this.models[this.index++] }
+              } else {
+                return { done: true }
+              }
+            }
+          }
+          return iterator
+        }
+      }
+
+      const vm = new Vue({
+        template: `
+          <div>
+            <span v-for="(item, i) in list">{{i}}-{{item}}</span>
+          </div>
+        `,
+        data: {
+          list: iterable
+        }
+      }).$mount()
+      expect(vm.$el.innerHTML).toBe('<span>0-a</span><span>1-b</span><span>2-c</span>')
+      Vue.set(vm.list.models, 0, 'd')
+      waitForUpdate(() => {
+        expect(vm.$el.innerHTML).toBe('<span>0-d</span><span>1-b</span><span>2-c</span>')
+        vm.list.models.push('d')
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>0-d</span><span>1-b</span><span>2-c</span><span>3-d</span>')
+        vm.list.models.splice(1, 2)
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>0-d</span><span>1-d</span>')
+        vm.list.models = ['x', 'y']
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>0-x</span><span>1-y</span>')
+      }).then(done)
+    })
+
+    it('should render iterable of object values', done => {
+      const iterable = {
+        models: [
+          { value: 'a' },
+          { value: 'b' },
+          { value: 'c' }
+        ],
+        index: 0,
+        [Symbol.iterator] () {
+          const iterator = {
+            index: 0,
+            models: this.models,
+            next () {
+              if (this.index < this.models.length) {
+                return { value: this.models[this.index++] }
+              } else {
+                return { done: true }
+              }
+            }
+          }
+          return iterator
+        }
+      }
+
+      const vm = new Vue({
+        template: `
+          <div>
+            <span v-for="item in list">{{item.value}}</span>
+          </div>
+        `,
+        data: {
+          list: iterable
+        }
+      }).$mount()
+      expect(vm.$el.innerHTML).toBe('<span>a</span><span>b</span><span>c</span>')
+      Vue.set(vm.list.models, 0, { value: 'd' })
+      waitForUpdate(() => {
+        expect(vm.$el.innerHTML).toBe('<span>d</span><span>b</span><span>c</span>')
+        vm.list.models[0].value = 'e'
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>e</span><span>b</span><span>c</span>')
+        vm.list.models.push({})
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>e</span><span>b</span><span>c</span><span></span>')
+        vm.list.models.splice(1, 2)
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>e</span><span></span>')
+        vm.list.models = [{ value: 'x' }, { value: 'y' }]
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>x</span><span>y</span>')
+      }).then(done)
+    })
+
+    it('should render iterable of object values with index', done => {
+      const iterable = {
+        models: [
+          { value: 'a' },
+          { value: 'b' },
+          { value: 'c' }
+        ],
+        index: 0,
+        [Symbol.iterator] () {
+          const iterator = {
+            index: 0,
+            models: this.models,
+            next () {
+              if (this.index < this.models.length) {
+                return { value: this.models[this.index++] }
+              } else {
+                return { done: true }
+              }
+            }
+          }
+          return iterator
+        }
+      }
+
+      const vm = new Vue({
+        template: `
+          <div>
+            <span v-for="(item, i) in list">{{i}}-{{item.value}}</span>
+          </div>
+        `,
+        data: {
+          list: iterable
+        }
+      }).$mount()
+      expect(vm.$el.innerHTML).toBe('<span>0-a</span><span>1-b</span><span>2-c</span>')
+      Vue.set(vm.list.models, 0, { value: 'd' })
+      waitForUpdate(() => {
+        expect(vm.$el.innerHTML).toBe('<span>0-d</span><span>1-b</span><span>2-c</span>')
+        vm.list.models[0].value = 'e'
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>0-e</span><span>1-b</span><span>2-c</span>')
+        vm.list.models.push({})
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>0-e</span><span>1-b</span><span>2-c</span><span>3-</span>')
+        vm.list.models.splice(1, 2)
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>0-e</span><span>1-</span>')
+        vm.list.models = [{ value: 'x' }, { value: 'y' }]
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<span>0-x</span><span>1-y</span>')
+      }).then(done)
+    })
+  }
+
   it('should render an Object', done => {
     const vm = new Vue({
       template: `