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')