# Guiding Principles For Component Development

The following is a guideline of the principles we use to create web components here at Brightspot.

## Naming

- Always prefix custom elements with the namespace: `btu-`

- When element names contain multiple words, use kebab-case: `btu-my-component` **not** `btu-MyComponent`

- Component definition files should be named `index.ts`.

- Component definition files should live in a folder named the same as the custom element name: `MyComponent/index.ts`

- Component CSS files should be named `MyComponent.css` and live in `/src`. LESS CSS files for v4 compatibility should be in `/src/main/webapp/v4`.

## Attributes

- Always use kebab-case for attributes with multiple words: `foo-bar=` **not** `fooBar=`

- Always use boolean attributes for boolean values: `disabled` **not** `disabled="true"`

- Always default boolean values to `false`.

- Avoid using Lit's [reflect](https://lit.dev/docs/components/properties/#reflected-attributes) unless you expect the attribute to be usable in CSS.

- Always access data attributes via `dataset` instead of `getAttribute`.

## CSS

- Prefer CSS custom properties over component props to change the appearance of a component: `--MyComponent-gap-size`

- Prefer native CSS whenever possible instead of Less/Preprocessor APIs.

## DOM

- You should be able to instantiate the component with just a plain tag: `<btu-my-component/>`

- Components should still work if their children are removed and reattached.

## Events

