Ember prop spreading


Introduction

Because Ember doesn't support spreading props like React does, Pluma includes some utilities to help work around this limitation.

All Pluma components in Ember accept an additional argument called unsafe_props. The argument is an object that may contain any of the component's standard arguments.

Internally, when Pluma components read an argument, they first check if the argument is provided via standard argument syntax. If it's not available, they check whether that argument exists in unsafe_props, and use that value if it does.

This way, Pluma components can "spread" props: by passing an object to unsafe_props.

The utilities that support this are exported from Pluma for consumption in Ember apps.

Sprinkle support

With unsafe_props, it's possible to easily add sprinkle (utility argument) support to any custom app component.

First, extend your component's signature to include the AllBoxArgs interface, which contains all the possible Box component arguments. The interface is imported via

import type { AllBoxArgs } from '@customerio/pluma-components/ember';

Next, import the sprinkleProps utility as follows:

import { sprinkleProps } from '@customerio/pluma-components/ember';

and then use it to define a property in your component class, like so:

class SprinkleTestComponent extends Component<SprinkleTestComponentSignature> {
 sprinkleProps = sprinkleProps(this.args);
}

This creates a proxy object that we can then pass into a Pluma component's unsafe_props argument.

A complete example might look like the below:

import Component from '@glimmer/component';
import { PlumaBox, sprinkleProps } from '@customerio/pluma-components/ember';
import type { AllBoxArgs } from '@customerio/pluma-components/ember';

export interface SprinkleTestComponentSignature {
 Element: HTMLDivElement;
 Args: {
    // Define your own arguments
  isDisabled?: boolean;
 } & AllBoxArgs; // combine with Box arguments
 Blocks: {
  default: [];
 };
}

export default class SprinkleTestComponent extends Component<SprinkleTestComponentSignature> {
 sprinkleProps = sprinkleProps(this.args);

 <template>
  <PlumaBox @unsafe_props={{this.sprinkleProps}} ...attributes>
   {{yield}}
  </PlumaBox>
 </template>
}

Extending other Pluma components

The sprinkles example above works great if all we want is to add support for sprinkle utilities, most commonly by forwarding them to a PlumaBox component (though you can also use unsafe_props with other Pluma components).

Sometimes, you may want to extend a different Pluma component, and still support all the arguments that the original component supports.

To do so, you can use the propsProxy utility, which is what Pluma uses internally in all its components.

For example, this is what extending a PlumaButton might look like:

import Component from '@glimmer/component';
import { PlumaButton, propsProxy } from '@customerio/pluma-components/ember';
import type { ExtractSignature, SpreadArgs } from '@customerio/pluma-components/ember';

type PlumaButtonArgs = ExtractSignature<PlumaButton>['Args'];

// Define a set of all custom properties.
// This is used to create a props proxy object, which will forward all arguments
// except the ones in this set.
const customButtonProps = new Set([
  'myCustomArgument',
  'anotherCustomArgument',
] as const);

interface CustomButtonSignature {
  Element: HTMLButtonElement;
  Args: SpreadArgs<PlumaButtonArgs & {
    // define your own custom arguments here
    myCustomArgument?: string;
    anotherCustomArgument?: boolean;
  }>;
  Blocks: {
    default: [];
  };
}

class MyButton extends Component<CustomButtonSignature> {
  // Passing in ${'`'}except${'`'} will filter out all custom component arguments,
  // leaving only PlumaButton arguments to be forwarded.
 buttonProps = propsProxy(this.args, { except: customButtonProps });

  <template>
    <PlumaButton @unsafe_props={{this.buttonProps}} ...attributes>
      {{yield}}
    </PlumaButton>
  </template>
}