How to remove attributes from tags inside Vue components?


#1

(This is a re-post of this stackoverflow.com question.)

I want to use data-test attributes (as suggested here), so when running tests I can reference tags using these attributes.

<template>
    <div class="card-deck" data-test="container">content</div>
</template>

The element will be found using:

document.querySelector('[data-test="container"]')
// and not using: document.querySelector('.card-deck')

But I don’t want these data-test attributes to get to production, I need to remove them. How can I do that? (There are babel plugins that do that for react.)

Thanks!


#2

You could make a custom directive:

if(process.env.NODE_ENV === 'development'){
  Vue.directive('test-ref', {
    bind(el, binding){
      el.dataset.test = binding.arg
      //and you could do other useful stuff here too!
    }
  })
} else {
  Vue.directive('test-ref', {})
}

And to use it:

<div v-test-ref:container>

#3

@Herteby good solution to get rid of the attributes during runtime, but it won’t really improve code size, since we now simly have a reference to the directive in the render function instead of a hardcoded attribute.

One could come up with a little webpack-loader that cleans the template …

Edit: I think vue-loader can be configured to do that through compilerModules:

https://vue-loader.vuejs.org/en/options.html#compilermodules

looks complicated but a fun challenge …

It might be as simple as something like this:

compilerModules: [{
  preTransformNode(astEl) {
    if (process.env.NODE_ENV === 'production') {
      const { attrsMap, attrsList } = astEl
      if (attrsMap['data-test']) {
        delete attrsMap['data-test']
        const index  = _.findIndex(attrsList, x => x.name == 'data-test' )
        attrsList.splice(index, 1)
      }
    }
    return astEl
  }
}]

should really flesh this out and make it a little package

Edit: corrected a typo after feedback below.


#4

Thanks, it worked.
(I can still find the attribute in .nuxt/dist/server-bundle.json, but not in what gets to the client, so it seems to be ok.)

For anyone having the same issue, note there’s a typo in @LinusBorg code (it is preTransformNode, not preTranformNode) and what I did for a nuxt.js project was, in nuxt.config.js:

module.exports = {
  // ...
  build:   {
    // ...
    extend(config, ctx) {
      // ...
      const vueLoader = config.module.rules.find(rule => rule.loader === 'vue-loader')
      vueLoader.options.compilerModules = [{
        preTransformNode(astEl) {
          if (!ctx.dev) {
            const {attrsMap, attrsList} = astEl
            tagAttributesForTesting.forEach((attribute) => {
              if (attrsMap[attribute]) {
                delete attrsMap[attribute]
                const index = attrsList.findIndex(x => x.name === attribute)
                attrsList.splice(index, 1)
              }
            })
          }
          return astEl
        },
      }]
    }
  }
}

tagAttributesForTesting is an array holding all attributes you want to remove. Please note that if you dynamically bind the attribute (if you use the v-bind directive and its shortcut :), tagAttributesForTesting should be like ["data-test", ":data-test", "v-bind:data-test"].


#5

Wow, great to hear that actually worked, I just typed down the code and doesn’t have a chance to actually test it yet :metal::sweat_smile:


#6

Well… binary speaking, your code was a disaster, it didn’t work, I had to fix that damn typo :stuck_out_tongue:

BTW, Thanks for the solution.


#7

:joy::joy::joy: you’re welcome


#8

For anyone using vue-cli 3.0:

In vue.config.js

For < vue-cli 3.0.0-beta.10:

module.exports = {
  // ...
  vueLoader: {
    compilerModules: [
      {
        preTransformNode(astEl) {
          if (process.env.NODE_ENV === "production") {
            const { attrsMap, attrsList } = astEl;
            if (attrsMap["data-test"]) {
              delete attrsMap["data-test"];
              const index = attrsList.findIndex(
                x => x.name === "data-test"
              );
              attrsList.splice(index, 1);
            }
          }
          return astEl;
        }
      }
    ]
  }
  // ...
};

For vue-cli >= 3.0.0-beta.10:

