Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discovery: Allowing components to be extended/inherited #1879

Closed
6 tasks
jamigibbs opened this issue Jun 29, 2023 · 5 comments
Closed
6 tasks

Discovery: Allowing components to be extended/inherited #1879

jamigibbs opened this issue Jun 29, 2023 · 5 comments
Assignees
Labels
dst-engineering Issues that require work from Design System Team engineers platform-design-system-team

Comments

@jamigibbs
Copy link
Contributor

Description

We have started some discussion about how the component library could be used by other design system teams so that they can extend web component functionality and styles for their own use (USWDS and the VA Clinician Design System specifically).

We are using Stencil to build our web components which uses classes that theoretically should be able to be extended using inheritance but this is something that the Stencil library has intentionally decided not to support. It was recommended instead to use composition.

Ideally, the goal here is to find a way to "inherit/import" (i.e. not copy and paste) a component and then add/remove a property even with that limitation.

There are a couple of different approaches we want to explore related to composition:

  1. Nesting existing components in a parent/child way. An example of this is the va-notification component which also uses va-card.
  2. Programmatically like in this example where a base class is composed with a component that is essentially just building off of the original.

Some questions to answer:

  • If another DS team wanted to create a new component based off of a VA component (va-button for example), what would that look like for both of those approaches mentioned above (nesting and composition)? Build an MVP for both if possible.
  • Are there any limitations with either of those approaches?
  • Are there any other technical approaches to try other than those listed above?

Tasks

  • Read/explore the links above to the Stencil issue discussions about inheritance and composition.
  • Build MVP examples for the difference approaches mentioned above and any other possible solutions that might be relevant.
  • Present findings to the DST.
@caw310 caw310 added the dst-engineering Issues that require work from Design System Team engineers label Jun 29, 2023
@humancompanion-usds
Copy link
Collaborator

I'll add use cases here as I think of them.

Defeat variations that we don't want to support

One that has come up is that the USWDS Button currently offers accent color variations that we do not, at the moment, intend to expose to VA. When we get va-button as usa-button from USWDS, how do we defeat those variations (which I'd imagine they will add)?

Defeat properties we don't want to support

One theoretical use case is if USWDS wants a property on a component that we do not want. While I think we all agreed that we won't allow for disabling form fields, let's just use that as an example. Say there was a property that disabled a component and we didn't want to offer that property. How would we defeat it?

Add and/or alter properties we do want to support

Essentially the reverse of the above. Let's say we wanted to add inert as our preferred method of disabling a field. We might even want to use the same property name but have it do a different thing. How would we add a property and how might we alter a property to behave differently?

@jamigibbs
Copy link
Contributor Author

This will carry over because of unexpected PTO.

@jamigibbs
Copy link
Contributor Author

jamigibbs commented Aug 16, 2023

The Stencil Limitation

Our initial thought was that we could extend a component as a base class in another component but Stencil intentionally does not allow for any inheritance of Stencil Components. When you try to do it, you get the following error:

Stencil will not allow any inheritance of component classes, because stencil might need to apply transformation of the base class. In addition, allowing inheritance opens the world to a completely new world of problems, since the compiler will most likely not be able to static analyze the prototype chain at compiler time.

Screenshot 2023-08-11 at 11 38 17 AM

What are the options then?

  1. Composition with Generic Components (ie. nesting)

If one component is some kind of special case of another more generic component (e.g. MyWelcomeDialog and MyDialog), then you could let the "special" component render the "generic" component and configure it with properties.

This would mean that the "generic" component would need to be modified directly (additional props) to allow for customization, or, we would need to create small generic components that could be reused (composed) into a larger more specialized component.

It would essentially be "Nesting existing components in a parent/child way". For example:

// my-welcome-dialog.tsx

<Host background-color={this.bgColor}>
  <my-dialog 
    text-color={this.color}>
    hello world
  </my-dialog>
</Host>
  • Con: I think this approach would require a lot of restructuring of our existing components in order to make them more generic and composable, or, we'd need to add additional props to allow the components to be customized.
  • Pro: This approach does not require code duplication like the other options.
  • Pro: It also means that stylesheets and UI are available.

  1. Composition with base class

It seems that it's possible to create an abstract base class of a component that can be shared as was mentioned in this comment.

This approach might be more useful when needing to bring together multiple types of components to create a single combined component. Creating something like va-memborable-date would be an example where it's composed of multiple components (va-input and va-select) that share similar functionality.

But for one-off forks of individual components, it's clunky and creates a lot of overhead to manage.

  • Pro: We might be able to restrict how the component is being used with an interface contract between the classes.
  • Con: A large amount of code duplication (every prop, decorator, and lifecycle method would need to be copied between classes)
  • Con: If the intention is to create a completely "new" component from an existing component, this approach means we would probably need to modify all of our components (or at least the ones we want to be composable) and it creates a lot overhead.

  1. Composition with @use

The most straight forward approach might be to use the typescript-mix package which provides a @use decorator. This merges the original component's class with the new component's class making the functionality available.

import { VaCard } from '../va-card/va-card';

@Component({
  tag: 'va-card-extended',
  styleUrl: 'va-card-extended.scss',
  shadow: true,
})
export class VaCardExtended {

  @use(VaCard) this: VaCard;

  render() {
    const classes = classnames({'show-shadow': this.showShadow});
    return (
      <Host class={classes}>
        <slot></slot>
      </Host>
    );
  }
}

// va-card-extended.scss
@import '../va-card/va-card.scss';

  • Pro: The new component has access to all of the original component functionality without needing to duplicate it in the new component.
  • Pro: No need to modify the existing components.
  • Con: Still need to duplicate markup as well as import the original component's stylesheet.
  • Con: It requires an NPM package (not a native solution)

This might be how we could consume back from USWDS if/when they build web components.

This also might be useful for the VA Clinician Design System as a way to jump start their customization of VA components without us having to rebuild what we've already done.

Other Thoughts

Neither composition or component nesting seem to entirely meet our expectations or needs. I also don't think it will fully solve for any of these scenarios either:

  • Defeat variations that we don't want to support
  • Defeat properties we don't want to support
  • Add and/or alter properties we do want to support

But if a team did want a jumping off point to build from our components, a possible workflow would be:

  1. Fork the component-library and keep it in sync

  2. Create new components and leverage the typescript-mix package mentioned above using the @use decorator to mixin the VA component to build from.

  3. Keep the forked repo in sync with the upstream library (VA Component Library). None of the original/existing VA components should be modified.

Screenshot 2023-08-16 at 12 09 44 PM

Screenshot 2023-08-16 at 12 08 49 PM

@caw310
Copy link
Contributor

caw310 commented Aug 17, 2023

@humancompanion-usds assigning to you to review.

@Andrew565
Copy link
Contributor

One strategy I thought of for 'defeating' properties or variations we didn't want to support would be to capture them and return our preferred 'default' instead. For example, if we wanted to prevent big buttons, we could do something like:

// va-small-button.tsx
// changes big buttons to regular

<Host>
  <va-button 
    ...other props...
    big={false}
  </va-button>
</Host>

And it would be used as <va-small-button props={props}> and if they tried to pass in the 'big' prop it would just be automatically re-assigned to "big=false". This is with the nesting strategy, but it could also work with the @use strategy I believe.

@caw310 caw310 closed this as completed Aug 24, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dst-engineering Issues that require work from Design System Team engineers platform-design-system-team
Projects
None yet
Development

No branches or pull requests

4 participants