DEV Community

Patrick Ahmetovic
Patrick Ahmetovic

Posted on • Originally published at patriscus.com

Non-Prop Attributes in Vue

During the implementation of a custom component, I explored the documentation to learn about a behavior that previously seemed a bit magical to me: Non-Prop Attributes.

Props in Vue

props are one of the ways you can implement component communication in Vue. It is pretty similar to React and provides an API to express the properties such as type, required, and default.

As an example, let's look at an input component that receives a prop named value.

// BaseInput.vue
<template>
  <div>
    <input v-bind="$attrs" :value="value">
  </div>
</template>

<script>
export default {
  name: 'BaseInput',
  props: {
    value: {
      type: String,
      required: true,
    },
  },
}
</script>
Enter fullscreen mode Exit fullscreen mode

And here's a possible usage:

// MyComponent.vue
<template>
  <BaseInput :value="foo" />
</template>
Enter fullscreen mode Exit fullscreen mode

Non-Prop Attributes

But what if you do this?

// MyComponent.vue
<template>
  <BaseInput :value="foo" :readonly="isReadOnly" />
</template>
Enter fullscreen mode Exit fullscreen mode

Here readonly is not defined in the props definition of BaseInput:

// BaseInput.vue
<script>
export default {
  // ...
  props: {
    value: {
      type: String,
      required: true,
    },
    // No definition for readonly
  },
  // ...
}
</script>
Enter fullscreen mode Exit fullscreen mode

So what happens with it?

In such cases, Vue adds these attributes to the $attrs object accessible as an instance property. In the example above, readonly gets added to the object as $attrs.readonly:

{
  "readonly": true
}
Enter fullscreen mode Exit fullscreen mode

Attribute Inheritance

Attribute inheritance (and disabling it) can leverage the usage of Non-Prop attributes. By default, attributes are added to the root element of your template. For example:

// BaseInput.vue
<template>
  <div>
    <input type="text" />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode
// MyComponent.vue
<BaseInput data-testid="my-component-id" />
Enter fullscreen mode Exit fullscreen mode

Will result in:

<div data-testid="my-component-id">
  <input type="text">
</div>
Enter fullscreen mode Exit fullscreen mode

Using inheritAttrs: false, we can opt-out of this default behavior to tell Vue that we are explicitly going to bind the attributes where we need to. In the example above, we may want to set the input element's attributes rather than the outer div. To do that, we add v-bind="$attrs" to the input element.

// BaseInput.vue
<template>
  <div>
    <input type="text" v-bind="$attrs" />
  </div>
</template>

<script>
export default {
  inheritAttrs: false,
  // ...
}
</script>
Enter fullscreen mode Exit fullscreen mode

Now, <BaseInput data-testid="my-component-id" /> will result in:

<div>
  <input type="text" data-testid="my-component-id">
</div>
Enter fullscreen mode Exit fullscreen mode

That is especially useful when all the possible attribute keys are either unknown beforehand or tedious to manually define as a prop. For instance, an input element can have a multitude of different attributes such as value, disabled, type, readonly, and more. Another example would be to add data-testid (or other data attributes) for testing purposes or integration with 3rd party libraries.

Note: In Vue 2, the class and style attributes are not added to $attrs. That means that despite using inheritAttrs: false, these two attributes are added to the root element no matter what. This behavior changed in Vue 3 by adding them to $attrs as well.

Discussion (0)