Page MenuHomePhabricator

[SPIKE] Determine implementation path for CSS-only components
Closed, ResolvedPublic5 Estimated Story PointsSpike

Description

Background

There are multiple ways we could share styles to support CSS-only components. We need to determine what's best for our users and ecosystem.

Exploration

CSS class approach

Other frameworks typically distribute CSS styles with pre-defined classes (Bootstrap's .btn, .btn-primary, etc). This approach offers good ease-of-use, but creates some coupling between the markup and the framework styles.]

Considerations:

  • Consistent with the approach used in other systems
  • Especially for simple components, quick and easy to use
  • Users must stick with the pre-defined markup and selectors, meaning less flexibility and potential difficulty for components with a lot of markup
  • Ease of use might lead to lots of use, which could make breaking changes become disruptive and difficult to manage
  • Ease of use and ubiquity might lead to misuse, which happened with mediawiki.ui. However, once Codex is fairly ubiquitous in MediaWiki, this could happen anyway (e.g. someone could slap the .cdx-button class on something as long as the Button component is loaded on the page somewhere)
  • Requires some creative problem solving in terms of class names. Right now, Codex Vue components follow the structure .cdx-component-name, which should be the base component selector. We may need to repurpose these names for the CSS-only version, then add .cdx-component-name--vue or .cdx-component-name--hydrated or some other variation for styles that only apply to the Vue version (e.g. interactivity). This could get especially hairy for components where the CSS-only version is very different than the Vue version (like Select)
  • We would need to figure out how to export these styles for use (possibly in a new package)

Less mixin approach

An alternative approach would be to distribute Codex styles as pure LESS mixins, where the consumer decides where the styles should ultimately be applied. This approach is described more in Codex ADR 04.

Pros and cons:

  • For more complex components, dev users wouldn't be beholden to a specific markup structure. Instead, they could apply mixins to whichever elements they choose.
  • This is in line with how we chose to share styles for base elements (see the ADR linked above)
  • Avoiding double-loading styles would be difficult. If the mixins are applied to different selectors throughout different problems (not to mention within Codex Vue components that are then loaded on the page), would it be possible to consolidate the styles so they're not loaded multiple times?
  • Not as straightforward to use

Open questions

  1. Which approach has the fewest or the most easily mitigated performance issues? See T323180.
  2. Which approach will result in the lowest maintenance burden?
  3. Which approach will prevent misuse?
  4. Which approach is preferred by our developer users?
  5. Is there room for implementing both approaches, e.g. creating and releasing mixins, but then also applying those mixins to standard classes?

Proposal: Class-based approach plus OOUI-style sub-packages

  1. Use cdx- classes for styling ("Bootstrap-style" approach). We are already writing BEM-style CSS inside our Vue components, so we don't need to make any changes here.
  2. Continue writing styles within Vue single-file components (SFCs). Where necessary, component styles will start making distinctions between no-JS styles and JS-enabled ones by using a modifier class like --js-active. Components can add this class in the mounted() lifecycle hook. We can write a composition function that takes a component’s base class name as an argument and applies this modifier class on mount automatically. Grouping some styles under a --js-active class may also prove useful for SSR in the future. Example patch here: https://gerrit.wikimedia.org/r/c/design/codex/+/866509/
  3. Define a series of new Codex sub-packages which are intended for use within MediaWiki. Each new package will group together a set of related components (maybe corresponding to the component groups we were considering adding to the documentation site). This will work similar to how we are handling codex-search currently. Each new package will get built in Vite’s library-mode, so a single JS and CSS file gets built with only the data for the relevant subset of components. An example of this can be seen in this patch, which creates a demo codex-buttons package: https://gerrit.wikimedia.org/r/c/design/codex/+/865840
  4. Each Codex sub-package gets its own ResourceLoader module containing only its CSS and JS files. Users only need to load the packages corresponding to the components they need in their feature.
  5. Load CSS and JS separately: There are two ways we can do this.
    • We could update ResourceLoader so that a user can get only the styles, or only the scripts, or both, when using a module that contains both CSS and JS. Currently this is only possible through an annoying work-around.
    • Alternatively, we will have to do this work manually. Each Codex sub-package would need to have two modules in MW – one containing only the CSS, i.e. codex-buttons-styles, and one containing only the JS files, i.e. codex-buttons. The latter package would depend on the former, but the former can be used without the latter.
  6. Interaction with SSR: In the future, we expect that a more robust SSR system will be available in at least some situations. SSR-based features would basically create their own bundle instead of relying on the pre-split ones that we’d be providing here. Such builds will depend on the full Codex package (which contains all CSS and JS code). The SSR tooling will be able to tree-shake out all unnecessary code for these features, so the regular Codex modules will not be needed at all.
    • We still have some open questions about this (@AnneT feel free to share any here), but I don't think they are blockers to this work moving forward. For example, there may be some components like <CdxSelect> or <CdxIcon> that may need to look pretty different in a CSS-only implementation vs a SSR-one (even pre-hydration).
Future work:

In the system described here, users of CSS-only components would still be responsible for authoring the proper markup for their UI. This will likely be somewhat tedious for all but the simplest features. We could make this experience a lot better by providing one or both of the following:

  • Better templating language support on the server; it would be great if template authors could just include a {% cdx-button %} partial that can take arguments corresponding to component properties
  • Alternatively, a PHP interface could be provided similar to how OOUI-PHP works currently
Feasibility of this approach

Aside from the potential (small-ish) changes to ResourceLoader (see point 4 above), we can do all of this now. And actually most of the styles we need already exist in the Vue components themselves. Most of the work would just be a matter of reorganizing some of our current code so that it can be used in multiple ways going forward.

Risks/Trade-offs
  • This implementation strategy will allow us to keep writing our component styles within the Vue SFCs, which is nice from a familiarity and developer productivity approach.
  • We would need to create some additional packages inside the Codex repo, but this will mostly be a one-time task. As we add new components to Codex we'll need to include them in one of the existing sub-packages or (more rarely) create a new one. But little additional code is required here and once a sub-package has been set up it will rarely change. These will be included in each release just like codex-search is (though we may not need to publish them on NPM since they are intended only for use in MW). See https://gerrit.wikimedia.org/r/c/design/codex/+/866509/ for an example of what this would look like.
  • CSS-only implementations of components will need to be tested; I think that VueTest is a good place to do this (maybe a new sub-page of that extension can be created for the CSS-only components)
  • We'll need to create several additional ResourceLoader modules (especially if we cannot come up with a way to get just the CSS or JS from a module that contains both). But like in the case of the additional Codex packages, most of the work here is a one-time effort.
  • We might want to include some easily copy-pastable markup snippets for users in the absence of a good way to package up CSS-only components in mustache templates or PHP.

Overall I think this approach is pretty low-risk, and doesn't rely on any speculative or complex new technical work.

Even after a full SSR solution is available (which may take some time), I think that there will always be valid use-cases for a simple, no-build-step approach to server-rendering Codex-consistent UIs.


Acceptance criteria

  • DST completes proofs of concept if necessary
  • DST engineers discuss and agree on an approach
  • DST gets and applies feedback from developer stakeholders
  • DST agrees on the implementation path and documents the decision via an ADR

Event Timeline

Restricted Application added a subscriber: Aklapper. · View Herald Transcript
Restricted Application changed the subtype of this task from "Task" to "Spike". · View Herald TranscriptNov 15 2022, 11:19 PM
AnneT updated the task description. (Show Details)

Based on the draft acceptance criteria for this epic (see https://phabricator.wikimedia.org/T321351#8452801), @AnneT and I would like to present the following proposal for how we would actually do this work. If folks like this idea I'll move this up into the task description as the agreed-upon implementation strategy.

CSS-only components: Implementation strategy (draft)
  1. Use cdx- classes for styling ("Bootstrap-style" approach). We are already writing BEM-style CSS inside our Vue components, so we don't need to make any changes here.
  2. Continue writing styles within Vue single-file components (SFCs). Where necessary, component styles will start making distinctions between no-JS styles and JS-enabled ones by using a modifier class like --js-active. Components can add this class in the mounted() lifecycle hook. We can write a composition function that takes a component’s base class name as an argument and applies this modifier class on mount automatically. Grouping some styles under a --js-active class may also prove useful for SSR in the future. Example patch here: https://gerrit.wikimedia.org/r/c/design/codex/+/866509/
  3. Define a series of new Codex sub-packages which are intended for use within MediaWiki. Each new package will group together a set of related components (maybe corresponding to the component groups we were considering adding to the documentation site). This will work similar to how we are handling codex-search currently. Each new package will get built in Vite’s library-mode, so a single JS and CSS file gets built with only the data for the relevant subset of components. An example of this can be seen in this patch, which creates a demo codex-buttons package: https://gerrit.wikimedia.org/r/c/design/codex/+/865840
  4. Each Codex sub-package gets its own ResourceLoader module containing only its CSS and JS files. Users only need to load the packages corresponding to the components they need in their feature.
  5. Load CSS and JS separately: There are two ways we can do this.
    • We could update ResourceLoader so that a user can get only the styles, or only the scripts, or both, when using a module that contains both CSS and JS. Currently this is only possible through an annoying work-around.
    • Alternatively, we will have to do this work manually. Each Codex sub-package would need to have two modules in MW – one containing only the CSS, i.e. codex-buttons-styles, and one containing only the JS files, i.e. codex-buttons. The latter package would depend on the former, but the former can be used without the latter.
  6. Interaction with SSR: In the future, we expect that a more robust SSR system will be available in at least some situations. SSR-based features would basically create their own bundle instead of relying on the pre-split ones that we’d be providing here. Such builds will depend on the full Codex package (which contains all CSS and JS code). The SSR tooling will be able to tree-shake out all unnecessary code for these features, so the regular Codex modules will not be needed at all.
    • We still have some open questions about this (@AnneT feel free to share any here), but I don't think they are blockers to this work moving forward. For example, there may be some components like <CdxSelect> or <CdxIcon> that may need to look pretty different in a CSS-only implementation vs a SSR-one (even pre-hydration).
Future work:

In the system described here, users of CSS-only components would still be responsible for authoring the proper markup for their UI. This will likely be somewhat tedious for all but the simplest features. We could make this experience a lot better by providing one or both of the following:

  • Better templating language support on the server; it would be great if template authors could just include a {% cdx-button %} partial that can take arguments corresponding to component properties
  • Alternatively, a PHP interface could be provided similar to how OOUI-PHP works currently
Feasibility of this approach

Aside from the potential (small-ish) changes to ResourceLoader (see point 4 above), we can do all of this now. And actually most of the styles we need already exist in the Vue components themselves. Most of the work would just be a matter of reorganizing some of our current code so that it can be used in multiple ways going forward.

Risks/Trade-offs
  • This implementation strategy will allow us to keep writing our component styles within the Vue SFCs, which is nice from a familiarity and developer productivity approach.
  • We would need to create some additional packages inside the Codex repo, but this will mostly be a one-time task. As we add new components to Codex we'll need to include them in one of the existing sub-packages or (more rarely) create a new one. But little additional code is required here and once a sub-package has been set up it will rarely change. These will be included in each release just like codex-search is (though we may not need to publish them on NPM since they are intended only for use in MW). See https://gerrit.wikimedia.org/r/c/design/codex/+/866509/ for an example of what this would look like.
  • CSS-only implementations of components will need to be tested; I think that VueTest is a good place to do this (maybe a new sub-page of that extension can be created for the CSS-only components)
  • We'll need to create several additional ResourceLoader modules (especially if we cannot come up with a way to get just the CSS or JS from a module that contains both). But like in the case of the additional Codex packages, most of the work here is a one-time effort.
  • We might want to include some easily copy-pastable markup snippets for users in the absence of a good way to package up CSS-only components in mustache templates or PHP.

Overall I think this approach is pretty low-risk, and doesn't rely on any speculative or complex new technical work.

Even after a full SSR solution is available (which may take some time), I think that there will always be valid use-cases for a simple, no-build-step approach to server-rendering Codex-consistent UIs.

egardner changed the task status from Open to In Progress.Dec 8 2022, 11:10 PM
egardner claimed this task.

Thanks for writing this up! Initial thoughts:

  1. Continue writing styles within Vue single-file components (SFCs). Where necessary, component styles will start making distinctions between no-JS styles and JS-enabled ones by using a modifier class like --js-active. Components can add this class in the mounted() lifecycle hook. We can write a composition function that takes a component’s base class name as an argument and applies this modifier class on mount automatically. Grouping some styles under a --js-active class may also prove useful for SSR in the future. Example patch here: https://gerrit.wikimedia.org/r/c/design/codex/+/866509/

Agreed, this will be useful for SSR. I like this approach of keeping all the code in one place even if it fails to realize all possible performance gains. We could split no-JS styles from JS-enabled styles at a later stage if we want to, and in the meantime we could try to measure what proportion of our styles are JS-only.

  1. Define a series of new Codex sub-packages which are intended for use within MediaWiki. Each new package will group together a set of related components (maybe corresponding to the component groups we were considering adding to the documentation site). This will work similar to how we are handling codex-search currently. Each new package will get built in Vite’s library-mode, so a single JS and CSS file gets built with only the data for the relevant subset of components. An example of this can be seen in this patch, which creates a demo codex-buttons package: https://gerrit.wikimedia.org/r/c/design/codex/+/865840

This sounds like a good approach to me. However, I think it should be refined to address the fact that some of these packages will have overlap and/or have dependencies on each other. For example, codex-buttons contains the Icon component, but some other packages presumably would as well. If there's a package that contains the Dialog component, that would presumably depend on codex-buttons and get the Button (and Icon?) components from it, rather than including it separately, etc. Ideally I'd like the build of that latter package to externalize the codex-buttons package and not include its own copy of the Button component, but getting that working in the build system may be tricky. We should either find a way to make that work, or come up with a structure / division of components into sub-packages where we don't need this.

  1. Each Codex sub-package gets its own ResourceLoader module containing only its CSS and JS files. Users only need to load the packages corresponding to the components they need in their feature.
  2. Load CSS and JS separately: There are two ways we can do this.
    • We could update ResourceLoader so that a user can get only the styles, or only the scripts, or both, when using a module that contains both CSS and JS. Currently this is only possible through an annoying work-around.
    • Alternatively, we will have to do this work manually. Each Codex sub-package would need to have two modules in MW – one containing only the CSS, i.e. codex-buttons-styles, and one containing only the JS files, i.e. codex-buttons. The latter package would depend on the former, but the former can be used without the latter.

I think the latter would probably be the best approach to get started with for now. It would double the number of modules, but that would then motivate us to change RL so that this can be handled better.

  1. Interaction with SSR: In the future, we expect that a more robust SSR system will be available in at least some situations. SSR-based features would basically create their own bundle instead of relying on the pre-split ones that we’d be providing here. Such builds will depend on the full Codex package (which contains all CSS and JS code). The SSR tooling will be able to tree-shake out all unnecessary code for these features, so the regular Codex modules will not be needed at all.

That's an interesting feature of an SSR system that I hadn't thought of. I wouldn't take for granted that the SSR tooling could also do tree-shaking though. We should add that as a potential feature and explore how it could be implemented, but we might not be able to do it, or might not be able to do it at first, or might make it part of the build step project rather than the SSR project.

  • We still have some open questions about this (@AnneT feel free to share any here), but I don't think they are blockers to this work moving forward. For example, there may be some components like <CdxSelect> or <CdxIcon> that may need to look pretty different in a CSS-only implementation vs a SSR-one (even pre-hydration).

One bit of infrastructure that can help with this is having the composable that sets the --js-active class in the mounted() hook also expose a ref that indicates whether the component is mounted yet or not. This could then be used to have Select, Icon, etc. output different HTML pre-hydration vs post-hydration with v-if/v-else

Future work:

In the system described here, users of CSS-only components would still be responsible for authoring the proper markup for their UI. This will likely be somewhat tedious for all but the simplest features. We could make this experience a lot better by providing one or both of the following:

  • Better templating language support on the server; it would be great if template authors could just include a {% cdx-button %} partial that can take arguments corresponding to component properties
  • Alternatively, a PHP interface could be provided similar to how OOUI-PHP works currently

Agreed, it should be as easy as possible to use these things in MediaWiki, whatever shape that ends up taking.

Feasibility of this approach

Aside from the potential (small-ish) changes to ResourceLoader (see point 4 above), we can do all of this now. And actually most of the styles we need already exist in the Vue components themselves. Most of the work would just be a matter of reorganizing some of our current code so that it can be used in multiple ways going forward.

Agreed, and we can even defer those RL changes in the short term, per my response to point 4 above.

Risks/Trade-offs
  • This implementation strategy will allow us to keep writing our component styles within the Vue SFCs, which is nice from a familiarity and developer productivity approach.
  • We would need to create some additional packages inside the Codex repo, but this will mostly be a one-time task. As we add new components to Codex we'll need to include them in one of the existing sub-packages or (more rarely) create a new one. But little additional code is required here and once a sub-package has been set up it will rarely change. These will be included in each release just like codex-search is (though we may not need to publish them on NPM since they are intended only for use in MW). See https://gerrit.wikimedia.org/r/c/design/codex/+/866509/ for an example of what this would look like.

I'm OK with not publishing them on NPM (I didn't really want to publish codex-search to NPM either), but we do need a mechanism to get these into MW, ideally via something compatible with foreign-resources. Maybe we can include these bundles in the main @wikimedia/codex package by copying some stuff around in prepublish hooks. If we do that, maybe we also don't need a bunch of separate packages/codex-foo directories, just some entry point files and a shared build config.

  • CSS-only implementations of components will need to be tested; I think that VueTest is a good place to do this (maybe a new sub-page of that extension can be created for the CSS-only components)

Agreed

  • We'll need to create several additional ResourceLoader modules (especially if we cannot come up with a way to get just the CSS or JS from a module that contains both). But like in the case of the additional Codex packages, most of the work here is a one-time effort.
  • We might want to include some easily copy-pastable markup snippets for users in the absence of a good way to package up CSS-only components in mustache templates or PHP.

Yes, we'll need to create some sort of documentation with examples eventually.

Overall I think this approach is pretty low-risk, and doesn't rely on any speculative or complex new technical work.

Even after a full SSR solution is available (which may take some time), I think that there will always be valid use-cases for a simple, no-build-step approach to server-rendering Codex-consistent UIs.

Thanks for thinking this all through, I like the fact that you came up with something that doesn't require many ongoing changes to how we write and structure our code, but mostly one-time changes to our infrastructure.

In regards to the "sub-packages" ides:

This sounds like a good approach to me. However, I think it should be refined to address the fact that some of these packages will have overlap and/or have dependencies on each other. For example, codex-buttons contains the Icon component, but some other packages presumably would as well.

I was thinking that we could try to do this the same way that OOUI is broken up into modules. Maybe there is a codex-core that all the other ones depend on which contains certain key things like Button, Icon, TextInput, etc. Something else for layouts (tabs, maybe card), something for Dialogs, etc.

I'd be curious to see of we can map our idea of component groups for the docs site onto these sub-packages and come up with a usable result.

Anyway, I think we may be able to avoid complex dependency relationships at the cost of somewhat less-granular modules.

egardner updated the task description. (Show Details)
egardner triaged this task as High priority.Dec 9 2022, 5:57 PM

A few more thoughts:

MediaWiki-only

Are we sure we want these to be MW-only? Are there any other circumstances in which someone in the movement could want just the non-interactive styles for something?

This impacts the implementation of the icon component quite a bit—if it's MW-only we can set up some stuff on the PHP side to help; otherwise we need to think of another solution. Plus, I just hesitate to add something to Codex that is only usable in MW. That said, if there's not much of a use case outside of MW and making it MW-agnostic will be difficult, it may be worth just accepting that.

--js-active class

I agree that adding this class to components on mount makes sense. I still have a few questions about CSS-only components vs. server-rendered Vue components, and the sustainability of adding a class to the Vue components that is JS-specific but not Vue-specific, but I think this will generally work well.

Which components, and effort

I've reviewed our existing components and identified a few that will be trickier and a few that probably don't need CSS-only versions.

Tricky ones:

  • Icon, as mentioned above.
  • Tabs: totally achievable, but will require some special tricks to work without JS
  • ToggleButton (and ToggleButtonGroup): we'd need to use a checkbox to enable this without JS

Components that probably don't need a CSS-only version

  • Dialog: what's the point?
  • Lookup: there's no HTML element equivalent; people should just use TextInput instead
  • TypeaheadSearch: similar to Lookup, people should probably just use SearchInput (although we should evaluate whether there are parts of TahS's styles that we should make sharable)

I've moved Eric's proposal into the parent epic task (T321351), and will open a subtask of that for implementing the actual components with my notes on individual components. Closing this spike; please post further discussion in the epic!

Change 920770 had a related patch set uploaded (by Anne Tomasevich; author: Anne Tomasevich):

[design/codex@main] docs: Add ADR for CSS Components

https://gerrit.wikimedia.org/r/920770

Change 920770 merged by jenkins-bot:

[design/codex@main] docs: Add ADR for CSS Components

https://gerrit.wikimedia.org/r/920770

Change 922610 had a related patch set uploaded (by VolkerE; author: VolkerE):

[mediawiki/core@master] Update Codex from v0.10.0 to v0.11.0

https://gerrit.wikimedia.org/r/922610

Change 922610 merged by jenkins-bot:

[mediawiki/core@master] Update Codex from v0.10.0 to v0.11.0

https://gerrit.wikimedia.org/r/922610