[Proposal] Explore the possibility of a "Universal Adapter" interface #840
Description
Background / Problem
MDC-Web was designed from the ground up to integrate with frameworks. Our initial solution to the problem of widely varying environments and host framework APIs (virtual DOM, shadow DOM, incremental DOM, no DOM, etc.) was our Component / Foundation / Adapter architecture model.
Since we've released MDC-Web we've built ~30 components using this model. Furthermore, we've been able to prove out its use case both by building our own framework examples and through our external community's use of the model.
That being said, we've also been able to see a few problems with the approach, such as the following:
- Duplication of logic across components: Almost all adapter interfaces require the user to implement
addClass
,removeClass
, etc. - Hard to understand / large learning curve: Many contributors seem to be confused / frustrated by writing adapters and instead opt to just wrap our vanilla components (An example of integrating with React without the Foundation / Adapter pattern #407, React example complexity #393, Add createAdapter to other components? #728).
- O(n) effort: Every component needs to have its adapter implemented separately; N adapters for N components
- Lots of breaking changes: Every time an adapter interface is modified, it's a breaking change
- Documentation overhead: Every adapter API needs to be documented in a README. This leads to developers wanting to implement Material Components for their framework needing to peruse multiple files scattered across multiple locations. This is made even worse if by error we forget to document a specific adapter method.
Solution - A "Universal Adapter" interface
The main idea here is to build one single adapter per platform/framework, instead of per component.
We had talked about this idea during the initial design phase of MDC-Web, but punted on it because we didn't want to try and build an overarching adapter without knowing exactly what it would need, thus have it be subject to a lot of churn. I feel like we've built enough into the library now that we would have a good idea of what it would need to facilitate.
For example, for our vanilla components there would be one MDCVanillaDOMAdapter
(or some other, better name) that would implement every method required for every foundation to interact with its host environment. As such, it would expect components to be structured in a certain way and share a common interface and way of doing things. For example, it might expect that all components expose a property root
that allows access to the underlying DOM element the component is wrapping. This adapter could then be used by all foundation code. Thus, components simply become idiomatic wrappers for foundations, and do not have to implement any of the adapter logic themselves.
Using a single, universal adapter presents much less of a learning curve. Rather than have to think about how to build an adapter every time a developer wants to build / integrate a component, she instead would only have to learn how to use one single adapter across multiple components. While there would still be a learning curve WRT foundations / components, not having to figure out how to write or implement an adapter would save the developer a lot of time. Furthermore, frameworks like angular have similar mechanisms for dealing with heterogeneous platforms (see platform-*
packages).
A single adapter also means that documentation on how to build and design ones own adapters would be centralized, rather than spread out across the codebase. This means that if framework authors wanted to build an adapter for their framework, they could find the entire specification for what would be needed in one location.
Finally, this means that we could theoretically take on the task of building adapters for frameworks other than the vanilla DOM. React would be a great place to start with this. There are currently ~12 dependent react wrapper packages for MDC-Web on npm, each with varying degrees of support and fidelity. By building an official adapter, we could remove an extremely large barrier when building Material Design components for these frameworks.
Open-ended questions
What goes in the interface? What doesn't?
Need to audit common operations and figure out how to apply them across elements. A big question here is how to target arbitrary "DOM elements" that need to be queried / modified. One approach here could be to use unique strings or Symbols as abstract identifiers for component elements:
// mdc-textfield/foundation.js
export const ROOT = Symbol('mdc-textfield');
export const LABEL_ELEMENT = Symbol('mdc-textfield__label');
// somewhere in the code...
this.adapter_.addClass(ROOT, cssClasses.FOCUSED);
this.adapter_.addClass(LABEL_ELEMENT, cssClasses.LABEL_FLOAT_ABOVE);
These identifiers could then be mapped to elements in component code.
How do we handle heirarchal component relationships?
Components like MDCTabBar
and MDCSelect
have adapter methods that operate on subcomponents rather than elements, e.g. getOffsetLeftForTabAtIndex
. We'd have to figure out a way to facilitate these relationships using a universal adapter.
How do we ensure adapters do not become extremely overcomplicated?
Having one gigantic, overly-complex adapter API is just as bad as having 10s of smaller, less-complex adapter APIs, IMO. We'd have to put a lot of thought into designing an adapter interface that's resilient enough to facilitate the majority of future use cases that new components will require. We don't want more breaking changes because due to continuously updating one single adapter.