Skip to content

Components

Components are the building blocks of Peak.js applications. They encapsulate HTML, CSS, and JavaScript into reusable, self-contained units that can be composed together to build complex user interfaces.

Component Structure

A Peak.js component is a single HTML file with three optional sections:

html
<template>
  <!-- HTML template goes here -->
</template>

<script>
  // JavaScript class goes here
  export default class {
    // Component logic
  }
</script>

<style>
  /* CSS styles go here */
</style>

Template Section

The <template> contains the component's HTML structure:

html
<template>
  <div class="card">
    <header>
      <h2 x-text="title" />
      <button @click="toggle" x-text="isExpanded ? 'Collapse' : 'Expand'" />
    </header>

    <main x-show="isExpanded" x-transition>
      <slot />
    </main>
  </div>
</template>

Script Section

The <script> contains the component's JavaScript logic:

html
<script>
export default class {

  // initialize reactive component state
  initialize() {
    this.title = this.$prop('title')
    this.expanded = this.$prop('expanded')
    this.isExpanded = this.expanded || false
  }

  // component methods
  toggle() {
    this.isExpanded = !this.isExpanded
    this.$emit('toggle', { expanded: this.isExpanded })
  }

  // lifecycle hooks
  mounted() {
    console.log('Component mounted')
  }
  teardown() {
    console.log('Component destroyed')
  }
}
</script>

Style Section

The <style> section contains CSS for the component. The styles are scoped just to this component. This means that you are free to use simple, low-specificity selectors, without worrying that styles will leak into child components, or other part of the document.

html
<style>
/* styles will apply only to this component */
button {
  background: #38f;
  border: none;
  border-radius: 4px;
  color: white;
  padding: 8px 16px;
}
header {
  align-items: center;
  display: flex;
  background: #eef;
  justify-content: space-between;
  padding: 16px;
}
.card {
  border: 1px solid #eee;
  border-radius: 8px;
}
</style>

Props

Props are how to provide data to components. Props are like regular HTML element attributes, except that they can reference complex data types like objects and arrays; and they are reactive. That means if a parent component passes an array as a prop, when the array changes, the child will reflect the change immediately.

Defining Props

Declare reactive props with $prop() in the the initialize() lifecycle method:

html
<!-- components/x-user-card.html -->
<template>
  <div class="user-card" :class="`size-${size}`">
    <img :src="user.avatar" :alt="user.name">
    <p x-text="user.email"></p>
    <span :class="`status ${user.status}`" x-text="user.status"></span>
  </div>
</template>

<script>
export default class {
  initialize() {
    this.name = this.$prop('name')
    this.size = this.$prop('size')
  }
}
</script>

Using Props

Pass props using as attributes. When the attribute name starts with a : then the value is evaluated dynamically as an expression.

html
<!-- static props -->
<x-user-card :user="currentUser" show-actions="true" />

<!-- dynamic props -->
<x-user-card
  :user="user"
  :show-actions="user.id === currentUser.id"
/>

<!-- loop with props -->
<x-user-card
  x-for="user in users"
  :key="user.id"
  :user="user"
  :show-actions="canEdit(user)"
  @edit="handleEditUser">
/>

Lifecycle Methods

initialize()

Called when the component is first created, before mounting:

html
<script>
export default class {
  initialize() {
    // set initial state
    this.count = 0
    this.items = []

    // set up watchers
    this.$watch('count', () => {
      console.log('Count changed:', this.count)
    })

    // initialize external libraries
    this.setupAnalytics()
  }

  setupAnalytics() {
    // initialize analytics that don't need DOM
    this.analytics = new Analytics({
      userId: this.userId,
      component: 'x-counter'
    })
  }
}
</script>

mounted()

Called after the component is mounted to the DOM:

html
<script>
export default class {
  async mounted() {
    // DOM is available here
    console.log('Component element:', this)

    // access refs
    if (this.$refs.canvas) {
      this.initializeChart()
    }

    // set up DOM event listeners
    this.setupKeyboardShortcuts()

    // load initial data
    await this.loadData()

    // Initialize third-party libraries that need DOM
    this.initializeLibraries()
  }

  initializeChart() {
    const ctx = this.$refs.canvas.getContext('2d')
    this.chart = new Chart(ctx, this.chartConfig)
  }

  setupKeyboardShortcuts() {
    document.addEventListener('keydown', this.handleKeydown.bind(this))
  }

  async loadData() {
    this.loading = true
    try {
      this.data = await fetch('/api/data').then(r => r.json())
    } finally {
      this.loading = false
    }
  }
}
</script>

teardown()

Called when the component is removed from the DOM:

html
<script>
export default class {
  initialize() {
    this.timers = []
    this.eventListeners = []
  }

  mounted() {
    // set up timer
    const timer = setInterval(() => {
      this.updateTime()
    }, 1000)
    this.timers.push(timer)

    // set up event listener
    const listener = this.handleResize.bind(this)
    window.addEventListener('resize', listener)
    this.eventListeners.push({ event: 'resize', listener })
  }

  teardown() {
    // clean up timers
    this.timers.forEach(timer => clearInterval(timer))

    // clean up event listeners
    this.eventListeners.forEach(({ event, listener }) => {
      window.removeEventListener(event, listener)
    })

    // clean up third-party libraries
    if (this.chart) {
      this.chart.destroy()
    }

    // cancel ongoing requests
    if (this.abortController) {
      this.abortController.abort()
    }

    console.log('Component cleaned up')
  }
}
</script>

Component Registration

Register components using the component() function. Components are registered globally as custom elements, and so can be used directly anywhere in the document.

javascript
import { component } from './peak.js'

component('x-button', './components/x-button.html')
component('x-modal', './components/x-modal.html')
component('x-form', './components/x-form.html')

Extending Native Elements

Components can extend native HTML elements, allowing you to create specialized versions of standard elements like table rows, list items, or form controls. This is particularly useful for creating components that need to fit into specific HTML structures or inherit native element behavior.

To create a component that extends a native element, pass an options object as the third parameter to the component() function with an extends property:

javascript
component('x-table-row', './components/x-table-row.html', { extends: 'tr' })

When using these components, use the is attribute on the native element you're extending:

html
<table>
  <tr is="x-table-row"></tr>
  <tr is="x-table-row"></tr>
  <tr is="x-table-row"></tr>
</table>

And here's an example table row component:

html
<!-- components/x-table-row.html -->
<template>
  <td>Column 1</td>
  <td>Column 2</td>
</template>

This approach allows you to create reusable table row components that maintain all the semantics and behavior of native <tr> elements while adding your custom functionality.