简体   繁体   中英

How to set className without {styles.red} in Next.js

Description

I want to use only the pure name of the class without {styles.class-name} convention in Next.js, I google it and find I need to configure the next.config.js file. So, have someone good references for this?

I have this:

And it works fine in Next.js

第一

And need this:

This is not working by default in Next.js

第二

Next.js has globals.css file and it's in styles folder so you can make:

className="red"

and then in globals.css file:

.red{
//...
}

But I recommend you to use .module.css files because it's cleaner and doesn't make a mess.

UPDATE:

I hadn't checked this earlier. You can directly use babel-plugin-react-css-modules .


Ignore the content below

I had earlier wrote a Babel plugin for this. It is not robust, can be improved, but does the job:

// babel-plugin-cx.js

const __path = require('path');

const plugin = ({ types: t }) => {
  const cloneNode = t.cloneNode || t.cloneDeep;

  return {
    name: 'babel-plugin-cx',
    visitor: {
      Program: {
        enter(path, state) {
          state.stylesIdentifier = path.scope.generateUidIdentifier('styles');
        },

        exit(path, state) {
          if (state.hasProp) {
            const importDeclaration = t.importDeclaration(
              [t.importDefaultSpecifier(state.stylesIdentifier)],
              t.stringLiteral(
                __path
                  .parse(state.file.opts.filename)
                  .name.toLowerCase()
                  .replace(/^([^]*)$/, state.opts.pathReplace),
              ),
            );

            path.node.body.unshift(importDeclaration);
          }
        },
      },

      JSXAttribute(path, state) {
        if (path.node.name.name !== state.opts.propName) return;

        if (
          state.opts.ignoredElements.includes(
            path.findParent((p) => p.isJSXOpeningElement()).node.name.name,
          )
        )
          return;

        path.node.name.name = 'className';
        const value = path.get('value');

        if (value.isLiteral()) {
          value.replaceWith(
            t.jsxExpressionContainer(
              t.memberExpression(
                cloneNode(state.stylesIdentifier),
                cloneNode(value.node),
                true,
                false,
              ),
            ),
          );

          state.hasProp = true;
        } else if (value.isJSXExpressionContainer()) {
          const expression = value.get('expression');
          expression.replaceWith(
            t.memberExpression(
              cloneNode(state.stylesIdentifier),
              cloneNode(expression.node),
              true,
              false,
            ),
          );

          state.hasProp = true;
        }
      },

      JSXSpreadAttribute(path, state) {
        if (
          state.opts.ignoredElements.includes(
            path.findParent((p) => p.isJSXOpeningElement()).node.name.name,
          )
        )
          return;

        const argument = path.get('argument');
        if (!argument.isObjectExpression()) return;
        const properties = argument.get('properties');

        for (const property of properties) {
          if (property.node.key.name === state.opts.propName) {
            property.node.key.name = 'className';
            const value = property.get('value');
            value.replaceWith(
              t.memberExpression(
                cloneNode(state.stylesIdentifier),
                cloneNode(value.node),
                true,
                false,
              ),
            );

            state.hasProp = true;
          }
        }
      },
    },
  };
};

module.exports = plugin;
// .babelrc

{
  "presets": ["next/babel"],
  "plugins": [
    [
      "./babel-plugin-cx",
      {
        "pathReplace": "./$1.module.css",
        "propName": "cx",
        "ignoredElements": ["circle", "ellipse", "radialGradient"]
      }
    ]
  ]
}

If using Typescript, you need to add this (also add this file to includes array in tsconfig.json :

// custom.d.ts

import type * as React from 'react';

declare module 'react' {
  // eslint-disable-next-line @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-unnecessary-qualifier
  interface HTMLAttributes<T> extends React.DOMAttributes<T> {
    cx?: string;
  }
}

After this, you can simply write:

<div cx="red">Red text!</div>

There is no need to import the module file. It will be automatically imported if necessary from the specified "pathReplace" option. ( $1 indicates the file name using the cx attribute).

pathReplace example:

@styles/_$1.module.scss : if file using cx is Component.tsx then the styles will be imported from module @styles/_component.module.scss .

You can configure the propName using the provided option. You will also need to change next-env.d.ts accordingly. And, change the ignoredElements option accordingly, as your attribute name may be same as some attribute defined in the JSX/HTML standard.

Caveats:

  • The plugin currently completely ignores className attribute if cx is set on the element/component, ie, if you need to merge with some global class name then use className={`global ${styles.red}`} .

  • More than one class in cx is not supported. Can be implemented easily, but I was quite lazy.

  • Spread attribute is only partially supported:

     // supported: <div {...{cx: "red", other: "attribute"}}>Red text!</div> // not supported: const attrs = {cx: "red", other: "attribute"}; <div {...attrs}>Red text!</div>

    You may like to configure this rule in ESLint react/jsx-props-no-spreading .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM