Skip to content

Documenting Components (arguments, attributes and yield) #591

@gossi

Description

@gossi

I'm looking for a convention/standard to document components, especially targeting these information:

  • arguments
  • attributes
  • yield

for components, template-only components while supporting for java- and typescript. Primarily focused for glimmer components, yet the js-only approach should also be suitable to be applied to ember components, too.

The idea is, that these conventions can be exported into an intermediate format, which itself can be consumed by documentation tools (such as ember-cli-addon-docs or storybook) and on the other hand for tools providing support at our fingertips, e.g. (unstable) ember language server, so we have suggestions through intelli-sense/auto-completion in our preferred editors.

Arguments

Arguments can easily be described as an interface as this already the proposed way for doing this in glimmer components, when using typescript:

export interface InputGroupArgs {
  value: string;
  name: string;
  label?: string;
  error?: string;
  changed?: (value: string) => void;
}

/**
 * An input group is an input field with additional label and error field
 *
 * Usage:
 * 
 * ```hbs
 * <InputGroup @value="abc" @name="given-name" @changed={{this.updateGivenName}} />
 * ```
 */
export default class InputGroupComponent extends Component<InputGroupArgs> {}

JS / Template-only Components

For javascript and template-only components I propose the same approach as above in a component.d.ts file.
Along with that is a side-benefit on top of it, which is type-coverage of your package. Second is, that most editors already use it as input to provide intelli-sense mechanism, even if the choice is to use javascript over typescript.

Alternative

Instead of using a component.d.ts regular JS can be used and special @argument tags in docblock can be used (as it is done today with ember-cli-addon-docs):

/**
 * An input group is an input field with additional label and error field
 *
 * Usage:
 * 
 * ```hbs
 * <InputGroup @value="abc" @name="given-name" @changed={{this.updateGivenName}} />
 * ```
 * 
 * @argument string value
 * @argument string name
 * @argument (string) label
 * @argument (string) error
 * @argument (Function) changed
 */
export default class InputGroupComponent extends Component {}

Drawback of this approach is, that would only work for JS components and is not a solution for template-only components. Second is, that supported types are usually limited to what jsdoc et al. support and not as rich as in comparison to the typesystem typescript offers.

Yield

The type for yield is an array and many people use the first entry to contain an object/hash. First separate between how to document the yield type itself and second where to make the connection between the typed yield and the component.

Typing yield:

import InputGroupLabelComponent from '...';
import InputGroupErrorComponent from '...';

interface InputGroupBuilder {
  Label: InputGroupLabelComponent;
  Label: InputGroupErrorComponent;
}

/**
 * Whether the input group has an error
 */
type IsErrored = boolean;

type InputGroupYield = [
  InputGroupBuilder,
  IsErrored
];

For connecting yield to the component I see two options:

Option 1:

Using a special jsdoc tag @yield (as been done in ember-cli-addon-docs today):

/**
 * [...]
 * 
 * @yield InputGroupYield
 */
export default class InputGroupComponent extends Component<InputGroupArgs> {}

Option 2:

Extend generics to accept a second value as yield:

/**
 * [...]
 */
export default class InputGroupComponent extends Component<InputGroupArgs, InputGroupYield> {}

There is no real benefit from a technical perspective as you are not accessing the yield block from the code as of today and I don't see that happening in the future. Instead would block a slot of the generics, that might be better suited to something else in the future.

As a counter to this, the pair reads coherently as ... extends Component<Input, Output> (which is why I personally like it).

Attributes

There is already an issue open regarding this: #586 (...attributes -- Component API and Discoverability). In speaks about a list of things unknown to the consumer of a component and where passed attributes will end up (if ever). Whereas the argument is that those information are implementation details of the respective component to decide on that.

While I agree this is implementation details I as a consumer still better have some information what I can do use attributes for. It still belongs to the components internals but here are my questions I have when passing down attributes:

  • Which element(s) my ...attributes are added to ?
  • is it is the most outer one (keep in mind you can have multiple ones)?
  • What other attributes are already available?
  • What attributes will be overridden by the component?

That's mostly a11y related. Given the element + attribute combination has a massive effect on listener usage: e.g. whether I can use a {{on 'click' ...}} modifier or not.

!!!

Provide Suggestions

!!!

Appendix

You may want to document your component.ts (typescript) or component.d.ts (js / template-only) as described above. Using the generics option for yields, this is how the full example looks like:

import Component from '@glimmer/component';
import InputGroupLabelComponent from '...';
import InputGroupErrorComponent from '...';

interface InputGroupBuilder {
  Label: InputGroupLabelComponent;
  Label: InputGroupErrorComponent;
}

/**
 * Whether the input group has an error
 */
type IsErrored = boolean;

type InputGroupYield = [
  InputGroupBuilder,
  IsErrored
];

export interface InputGroupArgs {
  value: string;
  name: string;
  label?: string;
  error?: string;
  changed?: (value: string) => void;
}

/**
 * An input group is an input control with additional label and error fields
 *
 * Usage:
 * 
 * ```hbs
 * <InputGroup @value="abc" @name="given-name" @changed={{this.updateGivenName}} />
 * ```
 */
export default class InputGroupComponent extends Component<InputGroupArgs, InputGroupYield> {}

Let's discuss to fill the blank spots.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions