# Components

SSE's page routing model allows us to cleanly separate page-specific code in a very manageable way. But in some cases your code needs to be tied to a "component" rather than a page or path.

{% hint style="info" %}
**Component** is a significant term in Webflow, but SSE "components" aren't strictly tied to Webflow Components. We'll be discussing both here- to distinguish...

* "Components" (capitalized) will refer to Webflow Components.
* "components" (lowercase) will refer to SSE components.
  {% endhint %}

These are some examples of "components" that might use code:

* A specially configured SwiperJS setup
* A custom component you've built such as an accordion
* Specialized form validation
* A fancy multi-step form
* A cool color-picker
* Also, any Webflow Component that needs code attached to its functionality

Often, these "components" need to be reused on multiple pages, and your design team might for example duplicate a quiz or a contact form to another page at any time- ideally the dev team would not need to do *anything* for that component to automatically work on that new page.

## Goals

* Efficient code execution, only run code when it's needed
* Code isolation, e.g. the code & CSS for a multi-step form should be distinct from the rest of your source code
* Reusability. Your development work should be easy to repurpose on other projects you build.
* Webflow Component support. Take full advantage of the Webflow Team's work on components and leverage it in every way possible to maximize the finished "smart" component.
* Create a design paradigm that supports the possibility of multiple "component" instances per page.

## Implementation (v2.0+)

{% hint style="success" %}
**New in v2.0:** Components are fully implemented with automatic discovery, ComponentBase class, and automatic context detection!
{% endhint %}

### Creating a Component

Components extend `ComponentBase` for automatic element and context detection:

```typescript
import { ComponentBase, component } from '@sygnal/sse-core';

@component('accordion')
export class AccordionComponent extends ComponentBase {

  protected onPrepare(): void {
    // Synchronous setup
    // this.element and this.context automatically available
    console.log('Accordion component:', this.context.name);
  }

  protected async onLoad(): Promise<void> {
    // Asynchronous execution
    const items = this.element.querySelectorAll('.accordion-item');

    items.forEach(item => {
      item.addEventListener('click', () => {
        item.classList.toggle('open');
      });
    });
  }
}
```

### Using Components in Webflow

To use a component, add the `data-component` attribute to any element in Webflow:

1. Select the element in Webflow Designer
2. Add a custom attribute: `data-component` = `accordion`
3. The component name must match the name in the `@component` decorator

```html
<div data-component="accordion" class="accordion-wrapper">
  <!-- Your accordion HTML -->
</div>
```

### Multiple Component Instances

Components automatically support multiple instances on the same page. Each instance gets its own separate class instance:

```html
<!-- First accordion -->
<div data-component="accordion" data-component-id="main-faq">
  <!-- FAQ content -->
</div>

<!-- Second accordion -->
<div data-component="accordion" data-component-id="secondary-info">
  <!-- Info content -->
</div>
```

Both will initialize independently with their own `AccordionComponent` instance.

## Automatic Context Detection

When extending `ComponentBase`, you automatically get:

### this.element

The HTMLElement the component is bound to:

```typescript
protected async onLoad(): Promise<void> {
  // Direct access to the component's root element
  const children = this.element.querySelectorAll('.child-item');
  this.element.addEventListener('click', () => {
    console.log('Component clicked');
  });
}
```

### this.context

Component metadata automatically extracted:

```typescript
protected onPrepare(): void {
  console.log(this.context.name);           // 'accordion'
  console.log(this.context.id);             // 'main-faq'
  console.log(this.context.dataAttributes); // All data-* attributes
}
```

Available context properties:

* `this.context.name` - Component name from `data-component`
* `this.context.id` - Optional ID from `data-component-id`
* `this.context.dataAttributes` - All data-\* attributes as key-value pairs

## Accessing Page Information from Components

**New in v2.0:** Components can access the current page via the singleton pattern:

```typescript
import { ComponentBase, component, PageBase } from '@sygnal/sse-core';

@component('navigation')
export class NavigationComponent extends ComponentBase {

  protected async onLoad(): Promise<void> {
    // Access current page info
    const page = PageBase.getCurrentPage();

    if (page) {
      const info = page.getPageInfo();
      console.log('Current page ID:', info.pageId);
      console.log('Collection ID:', info.collectionId);
      console.log('Item slug:', info.itemSlug);

      // Adjust navigation based on current page
      if (info.collectionId === 'blog') {
        this.element.classList.add('blog-nav');
      }
    }
  }
}
```

Use the public `getPageInfo()` accessor (the `pageInfo` property is protected) whenever a component reads Webflow page context.

## Component Discovery and Registration

**New in v2.0:** Components are automatically discovered using the `@component` decorator!

### Automatic Discovery

Simply decorate your component class and import it:

```typescript
// src/components/accordion.ts
import { ComponentBase, component } from '@sygnal/sse-core';

@component('accordion')
export class AccordionComponent extends ComponentBase {
  // Implementation
}
```

### Register in routes.ts

Import your component files to trigger decorator registration:

```typescript
// src/routes.ts
import { RouteDispatcher, getAllPages } from "@sygnal/sse-core";
import { Site } from "./site";

// Import pages
import "./pages/home";
import "./pages/blog";

// Import components to register them
import "./components/accordion";
import "./components/navigation";
import "./components/form-validator";

export const routeDispatcher = (): RouteDispatcher => {
    const dispatcher = new RouteDispatcher(Site);
    dispatcher.routes = getAllPages();
    return dispatcher;
}
```

That's it! SSE automatically finds all `data-component` attributes in your HTML and initializes the matching components.

## Component Lifecycle

Components follow the same lifecycle as pages:

1. **onPrepare()** - Runs synchronously during `<head>` load
   * Use for quick setup that doesn't require DOM manipulation
   * Context and element are available
2. **onLoad()** - Runs asynchronously after DOM is ready
   * Use for event listeners, DOM queries, async operations
   * Full DOM access guaranteed

```typescript
@component('my-component')
export class MyComponent extends ComponentBase {

  protected onPrepare(): void {
    // Quick synchronous setup
    console.log('Component preparing:', this.context.name);
  }

  protected async onLoad(): Promise<void> {
    // Async operations, DOM manipulation
    await this.loadData();
    this.attachEventListeners();
  }

  private async loadData() {
    // Fetch data, etc.
  }

  private attachEventListeners() {
    this.element.addEventListener('click', () => {
      // Handle click
    });
  }
}
```

## Data Attributes for Configuration

Use data attributes to configure component behavior:

```html
<div
  data-component="slider"
  data-component-id="hero-slider"
  data-autoplay="true"
  data-speed="3000"
  data-loop="true"
>
  <!-- Slider content -->
</div>
```

Access in your component:

```typescript
@component('slider')
export class SliderComponent extends ComponentBase {

  protected async onLoad(): Promise<void> {
    // Access configuration from data attributes
    const config = this.context.dataAttributes;

    const autoplay = config['autoplay'] === 'true';
    const speed = parseInt(config['speed'] || '2000');
    const loop = config['loop'] === 'true';

    this.initializeSlider({ autoplay, speed, loop });
  }

  private initializeSlider(config: any) {
    // Initialize with config
  }
}
```

## Best Practices

1. **One component class per file** - Keep components organized in `/src/components/`
2. **Use descriptive names** - Component names should be clear: `accordion`, `multi-step-form`, `image-gallery`
3. **Scope your queries** - Always query within `this.element` to avoid conflicts:

   ```typescript
   // Good
   const items = this.element.querySelectorAll('.item');

   // Bad - might affect other components
   const items = document.querySelectorAll('.item');
   ```
4. **Clean up resources** - Remove event listeners if component is destroyed
5. **Use data attributes for configuration** - Keep components flexible and reusable

## Example: Complete Component

Here's a complete example of a tabs component:

```typescript
import { ComponentBase, component } from '@sygnal/sse-core';

@component('tabs')
export class TabsComponent extends ComponentBase {

  protected async onLoad(): Promise<void> {
    const tabButtons = this.element.querySelectorAll('[data-tab-button]');
    const tabPanes = this.element.querySelectorAll('[data-tab-pane]');

    tabButtons.forEach((button, index) => {
      button.addEventListener('click', () => {
        // Remove active from all
        tabButtons.forEach(btn => btn.classList.remove('active'));
        tabPanes.forEach(pane => pane.classList.remove('active'));

        // Add active to clicked
        button.classList.add('active');
        tabPanes[index]?.classList.add('active');
      });
    });

    // Activate first tab by default
    tabButtons[0]?.classList.add('active');
    tabPanes[0]?.classList.add('active');
  }
}
```

Use in Webflow:

```html
<div data-component="tabs" class="tabs-wrapper">
  <div class="tab-buttons">
    <button data-tab-button>Tab 1</button>
    <button data-tab-button>Tab 2</button>
    <button data-tab-button>Tab 3</button>
  </div>
  <div class="tab-content">
    <div data-tab-pane>Content 1</div>
    <div data-tab-pane>Content 2</div>
    <div data-tab-pane>Content 3</div>
  </div>
</div>
```