- To raise custom events, use the built in `emit` method exposed by [EventEmitterMixin.ts](https://github.com/perfectsense/brightspot/blob/97b7401ab9dca223746401afb83fedd5857dfa3d/cms/tool-ui/src/EventEmitterMixin.ts).

- Always name events starting with the `btu` prefix, then your component name, followed by the event name: `btu-my-component-success`

- Prefer declarative event handlers when possible: `<btu-my-component @click="${this.handler}"...`

## Development

- Use our mixins to extend your component where applicable. For example, [EventEmitterMixin.ts](https://github.com/perfectsense/brightspot/blob/97b7401ab9dca223746401afb83fedd5857dfa3d/cms/tool-ui/src/EventEmitterMixin.ts) or [PlaceholderEditableMixin.ts](https://github.com/perfectsense/brightspot/blob/97b7401ab9dca223746401afb83fedd5857dfa3d/cms/tool-ui/src/PlaceholderEditableMixin.ts).

- Wherever possible, use private fields.

- Private class members and methods should be written with the keyword `private` and be prefixed with an underscore: `private _foo`. _Note:_ As of this writing, we cannot use private class field syntax combined with decorators due to the older version of Babel which Lit uses internally. See more here: https://lit.dev/docs/components/decorators/#decorators-babel

- Always declare static props, then static methods, followed by private, protected and finally public.

- Always declare class fields alphabetically.

- Always define the `render()` method last.

- Always `return this` in Lit's `#createRenderRoot`. (_Because all of our components need to be in the light dom._)

- If referencing the `BRIGHTSPOT` window variable, always use optional chaining to prevent errors in case the code runs in non-CMS environments like Playwright tests: `window.BRIGHTSPOT?.ui.tooltips...`

- If a JavaScript file only has `onFind` logic, move the logic into [Entry.ts](Entry.ts).

## TypeScript

- Always include your component in the global `HTMLElementTagNameMap` ([Lit: Typings](https://lit.dev/docs/components/defining/#typescript-typings)) for the best TypeScript support in the IDE.

- Avoid using TypeScript [assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) `as` in most cases. Instead, try [narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#instanceof-narrowing) your types using `instanceof`.

- Prefer TypeScript’s [type arguments](https://www.typescriptlang.org/docs/handbook/2/functions.html#generic-functions) over assertions: `document.querySelector<HTMLInputElement>('...')` **not** `const foo: NodeListOf<HTMLInputElement> = document.querySelector('...')`

## Documentation

- Always create a README file inside your component’s folder.

- Ensure the element, methods, events, attributes, and props are well documented.

- Ensure any deprecations are documented when upgrading legacy components.

## Tests

- Use the `baseTest` module as a basis for initializing tests.

- Always start with a simple test that ensures that the default version of the component works as described.

- Attempt to test all the public facing features the component offers.

- Group multiple tests for a single component inside a [describe](https://playwright.dev/docs/api/class-test#test-describe-1) block.

- If you’ve upgraded a component/module from an older version of the code it should maintain backwards compatibility. There should be at least one test for the modern way of using it as well as a test for backward compatible usage.

- When upgrading Playwright, ensure its version matches the one used in the backend. The relevant backend versions can
  be found in `/platform/brightspot-dependencies/build.gradle` and `/antithesis/workload-container/Dockerfile`.

## Uncommon Conventions

- Import statements for Lit should include the `.js` extension. Otherwise the tests will fail. More information on why can be found here: https://github.com/lit/lit/issues/1986

- Avoid using [private class members](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields) because, as of this writing, the older version of Babel which Lit uses internally doesn't support them in combination with Decorators. See more here: https://lit.dev/docs/components/decorators/#decorators-babel

---

# Web Component Creation Guide

Brightspot components strive to behave like typical web components. But they must also upgrade existing markup while maintaining backward compatibility. One of the trade-offs to this is our components can't exist in the Shadow Dom. This introduces a few feature gaps (like slot API) which we needed to work around. The following section aims to explain how we address those gaps and how you can create components without having to think about them during development.

## Common Patterns

### Broadcasting Events

Usually, components should mixin the functionality from our `EventEmitterMixin.ts`.
This gives you the ability to broadcast custom events simply by calling `this.emit('CUSTOM-EVENT-NAME')` from within your component. It also enforces type checking of custom events so you get the benefit of intellisense and type safety at compile-time.

For example:

```// Dog.ts
export default class Dog extends EventEmitterMixin(LitElement) {
  ...
  bark() {
    this.emit('btu-dog-barked')
  }
  ...
}
```

### Composition With Legacy Markup

Sometimes components need access to a direct child element in order to function. One gotcha is that the child element could be added to the dom _after_ our component is connected. The child could be removed, or reattached. But our component doesn't know when the child is or isn't attached. To avoid repeatedly writing logic in each component to deal with those scenarios, we have a utility class called [WrappingMixin.ts](https://github.com/perfectsense/brightspot/blob/97b7401ab9dca223746401afb83fedd5857dfa3d/cms/tool-ui/src/WrappingMixin.ts).

Here is an example. Suppose we need to make a new component called `InputPlus`. The `InputPlus` component should automatically wrap all existing `<input>` elements.

This means the component needs to work the same in two different implementation scenarios:

1. **Declaratively**:<br>
   Expressed directly in markup.
   eg: `<btu-input-plus ... ><input ...></btu-input-plus>`
2. **Upgrade Legacy**:<br>
   Automatically upgrades existing markups.
   eg: `<input ...>` becomes: `<btu-input-plus ...><input ...></btu-input-plus>`

We'll use the `WrappingMixin.ts` module to help with both scenarios.

First, extend WrappingMixin:

```
InputPlus.ts
------------

export default class InputPlus extends WrappingMixin(LitElement) { ... }
```

Next, in the code which handles upgrading legacy markup (typically Entry.ts), select the element you want to wrap. In this example it is an `<input>`, so we'll query for it using onFind:

```
Entry.ts
--------

onFind('input', (input: HTMLInputElement) => { ... }
```

Import and call `wrap()` on the input:

```
Entry.ts
--------

import { wrap } from './WrappingMixin'

onFind('input', (input: HTMLInputElement) => {
  wrap(input, InputPlus, (inputPlusElement) => { ... }
}
```

The `wrap()` method accepts 3 parameters:

1. The html element to wrap.
2. The component Class to use as a wrapper.
3. (optional) A callback function which sends an argument of the new instance of your component. This runs after the component is created but before connectedCallback. Note that this is _not_ a callback to indicate that the element _is_ wrapped.

Technically, that's all you need to do. In both scenarios the component would wrap the input and produce the following:

```
<btu-input-plus>
  <input/>
</btu-input-plus>
```

#### Advanced usage

Our component may need to reference the wrapped `<input>` element. To accomplish that, you need to tell the wrapping mixin how to find the child by re-implementing the `find()` method:

```
InputPlus.ts
------------
{
  // Overrides #find in WrappingMixin.
  find() {
    return this.querySelector<HTMLInputElement>(':scope > input') <-- note the specific selector
  }
}
```

By default the child reference in the wrapping mixin is accessed by `this.child`. However, if you want to reference the child differently inside your component override the `set child()` method. For our example this could be as simple as referencing with a class field named `input` for readability.

```
InputPlus.ts
------------

// Overrides #set child in WrappingMixin.
set child(_c: HTMLInputElement) {
  super.child = _c
  this.input = _c
}
```

\*_Note: If you override set child, be sure to set `super.child` accordingly._

# Mixins

### [Placeholder Editable Mixin](https://github.com/perfectsense/brightspot/blob/release/4.7/cms/tool-ui/src/PlaceholderEditableMixin.ts)

This mixin replaces [jquery.editableplacholder.js](https://github.com/perfectsense/brightspot/blob/237ebc60ec3fdb8a92c4f251e6e7656789d87799/cms/tool-ui/src/main/webapp/script/jquery.editableplaceholder.js) that handled placeholder functionality for text input fields as follows:

- when the input is focused, the placeholder value becomes the value of the input if the input is empty
- when the input is blurred, the placeholder should come back if the input is empty
- if the input value is the same as the placeholder,it ensures that value is not saved

Note that this only handles simple single line text and `textarea` inputs with `data-editable-placeholder` attribute. Rich text fields and code inputs have their own placeholder manager

### [Wrapping Mixin](https://github.com/perfectsense/brightspot/blob/release/4.7/cms/tool-ui/src/WrappingMixin.ts) (TBD)
