Non-Prop Attributes in Vue
Tags: vue, webdev
Published: 2021-02-23 10:50:12

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`.

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

<script>
export default {
  name: 'BaseInput',
  props: {
    value: {
      type: String,
      required: true,
    },
  },
}
</script>
```

And here's a possible usage:

```vue
// MyComponent.vue
<template>
  <BaseInput :value="foo" />
</template>
```

## Non-Prop Attributes

But what if you do this?

```vue
// MyComponent.vue
<template>
  <BaseInput :value="foo" :readonly="isReadOnly" />
</template>
```

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

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

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`:

```json
{
  "readonly": true
}
```

## 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:

```vue
// BaseInput.vue
<template>
  <div>
    <input type="text" />
  </div>
</template>
```

```vue
// MyComponent.vue
<BaseInput data-testid="my-component-id" />
```

Will result in:

```html
<div data-testid="my-component-id">
  <input type="text">
</div>
```

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.

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

<script>
export default {
  inheritAttrs: false,
  // ...
}
</script>
```

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

```html
<div>
  <input type="text" data-testid="my-component-id">
</div>
```

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.