fix(shared): class presence overrides
fix #13025 for class
Match behavior of style overriding present in normalizeStyle for normalizeClass.
Example: <div class="foo" :class="{ foo: false }"></div> -> <div class=""></div>
I have assumed that the discrepancy between class and style is a bug hence listing this as a fix.
Size Report
Bundles
| File | Size | Gzip | Brotli |
|---|---|---|---|
| runtime-dom.global.prod.js | 101 kB (+289 B) | 38.6 kB (+108 B) | 34.7 kB (+81 B) |
| vue.global.prod.js | 159 kB (+289 B) | 58.7 kB (+119 B) | 52.2 kB (+54 B) |
Usages
| Name | Size | Gzip | Brotli |
|---|---|---|---|
| createApp (CAPI only) | 46.9 kB (+290 B) | 18.3 kB (+98 B) | 16.8 kB (+82 B) |
| createApp | 54.9 kB (+290 B) | 21.4 kB (+95 B) | 19.5 kB (+89 B) |
| createSSRApp | 59.2 kB (+290 B) | 23.1 kB (+103 B) | 21.1 kB (+81 B) |
| defineCustomElement | 59.9 kB (+290 B) | 23 kB (+113 B) | 20.9 kB (+80 B) |
| overall | 69 kB (+290 B) | 26.6 kB (+111 B) | 24.2 kB (+48 B) |
@vue/compiler-core
npm i https://pkg.pr.new/@vue/compiler-core@13911
@vue/compiler-dom
npm i https://pkg.pr.new/@vue/compiler-dom@13911
@vue/compiler-sfc
npm i https://pkg.pr.new/@vue/compiler-sfc@13911
@vue/compiler-ssr
npm i https://pkg.pr.new/@vue/compiler-ssr@13911
@vue/reactivity
npm i https://pkg.pr.new/@vue/reactivity@13911
@vue/runtime-core
npm i https://pkg.pr.new/@vue/runtime-core@13911
@vue/runtime-dom
npm i https://pkg.pr.new/@vue/runtime-dom@13911
@vue/server-renderer
npm i https://pkg.pr.new/@vue/server-renderer@13911
@vue/shared
npm i https://pkg.pr.new/@vue/shared@13911
vue
npm i https://pkg.pr.new/vue@13911
@vue/compat
npm i https://pkg.pr.new/@vue/compat@13911
commit: fe15e02
This change adds quite a few bytes to the build. Not necessarily a problem, but it's something I think we need to take into account when deciding whether this fix is worth it.
The underlying problem described in #13025 is interesting, but I don't think changing how we merge classes is the correct way to address that use case.
I think changing how normalizeClass behaves will break a lot of existing applications. It's common for elements to accrue classes from a variety of sources, including attribute inheritance.
This change would alter the semantics of falsey values from 'do nothing' to 'remove this class'. Currently, :class="{ a: b }" is equivalent to :class="b ? 'a' : ''". It's purely an additive process, adding the class a if required. It doesn't block that same class from being added by an earlier source.
class and style are fundamentally different and their handling reflects that. I don't see this as an inconsistency, they're just different.
I think the correct mental model for style is a map, with key/value pairs. Merging those maps needs a way to handle clashing keys. Browsers already need to handle this for HTML, e.g. style="color: red; color: blue" will use blue as the value for color. Our handling reflects that and gives priority to the last value. We allow an object to be used with :style as its a natural fit for specifying a map.
The mental model for class is different, that behaves more like a set. When we merge those sets we essentially just take the union of those sets. (I'm glossing over duplicate classes, but I don't think they matter from the perspective of the mental model). When we use an object with :class it should be seen as a convenient way to specify one of those sets. Allowing the truthiness of the value to determine inclusion is useful for conditionals, but a falsey value is only intended to indicate that the key should not be added to current set. That information isn't supposed to be retained beyond the creation of the current set and shouldn't be taken into account when taking the union of the sets.
@skirtles-code Thanks for the feedback!
I completely agree about the map vs. set mental models for styles and classes, but I don't necessarily think that this set mental model is at odds with the class removal logic i.e. the mental model assumed by this PR is that there is also a set difference operation for the new set of falsy classes instead of only a union with the added ones.
From some testing I'm seeing that some other frameworks that have a class truthiness-inclusion feature also utilize this class removal logic on merging. Of course that's not to say that Vue should also follow suit just because other projects are doing it or that their behavior is inherently better, but just some data points about this removal logic not being too far-fetched:
- Angular via single-class binds and also with the
ngClassdirective - Svelte via class directives. It is a little different since the ordering of the directive vs the base class on the HTML element does not make a difference, but the override-removal behavior is still there
There are also some counterexamples I found to be fair:
- clsx is purely-additive when merging
-
classnames is also purely-additive (though the
classnames/dedupesubpackage does do removals on subsequent falsy values)
That being said, I understand the concern of potentially breaking existing applications with this change, especially if you otherwise feel that the value proposition of this PR isn't worth it. I suppose a compiler feature flag could be used to avoid the breaking change, but that has its own set of cons.
Do you have a different idea for how https://github.com/vuejs/core/issues/13025 should be addressed (or if it's even a problem at all)? If so then I'd be happy to rework this PR to accommodate for it!