vue-fragment icon indicating copy to clipboard operation
vue-fragment copied to clipboard

"TypeError: Cannot read property 'insertBefore' of null"

Open OmgImAlexis opened this issue 6 years ago • 13 comments

I'm still getting the `` error with the latest version of this. I did notice most of the other instances of this error have gone away.

index.js?1ef8:2178 [Vue warn]: Error in directive fragment inserted hook: "TypeError: Cannot read property 'insertBefore' of null"

found in

---> <Fragment>
       <HelpLink> at components/helpers/help-link.vue
         <DeviceList> at components/device-list/index.vue
           <Home> at components/home/index.vue
             <DetectNetwork> at components/helpers/detect-network.vue
               <App> at components/app.vue
                 <Root>

index.js?1ef8:2178 TypeError: Cannot read property 'insertBefore' of null
    at inserted (vue-fragment.esm.js?3f08:1)
    at callHook$1 (vue.runtime.esm.js?2b0e:6289)
    at callInsert (vue.runtime.esm.js?2b0e:6228)
    at wrappedHook (vue.runtime.esm.js?2b0e:2076)
    at Object.invoker [as insert] (vue.runtime.esm.js?2b0e:2019)
    at invokeInsertHook (vue.runtime.esm.js?2b0e:5956)
    at VueComponent.patch [as __patch__] (vue.runtime.esm.js?2b0e:6175)
    at VueComponent.Vue._update (vue.runtime.esm.js?2b0e:2666)
    at VueComponent.updateComponent (vue.runtime.esm.js?2b0e:2784)
    at Watcher.get (vue.runtime.esm.js?2b0e:3138)
    at Watcher.run (vue.runtime.esm.js?2b0e:3215)
    at flushSchedulerQueue (vue.runtime.esm.js?2b0e:2977)
    at Array.eval (vue.runtime.esm.js?2b0e:1833)
    at flushCallbacks (vue.runtime.esm.js?2b0e:1754)

This is my component with the issue. I've commented out the section where the error is coming from. If I use the v-else that's not commented out I get no errors and everything works fine. There's a very good chance I'm just using this incorrectly.

<template>
	<component v-if="tag && !tagExcluded" :is="tag">
		<component :is="component">
			<slot></slot>
		</component>
	</component>
	<component v-else-if="tag && tagExcluded" :is="component">
		<slot></slot>
	</component>
	<div v-else style="color:red;">{{ component }}</div>
	<!-- <component v-else :is="component">
		<slot></slot>
	</component> -->
</template>

<script>
/**
 * @name HelpLink
 * @description Open help box on click
 * @prop {string} [name='*']
 * @prop {boolean} [exclude=true]
 * @prop {boolean} [forceExclude=false]
 * @prop {string} [tag]
 */
export default {
	name: 'help-link',
	data() {
		return {
			show: false,
			el: null
		};
	},
	props: {
		name: {
			type: String,
			default: '*'
		},
		exclude: {
			type: Boolean,
			default: true
		},
		forceExclude: {
			type: Boolean,
			default: false
		},
		tag: {
			type: String
		}
	},
	methods: {
		handler() {
			this.show = !this.show;
			this.$bus.emit(`help:${this.name}:${this.show ? 'show' : 'hide'}`);
		}
	},
	computed: {
		tagExcluded() {
			const { exclude } = this
			const excluded = [
				'table',
				'thead',
				'tbody',
				'td',
				'tr',
				'button'
			];

			return this.forceExclude || (this.exclude && excluded.includes(this.$slots.default[0].tag));
		},
		component() {
			if (this.tagExcluded) {
				return 'fragment';
			}
			return 'a';
		}
	},
	created() {
		// Set key to name
		this.$vnode.key = this.name;
	},
	mounted() {
		// Set cursor
		this.$el.style.cursor = 'help';

		// Add emitter to click event
		this.$el.addEventListener('click', this.handler);
	},
	beforeDestroy() {
		this.$el.removeEventListener('click', this.handler);
	}
}
</script>

