emmet icon indicating copy to clipboard operation
emmet copied to clipboard

Support for css modules in jsx/tsx

Open gabriel-peracio opened this issue 4 years ago • 45 comments

Abbreviation Current Behavior Desired Behavior
.foo <div className="foo"></div> <div className={styles.foo}></div>

Sorry for reopening this issue, but #533 was closed by the author and I didn't want it to quietly disappear. I've wanted this for years and haven't found a clean solution yet. I took a look at the emmet source code and I can't think of a clean way to implement this myself.

@sergeche you mentioned you would try to implement this in an upcoming emmet version - did you manage to do it?

gabriel-peracio avatar Jun 17 '20 21:06 gabriel-peracio

In new implementation it’s supported as div[class={styles.foo}]. I’ll think about new syntax for such case: changing basic .foo is a very bad decision, maybe .{foo}?

sergeche avatar Jun 17 '20 21:06 sergeche

I was thinking this should be some kind of config, not really messing with the basic abbreviation itself (which I agree would be a disaster). I also think that creating a custom syntax that produces this behavior is undesirable, even if the syntax is extremely simple.

A very simple idea I had would be a user-provided post-expansion function, where it takes the expanded abbreviation as input, and produces an arbitrary string as output, that then replaces the expanded abbreviation. We already have the concept of filters/actions and the .json files, so there is some precedent

This could be generalized to cover many use cases, not just this one. For instance it would be relatively trivial to convert className="foo" to className={styles.foo} with some regex

gabriel-peracio avatar Jun 17 '20 21:06 gabriel-peracio

As an aside, my workaround for this issue is:

  1. Create a custom regex that replaces className="foo" with className={styles.foo}
  2. Create a macro that selects all text and replaces it using the regex above
  3. Whenever I expand something on emmet, I also run my script to "fix" it

This is obviously an ugly hack that has a bazillion problems (such as trashing your cursor position and needing to be run manually instead of after expansion)

gabriel-peracio avatar Jun 17 '20 22:06 gabriel-peracio

In new Emmet, abbreviation is parsed into AST so you can walk over abbreviation nodes and rewrite it as you like. The problem is that Emmet has multiple implementations: in JS and Python (also unofficial Java implementation in WebStorm) so there’s no single solution. A custom option might work here but for now it will be applied globally to Emmet, not per-project

sergeche avatar Jun 17 '20 22:06 sergeche

A custom option that is applied globally would work for me. I mean vscode already has workspace-specific customizability for emmet (AFAIK) so I would still reap the benefits.

Also, even though I need emmet to expand .foo to <div class="foo"></div> in one project (django templates) and <div className={styles.foo}></div> in another (react app), it is very rare that I switch from one project to another in the same programming session, so I could live with having to manually change the setting back and forth.

A way to hook into the AST and modify the nodes myself would also be perfectly fine, I imagine I could write the transformer easily enough

Is the transformation from class to className for jsx/tsx in the new emmet using the AST transformer you mentioned, or is it still handled as a special case as in the current implementation?

gabriel-peracio avatar Jun 17 '20 22:06 gabriel-peracio

This setting would affect JSX context only, e.g. your HTML and Django templates would behave as usual. Also, new Emmet will use web-based UI for config, although JSON config in editor might still be possible

sergeche avatar Jun 17 '20 22:06 sergeche

This feature will be super useful for me too. thanks

dimagimburg avatar Jun 18 '20 20:06 dimagimburg

Absolutely need this feature. Also div.{foo} sounds great 🙌

tusharsadhwani avatar Jul 14 '20 09:07 tusharsadhwani

How about div..foo? It would be super fast to type.

svobik7 avatar Aug 08 '20 08:08 svobik7

@svobik7 sounds good. I guess the only possible value here is a literal? You won't type, like, string here? Something like div.{"my class"}?

sergeche avatar Aug 08 '20 08:08 sergeche

@sergeche sure. If multiple classNames are needed it could work same as single dot statement:

  1. div..foo --> <div className={styles.foo}>
  2. div..foo..bar --> <div className={${styles.foo} ${styles.bar}}></div>

But I think that the second use-case is out of the scope of this issue for now.

svobik7 avatar Aug 08 '20 09:08 svobik7

@svobik7 sounds good. I guess the only possible value here is a literal? You won't type, like, string here? Something like div.{"my class"}?

I guess you could, since kebab class names have to be transformed to <div className={styles["my-class"]}/>, but that is a bit exotic. I'm fine without it.

The multiple classes would be nice to have but I can live without. I'd also prefer it transformed to <div className={cn(styles.foo, styles.bar)}/> but at that point you might as well write a plugin

gabriel-peracio avatar Aug 08 '20 22:08 gabriel-peracio

Heres an extension that I found that converts to css modules syntax. Found this to be the best solution until we get emmet support.

https://marketplace.visualstudio.com/items?itemName=jingxiu.CSS-Modules-transform

DivMode avatar Aug 20 '20 06:08 DivMode

Excuse me, is this problem being handled?

BTW, is there any way to uses styleName instead of className ? The styleName is from https://github.com/gajus/babel-plugin-react-css-modules.

Abbreviation Current Behavior Desired Behavior
.foo <div className="foo"></div> <div styleName="foo"></div>

hzmming avatar Oct 05 '21 03:10 hzmming

Any official progress?

oychao avatar Dec 13 '21 03:12 oychao

