Re: [w3c/webcomponents] Editor support for WebComponents (#776)

First of all, thank you for your great work on this JSON format @octref :tada:

Right now the format has a lot of momentum. [Experimental support in VS Code](https://code.visualstudio.com/updates/v1_31#_html-and-css-custom-data-support), [the Stencil compiler generates it](https://github.com/ionic-team/stencil/pull/1256) and [it’s considered for the reboot of webcomponents.org](https://github.com/webcomponents/webcomponents.org/issues/1250).

I would really like to get the discussion going around expanding the format, and I hope my thoughts and suggestions will help this discussion. Here I suggest some features that I think would be a great addition for a potential next version of this JSON format. I know there are some open questions in my post here, but I'm pretty much posting these suggestions to start the discussion. Go [here](#conclusion) for TLDR.
# Purpose
I would like to briefly outline the purpose of this format (at risk of repeating the first comment on this issue). 

This JSON format is a common way of describing custom elements. A library shipping custom elements should include a JSON file with metadata describing custom elements provided by the library. This format can be used to give editor support, generate documentation/demos and much more.

I think the format should be simple enough to be written by hand, but a library maintainer would likely be using a tool to generate them. The tool that the maintainer chooses would depend on which library they used to build the elements (e.g. the Stencil compiler already outputs this format).

I also think it should be a goal that it’s possible to describe the API of built-in HTML elements using this format.

# Suggestions
## Describing members
I suggest the next version of this format supports the following 4 DOM features:
* Attributes
* Properties
* Events fired
* Slots

In my experience, these 4 features of a custom element API are the most important to know for a consumer. The existing version of this JSON format already supports attributes.

It would make sense to add 3 optional fields called `properties`, `events` and `slots` on the tag object next to the existing `attributes` field:

```
{
...
"tags": [
   {
      "name": "my-button",
      "description": "This is my button",
      "attributes": [...]
      "properties": [...]
      "events": [...]
      "slots": [...]
   }
],
...
}
```

Just like the existing `attributes`, I think the content of all 3 new fields should at least have a required `name` and an optional `description`.

The new `properties` objects should also have `values` and `valueSet` like the existing `attributes`.

Based on the aforementioned, the JSON would look like this:

```
{
...
"tags": [
    {
      "name": "my-button",
      "description": "This is my button",
      "attributes": [{ "name": "color", "values": [{ "name": "red" }, { "name": "green" }] }],
      "properties": [{ "name": "color", "values": [{ "name": "red" }, { "name": "green" }] }],
      "events": [{ "name": "colorchange", "description": "This event will fire whenever 'color' changes" }],
      "slots": [
        { "name": "", "description": "Default slot. This will become the main content of this element." },
        { "name": "header", "description": "Content that will go in the header." }
      ]
    }
]
...
}
```

Note that an unnamed slot has an empty string as the name because `name` is required for all members.

## Relation between property & attribute
In the example above, two related members called `color` are repeated as both an attribute and a property to indicate that both can be set on this element to control the color. There is however no explicit connection between them in the JSON right now.

In order to describe the relation between a property and an attribute, I think we need to look into the following:
1) A way to describe which attribute a property is related to. e.g. a property called "myText" could describe the same data as an attribute called "my-text".
2) A way to describe how setting one will affect the other (reflection).

To solve (1) I suggest adding `attribute: <string>` field that can be used when describing properties. This field would indicate that property and attribute describe the same data. This field only indicates the link. In addition, I think it should be optional to specify the corresponding attribute in the `attributes` array in order to stay DRY. 

To solve (2) I suggest adding `reflect: <"both"|"to-attribute"|"to-property">` field that can be used when describing properties. Setting reflect would indicate that updating the value of either the attribute or property could also affect the value of the other. I think that when using `reflect` it should be required that `attribute` is also present.

```
{
...
"tags": [
   {
      "name": "my-button",
      "description": "This is my button",
      "properties": [{
         "name": "myColor",
         "description": "This is my color",
         "attribute": "my-color",
         "reflect": "to-property"
      }]
   }
],
...
}
```

