# @brandsync/angular

Angular wrappers for the Brandsync design system web components (`@brandsync/wc`), generated by
`@stencil/angular-output-target`. Components are exposed as Angular **standalone components**, so
they can be imported directly into other standalone components or NgModules without an
intermediate module.

## Install

```sh
npm install @brandsync/angular @brandsync/wc
```

`@angular/core` (`^19.0.0`) is a peer dependency and must already be present in your app.

## Usage

```ts
import { Component } from '@angular/core';
import { BsComposer } from '@brandsync/angular';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [BsComposer],
  template: `<bs-composer></bs-composer>`,
})
export class AppComponent {}
```

## Required: load the design token stylesheet

Every component's shadow-DOM CSS is written against `--bs-*` custom properties that this package does not inject on its own. Without them, components render with no visible border/background and collapse to near-zero size (no console error — `var(--bs-*)` with no fallback just resolves to its CSS initial value). This is easy to miss in an Angular app that already has its *own* differently-named design tokens (e.g. `--surface-base`) mapped onto its own stylesheets — that app-level token file does not satisfy this, since the shadow-DOM CSS only resolves `--bs-*`-prefixed names.

Add the vendored token stylesheet to your app's global styles, e.g. in `angular.json`:

```json
"styles": [
  "node_modules/@brandsync/wc/dist/brandsync-wc/brandsync-wc.css",
  "src/styles.css"
]
```

or via a plain `@import` in `src/styles.css`:

```css
@import '@brandsync/wc/dist/brandsync-wc/brandsync-wc.css';
```

Note: the vendored stylesheet currently contains a handful of malformed token declarations from an upstream `brandsync-tokens` export step (e.g. `--bs-5-brand-colors-jade-brand-700: {Brand Colors.teal.700};`), which Angular's build may warn about. These are pre-existing in the vendored dependency and harmless — they don't affect any token this library actually uses.

See `@brandsync/wc`'s own README ("Design tokens — required, not optional") for the full explanation.

## Events are typed

Each component's outputs are real Angular outputs whose payload is typed, so you read the value from
`$event.detail` without `$any`:

```ts
@Component({
  standalone: true,
  imports: [BsCheckbox],
  template: `<bs-checkbox (bsChange)="onChange($event.detail)">I agree</bs-checkbox>`,
})
export class Example {
  onChange(checked: boolean) {} // `$event.detail` is a `boolean` here
}
```

The emitted event is a `BsCheckboxCustomEvent<boolean>` (a `CustomEvent` whose `detail` and `target` are
typed), exported from `@brandsync/wc`.

### Known limitation: programmatic `EventEmitter` subscription

Use template bindings (`(bsChange)="..."`) or a native listener. The proxies' `@Output()` fields are
**not** connected to the element's DOM events, so `componentRef.instance.bsChange.subscribe(...)`
(for example on a component created with `createComponent`) never emits. Angular attaches a native
DOM listener for every template event binding, which is why the template form works.

Connecting the emitters would make every `(bsChange)` template binding fire **twice** (once from
the native listener, once from the output), so this is intentionally left as is. To subscribe in
code, listen on the element:

```ts
import { fromEvent } from 'rxjs';
fromEvent<BsSwitchCustomEvent<boolean>>(ref.location.nativeElement, 'bsChange').subscribe(e => {});
```

## No `CUSTOM_ELEMENTS_SCHEMA`

You do not need `CUSTOM_ELEMENTS_SCHEMA`. Importing a component class (`imports: [BsButton]`) is what
makes `<bs-button>` a known element in that component's template. If you see "`bs-button` is not a known
element", the class is not in that component's `imports`.

## Forms: `ngModel` and `formControlName`

`bs-input`, `bs-checkbox`, `bs-switch` and `bs-slider` work with Angular forms. Import the matching
`ControlValueAccessor` directive next to the component:

| Component | Accessor | Control value |
| --- | --- | --- |
| `bs-input` | `TextValueAccessor` | `string` |
| `bs-checkbox`, `bs-switch` | `BooleanValueAccessor` | `boolean` |
| `bs-slider` | `NumericValueAccessor` | `number` (not for `type="range"`, which has two values) |

```ts
import { ReactiveFormsModule } from '@angular/forms';
import { BsInput, BsCheckbox, TextValueAccessor, BooleanValueAccessor } from '@brandsync/angular';

@Component({
  standalone: true,
  imports: [ReactiveFormsModule, BsInput, TextValueAccessor, BsCheckbox, BooleanValueAccessor],
  template: `
    <form [formGroup]="form">
      <bs-input formControlName="email" label="Email"></bs-input>
      <bs-checkbox formControlName="terms">I accept the terms</bs-checkbox>
    </form>`,
})
export class SignupForm {
  form = new FormGroup({ email: new FormControl(''), terms: new FormControl(false) });
}
```

- `@angular/forms` (`^19.0.0`) is a peer dependency.
- `bs-input` updates the control when the value is **committed** (blur, Enter, and immediately for the
  number stepper, dropdown and pin), like `updateOn: 'blur'`. Use `(bsInput)` for every keystroke.
- `control.disable()` disables the component.
- `bs-radio` has no accessor yet: bind it with `[checked]` and `(bsChange)`.
- Two-way `[(value)]` / `[(checked)]` is not provided; use `ngModel` or `formControlName` instead.
- A native `<form>` also works: give the controls a `name`, and a `<bs-button type="submit">` submits it.

See **Guides / Forms (Angular)** in the Angular Storybook for a working example.

## Boolean inputs need property binding

Bind booleans with `[...]`: `<bs-checkbox [disabled]="true">`, `<bs-input [required]="true">`. A bare
attribute (`<bs-checkbox disabled>`) is **not** coerced: Angular passes it as an empty string, which the
component treats as `false`. (Stencil's Angular output target can generate a coercion transform for this
via its `booleanAttributes` option, but that currently does not compile under this package's TypeScript
configuration, so it is not enabled.)

