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:
<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:
<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:
<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.
<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:
<!-- 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.
<!-- 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:
<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:
<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:
<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.
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:
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:
<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:
<!-- 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.