@oychao not yet, but thanks for reminding me, will take a look

sergeche avatar Dec 13 '21 09:12 sergeche

@sergeche Any chance I may remind you of this once again? Would really appreciate the feature!

Freakspot avatar Apr 28 '22 19:04 Freakspot

Just imagine how many people are writing react jsx/ tsx on daily basis. How many of them are using module css? Probably a lot.

And the best way I could find thus far in VSCode is to write div.{styles.myClass which turns into the complete <div className={styles.myClass}>.

Now I am also getting a warning when using NX monorepo scaffolding to generate libs in TypeScript that the above should be written as <div className={styles['myClass']}> which I cannot get with emmet at all.. it's a pain :) In order to at least get rid of the lint warning for the time being, we can remove "noPropertyAccessFromIndexSignature": true from the tsconfig of the library.

Hopefully emmet will properly support module css in react at some point.

snoop088 avatar May 14 '22 12:05 snoop088

Would be cool to be able to customize things like this with plugins. Then using styleName prop with react-css-modules would be easy, just replacing className with styleName. Or someone could use other name for css import, not just styles.

lunavod avatar May 18 '22 20:05 lunavod

Hi man, have you some news ? 🙄

n1ckfedorov avatar Aug 01 '22 00:08 n1ckfedorov

Any progress??

HappyMajor avatar Aug 09 '22 19:08 HappyMajor

There’s already some basic JSX support. Let’s sum up what features should be implemented:

  1. Support new .. syntax in JSX to produce styleName instead of className attribute.
  2. Add new option to configure prefix for styleName, e.g. jsx.styleNamePrefix: "style". If given, the class name provided after .. will be prefixed with given prefix object notation: div..foo<div styleName={style.foo}>, div..foo-bar<div styleName={style['foo-bar']}>.

That’s right?

sergeche avatar Aug 09 '22 19:08 sergeche

and for vue 😄

<div :class="$style.icon">
</div>

kikyous avatar Aug 16 '22 06:08 kikyous

I just pushed update (not released yet) which adds the following:

  • Option to override attribute name (in any syntax, not just JSX). E.g. you can map "class": "className" so when you write div[class=123], <div className="123"> will be outputted.
  • Option to add value prefix to any attribute. If prefix is specified, it considered as prefix in object notation and automatically converts value type to expression (in JSX-like syntaxes value it will be outputted as attr={...}).
  • Multiple consequent shorthand attributes (e.g. ## and ..) now has special internal property which can has its own attribute mapping with * suffix, for example: { "class": "className", "class*": "styleName" }. So .foo<div className="foo">, ..foo<div styleName="foo">.

Summing up, by default Emmet will work as follows:

equal(expand('.bar', { syntax: 'jsx' }), '<div className="bar"></div>');
equal(expand('..bar', { syntax: 'jsx' }), '<div styleName={styles.bar}></div>');
equal(expand('..foo-bar', { syntax: 'jsx' }), '<div styleName={styles[\'foo-bar\']}></div>');

equal(expand('.foo', { syntax: 'vue' }), '<div class="foo"></div>');
equal(expand('..foo', { syntax: 'vue' }), '<div :class="foo"></div>');

See https://github.com/emmetio/emmet/blob/master/test/expand.ts#L106-L114

And you can customize attribute mapping and value prefixes with markup.attributes and markup.valuePrefix options. Here’s how it’s done in default config:

jsx: {
    options: {
        'jsx.enabled': true,
        'markup.attributes': {
            'class': 'className',
            'class*': 'styleName',
            'for': 'htmlFor'
        },
        'markup.valuePrefix': {
            'class*': 'styles'
        }
    }
},
vue: {
    options: {
        'markup.attributes': {
            'class*': ':class',
        }
    }
}

https://github.com/emmetio/emmet/blob/master/src/config.ts#L390-L409

If this is the expected behaviour, I’ll release it as next version

sergeche avatar Aug 17 '22 09:08 sergeche

@sergeche that looks great. Considering most projects exclusively use one strategy for component classes it might make sense to let folks re-map the default, single period behavior (div.bar). But honestly, it's one extra keystroke and if the extra configuration doesn't feel right then what you have there is totally great.

vpicone avatar Oct 05 '22 21:10 vpicone

With this update, you’ll be able to re-map . operator for entire JSX

sergeche avatar Oct 06 '22 07:10 sergeche

I've always used <div className={styles.bar}></div>, never seen styleName before... So in my case I'd do

jsx: {
    options: {
        'jsx.enabled': true,
        'markup.attributes': {
            'class': 'className',
            'class*': 'className',
            'for': 'htmlFor'
        },
        'markup.valuePrefix': {
            'class*': 'styles'
        }
    }
},

right?

Also it would be cool if we could map ..foo.bar to <div className={cx(styles.foo, styles.bar)}></div> as well (for classnames or clsx support) but maybe I'm asking for too much haha

gabriel-peracio avatar Nov 10 '22 18:11 gabriel-peracio

@gabriel-peracio Yes, your example is correct.

As for <div className={cx(styles.foo, styles.bar)}></div> output – might be possible if it’s a commonly used feature

sergeche avatar Nov 14 '22 09:11 sergeche

@sergeche How can i customize attribute where can i find that?

msobkyy avatar Dec 02 '22 16:12 msobkyy

@msobkyy it’s not released yet. Will report here as soon as new version comes out

sergeche avatar Dec 05 '22 07:12 sergeche