## Describing types
The existing version of this format only supports string enumeration types (by using `values` arrays). This makes sense as all attributes in the DOM are strings. In addition (correct me if I’m wrong @octref) it seems like boolean attributes are [using the `valueSet: 'v'`](https://github.com/microsoft/vscode-html-languageservice/blob/master/src/languageFacts/data/html5Tags.ts) even though "v" doesn’t exist as a value set? 

I think this format will need to have some way of specifying more complex types than string enumeration types. This is applicable for both attributes and properties. 

Even though all attributes are strings as far as the DOM is concerned, I think it would be very useful to tell what type of value the attribute is coerced/converted to. Imagine reading the documentation about an element or using an IDE-plugin to autocomplete attributes: Here it would be great to know that the value given to e.g. `max` is coerced to a number or that the `disabled` attribute is a boolean attribute.

Let's look at some examples from built-in elements: [‘max’](https://html.spec.whatwg.org/multipage/input.html#the-min-and-max-attributes) on `<input>` is coerced to `number` and [`required`](https://html.spec.whatwg.org/multipage/input.html#the-required-attribute) on `<input>` is a boolean attribute, so coerced to a boolean. Another example is that the [`download`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#Attributes) on `<a>` behaves both as a boolean attribute and also accepts any string value (which becomes a prefilled file name). In this case, I think it would be powerful to tell that the type of this attribute is a union of `boolean` and `string`.

With all this in mind, I suggest that we explore adding an optional `type` field to the JSON format. This field can be used to describe the types of attributes, properties and events. I think it would make sense that the format of the `type` field is a [jsdoc type expression](https://jsdoc.app/tags-type.html), because then we avoid inventing a new type format. The type of the `download` attribute would then be: `"(boolean|string)"`. 

Here is an example of how it could look:
```
{
...
"tags": [
   {
      "name": "my-button",
      "description": "This is my button",
      "properties": [{
         "name": "prop",
         "description": "This is my property",
         "type": "(string|boolean)"
      }]
   }
],
...
}
```

In the following sections, I will explore what using jsdoc gives us and the challenges that follow. It could also be interesting to investigate if it makes sense to use Typescript definition files instead, but I fear that making the JSON format dependent on Typescript for types would not be the right way to go. Therefore I chose to go with jsdoc type expressions, but there might be something even better. I'm very interested in hearing what you think.

I'm aware that this opens a lot of questions that will need to be considered. Most notably these two questions which I will try to address in the next sections:
1. If this is a type expression, how would we define interfaces defined with [typedef](https://jsdoc.app/tags-typedef.html) or Typescript interfaces?
2. This would make `values` redundant since `type` now covers most use cases offered by it. Would it be valuable to deprecate `values`?

#### values and type
As mentioned before `values` already exist. This opens the question if it would make sense to deprecate this property. I'm in doubt. 

A string enumeration could with `type` be expressed like `"type": "('red'|'green')`, but with `values` it could be expressed like `"values": [{"name": "red", "description": "Makes the element red"}, {"name": "green"}]`. The big difference is that it’s possible for each value in `values` to have a `description` thus making it possible to document what each value of the string enumeration does. Therefore `type` doesn't entirely replace the functionality of `values`.

#### valueSet
We could also allow adding `type` to valueSet like this so it becomes possible to have shared types across the JSON file:

```
{
...
"valueSets": [
   {
      "name": "colors",
      "type": "('red'|'green'|'blue')"
   }
],
"tags": [
   {
      "name": "my-button",
      "description": "This is my button",
      "attributes": [{
         "name": "color",
         "valueSet": "colors"
      }, {
         "name": "color-hover",
         "valueSet": "colors"
      }]
   }
],
...
}
```

#### Complex types
Supporting complex types comes with some challenges. This is the point where I'm the most in doubt, so I look very much forward to hear what you think.

Let me give an example of a challenge: If we describe a property called `color` of type being a jsdoc type definition (or Typescript interface) called `Color` (with r,g,b properties), it would result in the `color` property (in the JSON file) having the type `"Color"`. The details of the type definition would be gone and we would now only know the name of the type, not the structure. If necessary, a tool parsing this JSON file, reading the type `Color`, would have to find the type with that name somewhere else (e.g. a d.ts file) or just print out that the type is `Color`. The question is how.

1. A possible solution could be a tool generating these files inlines the jsdoc type definitions like this: `"{r: number, g: number, b: number}"`. The downside when inlining type definitions is that we lose the name of the type.
2. Another solution would be to support `jsdoc type imports` like `import("./src/Foo.js').Bar`. The path would be relative to the JSON file. A tool analyzing the JSON file could now either look for a JSDoc type definition named "Bar" in "Foo.js" or a ts/d.ts file "Foo.d.ts" exporting a type named "Bar".  

#### Types and events
The `type` of events would describe the type of the dispatched event. Here are some examples:
* `Event`: This is the most general event type.
* `MouseEvent`: The built-in mouse event.
* `CustomEvent<number>`: This would be used if you dispatch a custom event. The generic type is the type of detail.
* `MyChangeEvent`: This could be used if you extend Event and dispatch your own event called MyChangeEvent. It leads us into the same territory as described above with Complex types. Where do you define what kind of event `MyChangeEvent` is? Again, this could be solved by using a jsdoc type import like `import("./src/Foo.js').MyChangeEvent`.

## Default property values
Properties usually are holding the "local" state of a custom element, therefore, it is highly interesting for users to know these default values/states.

Attributes do NOT have a default as users are providing the html. However, when using the combination of a default value and reflect, then a custom element will get set a certain attribute if not already defined by the user.

We could add `default: <string>` when describing properties like this:

```
{
...
"tags": [
   {
      "name": "my-user",
      "properties": [{
         "name": "color",
         "default": "red"
      }]
   }
],
...
}
```

## Deprecated
It would be useful to add `deprecated: <boolean | string>` to members and tags. This makes it possible to provide a clear path forward when APIs are going to change in an upcoming breaking release. 

**A tag gets deprecated**
```
{
...
"tags": [
   {
      "name": "my-button",
      "deprecated": "Please use <other-button> instead.",
      ...
   }
],
...
}
```

**A property or attribute gets deprecated**
```
{
...
"tags": [
   {
      "name": "my-user",
      "properties": [{
         "name": "name",
         "deprecated": "Use .fullName instead",
      }, {
         "name": "fullName",
      }]
   }
],
...
}
```

## Examples
In JSDoc a well-used feature is @example allowing to provide copyable code snippets for your users. We can extract that to be easily usable by documentation tools. Can be used for members and for tags.

The field to describe examples could be `example: <string[]>`. It's an array as multiple examples should be possible (like in JSDoc). If there is only one example then array will have a length of 1.

```
{
...
"tags": [
   {
      "name": "my-user",
      "properties": [{
         "name": "fullName",
         "example": ["el.fullName = ‘foo bar’;\n// this will make it available via\n console.log(el.firstName);\n console.log(el.lastName);", "another example"]
      }]
   }
],
...
}
```

## Methods
It would be very useful if it was possible to document methods of a custom element using this JSON format. When using and extending custom elements it's rather important to know which methods are available and therefore having them available for documentation would be a big plus. 

This can be done by adding `methods` to tags. The value of methods would be an array of method objects. Each method object would describe a method with a required `name`, optional `description` and an optional `params` array. Each param would consist of an optional jsdoc type expression `type` and an optional `description`.

This is an example of how it could look like:

```
{
...
"tags": [
   {
     "name": "my-dialog",
     "methods": [{
        "name": "open",
        "description": "Open the dialog",
        "params": [
          { "type": "String", "description": "Override title" }, 
          { "type": "HTMLElement", "description": "Element to focus after close" }
        ],
      }]
   }
],
...
}
```

## Summarizing properties
If these suggestions are accepted, properties will become the entry with the most info. e.g. it will have additionally “default”, “type”, “attribute” and “reflect”:

```
{
...
"tags": [
   {
      "name": "my-user",
      "properties": [{
         "name": "colorName",
         "type": "String",
         "default": "red",
         "values": [{ "name": "red" }, { "name": "green" }] }],
         "attribute": "color-name",
         "reflect": "to-attribute"
      }]
   }
],
...
}
```

## Path
I suggest adding `path: <string>` to tags. This field would describe where to find the element. The path could be a link/url/file, and the relative path would be relative from the JSON file.

This information would be useful to e.g. IDE-plugins and allows to find source files and Typescript definition files.

```
{
...
"tags": [
   {
      "name": "my-button",
      "description": "This is my button",
      "path": "./src/my-button.js"
      ...
   }
],
...
}
```

## Globals
In the existing vscode JSON format, global attributes can be described using `globalAttributtes` on the root object. This field would primarily be used if one were to describe existing, built-in elements, but I could also see a library shipping custom elements describing global attributes for styling purposes. 

In addition to [global attributes](https://html.spec.whatwg.org/multipage/dom.html#global-attributes) (like the ‘title’ attribute), there also exists [global events](https://html.spec.whatwg.org/multipage/webappapis.html#globaleventhandlers) (like the ‘click’ event).

Therefore I suggest that the existing `globalAttributes` is removed and replaced with `global` like this:

```
{
...
  "global": {
      "attributes": [...]
      "properties": [...]
      "events": [...]
      "slots": [...]
  }
...
}
```

Note that this would be a breaking change.

I chose to include `slots` and `properties` in `global` for completeness sake. Right now I’m not sure what it gives of value, but I like that all of the 4 features I mentioned before can also be described in `global` to give more flexibility.

# Sharing the JSON file
I have heard two different suggestions of what to call the JSON file: `web-components.json` and `custom-elements.json`. Personally I like the latter best because it’s broader and because the only requirement is that you describe custom elements.

I also think there could be value in being able to point to this file using a "customElements" field in `package.json`. The field could accept both a single path and an array of file paths: `"customElements": "./custom-elements.json"` or `"customElements": ["./src/my-button/my-button.json", "./src/my-input/my-input.json"]`. Describing custom elements using multiple files could make it easier if one were to maintain them by hand.

However, I'm not sure if adding `customElements` to package.json should be a requirement. Tools could just as well look for `custom-elements.json` in the root of a package.

# Conclusion
Overall we would like to encourage people to publish their (npm) packages including a `custom-elements.json`.

The file should look something like this:

```
{
...
"tags": [
    {
      "name": "my-button",
      "description": "This is my button",
      "attributes": [{ "name": "disabled", "type": "boolean" }],
      "properties": [{ 
        "name": "colorName",
        "default": "red",
        "type": "String",
        "type": "('red'|'green'|'blue')",
        "attribute”: "color-name",
        "reflect": "both",
        "deprecated": "Use .fanzyColor instead",
        "example": ["a code example in html or js"],
      }],
      "events": [{
        "name": "colorchange", 
        "description": "This event will fire whenever 'color' changes" 
      }],
      "slots": [
        { 
          "name": "", 
          "description": "Default slot. This will become the main content of this element." 
        },
        { 
          "name": "header", 
          "description": "Content that will go in the header." 
        }
      ],
      
      /* following is maybe worth adding - or wait for next iteration */
      "methods": [{
        "name": "open",
        "description": "Open the dialog",
        "params": [
          { "type": "String", "description": "Override title" }, 
          { "type": "HTMLElement", "description": "Element to focus after close" }
        ],
      ]}
    }
]
...
}
```

I tried to suggest as few additions/changes to the format which I think will result in the greatest improvement. The purpose of this post is to start the discussion around the format with some concrete suggestions and considerations. I'm sure that there are many questions still left to consider and perhaps not all suggested additions are needed. 

I added experimental support for some of what I suggest in this comment to [web-component-analyzer](https://github.com/runem/web-component-analyzer). I will add more features to the experimental support during the next couple of days. This way you can get an idea of how this format would look based on your own custom elements:

```bash
npx web-component-analyzer analyze my-element.js --format json --outFile custom-elements.json
```

**Note**: you can also use `--format vscode` to generate the existing JSON format.

Thanks for reading this comment. I look very much forward to hear what you think about these suggestions.







-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/w3c/webcomponents/issues/776#issuecomment-536749457

Received on Monday, 30 September 2019 20:58:40 UTC