OmgImAlexis avatar Dec 09 '18 00:12 OmgImAlexis

I am pretty sure this is the example that's causing the issue with the commented out v-else.

<thead>
    <help-link :name="`${type}Help`">
        <td>Device</td>
        <td>Identification</td>
        <td>Temp.</td>
        <td>Reads</td>
        <td>Writes</td>
        <td>Errors</td>
        <td>FS</td>
        <td>Size</td>
        <td>Used</td>
        <td>Free</td>
        <td>View</td>
    </help-link>
</thead>

OmgImAlexis avatar Dec 09 '18 00:12 OmgImAlexis

There's a very good chance I'm just using this incorrectly.

If so, then maybe i can improve documentation.


A little review of your component to begin with :

  1. you don't need mounted() and beforeDestroy() : you can use @click and <style> with <component />. see http://jsfiddle.net/rL10ewnf/1/ for a working example.

  2. a better design for your handler() is to make the eventbus dispatch when reacting to show's new value :

    watch: {
      show(newVal) {
        this.$bus.emit(`help:${this.name}:${newVal ? 'show' : 'hide'}`);
      }
    }
    
    // ...
    
    methods {
      handler() { this.show = !this.show }  
    }
    

    The benefit is that, if someone makes writes <help-link ref="helpLink">, then they can do this.$refs.helpLink.show = true or this.$refs.helpLink.handler() and your component will not misbehave. Also, because the handler() becomes a one-liner, you could do @click="show = !show" in your template instead and save some lines.

  3. You're twitching key and name attributes for no reason : you define name, but use it to set the key ; why not using key directly ? You can remove @prop {string} [name='*'] and then use this.$vnode.key instead. Of course, it means using key instead of name while using your component.

  4. few minor improvement in the syntax of the computed to make them look smaller such as ternary statements and separate enum definition.

  5. your template definition is highly complicated for no reason. it took me... 5 real minutes to understand it, which is more than the average of components i see on a daily basis. You can factorize the 2 latest cases (v-else-if and v-else), but even still, I don't know for sure what's the output for each case, and why would you do such a thing.

At the very least, your component should look more like :

<template>
  <component v-if="tag && !tagExcluded" :is="tag" class="helplink" @click="show = !show">
    <component :is="component"><slot /></component>
  </component>

  <component v-else :is="component"  class="helplink" @click="show = !show">
    <slot />
  </component>
</template>

<script>
/**
 * @name HelpLink
 * @description Open help box on click
 * @prop {boolean} [exclude=true]
 * @prop {boolean} [forceExclude=false]
 * @prop {string} [tag]
 */

export const EXCLUDED_TAGS = ['table', 'thead',	'tbody', 'td', 'tr', 'button'];

