Rendering Vue Slots content to an iframe

by Andy You

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://unpkg.com/vue/dist/vue.js"></script>

<div id="app">
  <label for="">
    Change this to Change IFrame content <br>
    <input type="text" v-model="dynamicPart">  
  </label>
  
  <div>
   <i-frame class="my-frame">
    <test-child title="This is a Title from a Prop!">
      <p>This was rendered in an iframe!</p>
      <p>Content of Input: <strong>{{dynamicPart}}</strong></p>
      <button @click="show = true">Click me to see events work, too!</button>
    </test-child>
  </i-frame> 
  <div v-if="show">
    <p>This paragraph appeared because of the button click in the iframe! </p>
  </div>
  </div>
  
</div>

SCSS

body {
  font-family: sans-serif;
}
.my-frame {
  border: 1px solid #CCC;
  box-shadow: 0 0 3px 2x rgba(0,0,0,.3);
  margin: 20px auto;
  height: 200px;
  width: 95%;
}

Babel + JSX

Vue.component('i-frame', {

  render(h) {
    return  h('iframe', {
    	on: { load: this.renderChildren }
    })
  },
  beforeUpdate() {
    //freezing to prevent unnessessary Reactifiation of vNodes
    this.iApp.children = Object.freeze(this.$slots.default)
  },  
  methods: {
    renderChildren() {
      const children = this.$slots.default
      const body = this.$el.contentDocument.body      
      const el = document.createElement('DIV') // we will mount or nested app to this element
      body.appendChild(el)

      const iApp = new Vue({
      	name: 'iApp',
        //freezing to prevent unnessessary Reactifiation of vNodes
        data: { children: Object.freeze(children) }, 
        render(h) {
          return h('div', this.children)
        },
      })

      iApp.$mount(el) // mount into iframe

      this.iApp = iApp // cache instance for later updates


    }
  }
})

Vue.component('test-child', {
  template: `<div>
    <h3>{{ title }}</h3>
    <p>
      <slot/>
    </p>
  </div>`,
  props: ['title'],
  methods: {
    log:  _.debounce(function() {
      console.log('resize!')
    }, 200)
  },
  mounted() {
    this.$nextTick(() => {
    	const doc = this.$el.ownerDocument
      const win = doc.defaultView
      win.addEventListener('resize', this.log)
    })
  },
  beforeDestroy() {
  	const doc = this.$el.ownerDocument
    const win = doc.defaultView
    win.removeEventListener('resize', this.log)
  }
})

new Vue({
  el: '#app',
  data: {
    dynamicPart: 'InputContent',
    show: false,
  }
})