module.exports = {
  // ...
  chainWebpack: config => {
        config.module
            .rule('vue')
            .use('vue-loader')
            .tap(options => {
                options.compilerOptions.modules = [
                    {
                        preTransformNode(astEl) {
                            if (process.env.NODE_ENV === 'production') {
                                const { attrsMap, attrsList } = astEl;
                                if (attrsMap['data-test']) {
                                    delete attrsMap['data-test'];
                                    const index = attrsList.findIndex(x => x.name === 'data-test');
                                    attrsList.splice(index, 1);
                                }
                            }
                            return astEl;
                        }
                    }
                ];
                return options;
            });
    }
  // ...
};

Thanks guys! :grinning:


#9

for vue loader 15.2.4

        test: /\.vue$/,
        loader: "vue-loader",
        options: {
          loaders: {
            // some loader options
          },
          compilerOptions: {
            modules: [
              {
                preTransformNode(astEl) {
                  if (process.env.NODE_ENV === "production") {
                    const { attrsMap, attrsList } = astEl;
                    if (attrsMap["data-test"]) {
                      delete attrsMap["data-test"];
                      const index = _.findIndex(attrsList, x => x.name == "data-test"); // note you need Lodash
                      attrsList.splice(index, 1);
                    }
                  }
                  return astEl;
                }
              }
            ]
          }
          // other vue-loader options go here
        }
      }

#10

Thanks for the answers, but I have a confusion now. If I add the configuration and run the command npm run test:e2e on a project created with @vue/cli 3.0.5 production mode kicks in and removes ‘data-test’ attributes on tags, which leads Cypress tests to fail (due to .get([data-test=...]) returns nothing).

If I run npm run serve in one terminal and run CYPRESS_baseUrl=http://localhost:8080 ./node_modules/cypress/bin/cypress open on another then tests are working as expected, but tests are bloated with XHR results and (I presume) due to HMR any change on application code won’t cause the tests to re-run (test code change does).
So a simple click to ‘Run all tests’ does the job, nonetheless having automated re-run would be awesome. After all HMR isn’t for that - to avoid page refresh on any change (I’m aware of it’s also for state-preserving and such).

What to do to have automated re-run of tests while following Cypress best-practices ([data-test=...]) and getting rid off those data-test attributes on production builds?
Thanks in advance.

Update

I’ve solved my problem by adding ‘.env.e2e’ file at the root of the project (as @LinusBorg suggested on this SO question), changing npm script for test and adding another condition to vue.config.js. Here are the additions;

# .env.e2e
NODE_ENV=production
LEAVE_TEST_TAG_ATTRS=true
"scripts": {
  "test:e2e": "vue-cli-service test:e2e --mode e2e"
}
// vue.config.js
// if (process.env.NODE_ENV === 'production') {
if (process.env.NODE_ENV === 'production' && process.env.LEAVE_TEST_TAG_ATTRS !== 'true') {
// remove attributes

I named the environment variable on purpose, it must be set to “true” (string) explicitly to leave [data-test=...] attributes intact. Which in the case of npm run test:e2e only. Other npm scripts works as expected; npm run serve leaves the attributes, npm run build removes them.

Also note even with this workaround any change of application code won’t cause the tests to re-run.


#11

And for a Nuxt 2 project:

// nuxt.config.js
module.exports = {
  // ...
  build:   {
    // ...
    extend(config, ctx) {
      // ...
      ctx.loaders.vue.compilerOptions = {
        modules: [{
          preTransformNode(astEl) {
            if (!ctx.isDev) {
              const {attrsMap, attrsList} = astEl
              tagAttributesForTesting.forEach((attribute) => {
                if (attrsMap[attribute]) {
                  delete attrsMap[attribute]
                  const index = attrsList.findIndex(x => x.name === attribute)
                  attrsList.splice(index, 1)
                }
              })
            }
            return astEl
          },
        }],
      }
    }
  }
}

#12

Has anyone configured this using the RollupJS plugin?