export default {
  name: 'help-link',

  props: {
    exclude: {
      type: Boolean,
      default: true
    },
    
    forceExclude: {
      type: Boolean,
      default: false
    },

    tag: String,

  data: () => ({ show: false })

  computed: {
    tagExcluded() {
      return this.forceExclude || (this.exclude && EXCLUDED_TAGS.includes(this.$slots.default[0].tag));
    },

    component() { return this.tagExcluded ? 'fragment' : 'a' }
  },

  watch: {
    show(newVal) {
      this.$bus.emit(`help:${this.$vnode.key}:${this.show ? 'show' : 'hide'}`);
    }
  },
}
</script>

<style>
.helplink {
  cursor: help;
}
</style>
<thead>
    <help-link :key="`${type}Help`">
        <td>Device</td>
        <td>Identification</td>
        <td>Temp.</td>
        <td>Reads</td>
        <td>Writes</td>
        <td>Errors</td>
        <td>FS</td>
        <td>Size</td>
        <td>Used</td>
        <td>Free</td>
        <td>View</td>
    </help-link>
</thead>

I honestly don't know what you try to accomplish with that. The way you write is testing for this.$slots.default[0].tag but you give an example with multiple children, meaning that this.$slots.default[n].tag will be ignored.

My assumption is that you want to add a click handler and a help cursor on the fly to a bunch of existing elements, and for such, you should probably use a render function or a directive instead.

y-nk avatar Jan 17 '19 08:01 y-nk

I get this error when running jest tests on my Vue components. It doesn't cause the test to fail, but lights up my terminal output. No errors show up when running in a browser, however.

Its a pretty standard Nuxt app, I haven't done much to change the standard jest setup so should be easily replicatable. Here's what it looks like when I run jest:

image

I can see its triggering from my OverviewPage component. The template looks like this:

<template>
  <Fragment>
    <Header
      :title="pageData.title"
      :subtitle="pageData.sub_title"
    />

    <StreamField :blocks="pageData.body" />
  </Fragment>
</template>

And noting that my StreamField component there also has a Fragment at the template root, and is reporting the same kind of errors.

My working knowledge of jest is not great so feeling a little over my head here, any help appreciated 🙏

tbredin avatar Aug 06 '20 07:08 tbredin

Additionally, thought I might mention that it appears that all of #42, #26, #24, and #18 are maybe all this same issue? Should they be merged into one spot? The error message is almost the same in each case:

TypeError: Cannot read property 'insertBefore' of null or TypeError: Cannot read property 'insertBefore' of Node

tbredin avatar Aug 06 '20 08:08 tbredin

I have the same problem as @tbredin . Does anyone know how to get around this situation?

hugo-cardoso avatar Aug 07 '20 16:08 hugo-cardoso

image

I found a functional approach. Fragment needs a parent to be rendered, and that's exactly what I do.

My component template: image

hugo-cardoso avatar Aug 07 '20 16:08 hugo-cardoso

I was having the same issue as @tbredin and @hugo-cardoso's solution worked wonders

Ribeiro-Tiago avatar Aug 28 '20 09:08 Ribeiro-Tiago

I have same: [Vue warn]: Error in mounted hook: "TypeError: Cannot read property 'insertBefore' of null

please help me ...

roborock avatar Oct 14 '20 10:10 roborock

@roborock upgrade to Vue 3, there's native support of fragments 🙏

y-nk avatar Nov 05 '20 16:11 y-nk

I personally solve this by using :tabindex in v-for so vue component know where insert it.

CutePotato avatar May 30 '21 15:05 CutePotato

I couldn't get @hugo-cardoso solution to work with a component that uses props. So I used the attachTo option instead:

const div = document.createElement('div')
div.id = 'root'
document.body.appendChild(div)

wrapper = mount(MyComponent,
  {
    attachTo: '#root',
    propsData: {
      propKey: propValue
    }
    // other options
  }
)

SebastianMelinat avatar Aug 06 '21 09:08 SebastianMelinat

Expanding on @SebastianMelinat's solution, one may also easily do

attachTo: document.body

as per the vue-test-utils docs and this comment in the vue-test-utils repo.

For me, this solved the problem completely using Vue2 and Nuxt2 and didn't require me to add any extra code.

dschreij avatar May 27 '22 14:05 dschreij

The attachTo trick (either variety) does not work well. It gets rids of the error, but it also makes sure the component is completely empty. When I log html() on the wrapper, it returns <div fragment="60bcdacce0"></div> and literally nothing more. The entire contents of the components are gone.

Since the error doesn't really affect the tests succeeding/failing, I thought about eating the error using a try..catch statement. But nope, even that doesn't work, because vue-fragment (or one of its dependencies) is doing a literal console.error(), which is eludes to bad design.

In component.js line 73 it blindly assumes parent to be defined, which is not good. Why this produces a console.error instead of a normal error to be thrown, is beyond me.

thany avatar Nov 23 '22 13:11 thany