Learn how to organize reusable UI components and work with JavaScript frameworks in CampsiteJS.
CampsiteJS provides a
src/components/directory andmake:componentcommand to help you organize reusable pieces of your interface.
Overview
You can place component files (HTML snippets, partials, or small JS widgets) in src/components/. This is purely an organizational convention — CampsiteJS does not process this folder specially.
For interactive JavaScript components, simply include the library you want (Alpine.js, Vue, etc.) in your layout. See the Working with Assets guide for recommended patterns.
Setup
CampsiteJS does not require any special configuration to use components or JavaScript libraries.
During Project Creation
When creating a new CampsiteJS project, you can select frameworks interactively:
npm create campsitejs@latest
You’ll be prompted:
? Sprinkle in JS frameworks?
◉ Alpine.js
◯ Vue.js
Directory Structure
src/
components/
HelloCampsite.vue # Vue component
alpine-card.html # Alpine component
pages/
index.njk
layouts/
base.njk
Vue.js Components
Creating a Vue Component
Create a .vue file in src/components/:
<template>
<div class="counter">
<h3>{{ title }}</h3>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</div>
</template>
<script>
export default {
name: 'Counter',
props: {
title: {
type: String,
default: 'Counter'
}
},
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
},
decrement() {
this.count--
}
}
}
</script>
<style scoped>
.counter {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 0.5rem;
}
button {
margin: 0.5rem 0.25rem;
padding: 0.5rem 1rem;
cursor: pointer;
}
</style>
Using Vue Components
Include Vue components in your pages or layouts:
---
layout: base.njk
title: Home
---
<div id="app">
<h1>Welcome to CampsiteJS</h1>
<!-- Use the Counter component -->
<counter title="My Counter"></counter>
<!-- Multiple instances -->
<counter title="First Counter"></counter>
<counter title="Second Counter"></counter>
</div>
Mounting Vue
Add Vue mounting code to your layout:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
</head>
<body>
{{ content | safe }}
<!-- Vue.js -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;
// Import your components
import Counter from './components/Counter.vue';
createApp({
components: {
Counter
}
}).mount('#app');
</script>
</body>
</html>
Advanced Vue Example
<template>
<div class="todo-list">
<h3>{{ title }}</h3>
<div class="input-group">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="Add a new task..."
>
<button @click="addTodo">Add</button>
</div>
<ul>
<li
v-for="(todo, index) in todos"
:key="index"
:class="{ completed: todo.done }"
>
<input
type="checkbox"
v-model="todo.done"
>
<span>{{ todo.text }}</span>
<button @click="removeTodo(index)">×</button>
</li>
</ul>
<p class="stats">
{{ remaining }} of {{ todos.length }} remaining
</p>
</div>
</template>
<script>
export default {
name: 'TodoList',
props: {
title: {
type: String,
default: 'My Tasks'
}
},
data() {
return {
newTodo: '',
todos: []
}
},
computed: {
remaining() {
return this.todos.filter(t => !t.done).length
}
},
methods: {
addTodo() {
if (this.newTodo.trim()) {
this.todos.push({
text: this.newTodo,
done: false
})
this.newTodo = ''
}
},
removeTodo(index) {
this.todos.splice(index, 1)
}
}
}
</script>
<style scoped>
.todo-list {
max-width: 500px;
margin: 0 auto;
}
.input-group {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
input[type="text"] {
flex: 1;
padding: 0.5rem;
}
ul {
list-style: none;
padding: 0;
}
li {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-bottom: 1px solid #eee;
}
li.completed span {
text-decoration: line-through;
opacity: 0.5;
}
.stats {
margin-top: 1rem;
color: #666;
}
</style>
Alpine.js Components
Alpine.js is a lightweight alternative to Vue.js, using HTML attributes for reactivity.
Creating an Alpine Component
<div x-data="{ count: 0 }" class="alpine-counter">
<h3>Alpine Counter</h3>
<p>Count: <span x-text="count"></span></p>
<button @click="count++">Increment</button>
<button @click="count--">Decrement</button>
</div>
Using Alpine Components
Include Alpine components using Nunjucks includes:
---
layout: base.njk
title: Home
---
<h1>Welcome to CampsiteJS</h1>
<!-- Include Alpine component -->
{% include "components/alpine-counter.html" %}
Adding Alpine.js to Layout
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
</head>
<body>
{{ content | safe }}
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
</body>
</html>
Alpine Examples
Toggle Component
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open" x-transition>
<p>This content can be toggled!</p>
</div>
</div>
Dropdown Menu
<div x-data="{ open: false }" @click.away="open = false">
<button @click="open = !open">
Menu
</button>
<ul x-show="open" x-transition>
<li><a href="/about">About</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
Form with Validation
<div x-data="{
email: '',
isValid: false,
checkEmail() {
this.isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email)
}
}">
<input
type="email"
x-model="email"
@input="checkEmail()"
placeholder="Enter email"
>
<p x-show="!isValid && email.length > 0" class="error">
Please enter a valid email
</p>
<button :disabled="!isValid">
Submit
</button>
</div>
Tab Component
<div x-data="{ activeTab: 'home' }">
<div class="tabs">
<button
@click="activeTab = 'home'"
:class="{ active: activeTab === 'home' }"
>
Home
</button>
<button
@click="activeTab = 'profile'"
:class="{ active: activeTab === 'profile' }"
>
Profile
</button>
<button
@click="activeTab = 'settings'"
:class="{ active: activeTab === 'settings' }"
>
Settings
</button>
</div>
<div class="tab-content">
<div x-show="activeTab === 'home'">
<h3>Home Content</h3>
</div>
<div x-show="activeTab === 'profile'">
<h3>Profile Content</h3>
</div>
<div x-show="activeTab === 'settings'">
<h3>Settings Content</h3>
</div>
</div>
</div>
Creating Components with CLI
Use the make:component command:
# Vue component
camper make:component MyComponent.vue
# Alpine component
camper make:component my-card.html
# Multiple components
camper make:component Card.vue, Modal.vue, Dropdown.html
Generated Vue component:
<template>
<div class="my-component">
<h3>{{ title }}</h3>
<p>{{ content }}</p>
</div>
</template>
<script>
export default {
name: 'MyComponent',
props: {
title: String,
content: String
}
}
</script>
<style scoped>
.my-component {
padding: 1rem;
}
</style>
Components vs Partials
| Feature | Components | Partials |
|---|---|---|
| Purpose | Interactive UI | Reusable templates |
| Rendering | Client-side | Server-side |
| JavaScript | Yes (Vue/Alpine) | No |
| Location | src/components/ |
src/partials/ |
| Usage | Dynamic content | Static content |
| Best For | Forms, modals, counters | Headers, footers, navigation |
When to Use Components
✅ Forms with validation
✅ Interactive widgets (counters, toggles)
✅ Dynamic content loading
✅ Client-side state management
✅ Real-time updates
When to Use Partials
✅ Site header/footer
✅ Navigation menus
✅ Static content blocks
✅ SEO-important content
✅ Content that needs to be rendered immediately
Best Practices
1. Component Organization
Keep components organized by type or feature:
src/
components/
common/
Button.vue
Modal.vue
forms/
ContactForm.vue
SearchForm.vue
alpine/
toggle.html
dropdown.html
2. Prop Validation (Vue)
Always validate props:
<script>
export default {
props: {
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0,
validator: (value) => value >= 0
}
}
}
</script>
3. Scoped Styles
Use scoped styles to prevent CSS conflicts:
<style scoped>
.my-component {
/* These styles only apply to this component */
}
</style>
4. Naming Conventions
Vue components:
- PascalCase:
MyComponent.vue - Use in templates:
<my-component>
Alpine components:
- kebab-case:
my-component.html - Descriptive names:
alpine-toggle.html
5. Keep Components Small
Break large components into smaller, reusable pieces:
<!-- ❌ Too large -->
<template>
<!-- 500 lines of template code -->
</template>
<!-- ✅ Better -->
<template>
<div>
<component-header />
<component-body />
<component-footer />
</div>
</template>
Listing Components
View all components in your project:
camper list
Output:
🧩 Components (5):
• Counter.vue
• TodoList.vue
• ContactForm.vue
• alpine-counter.html
• alpine-toggle.html
CDN vs Local Installation
Using CDN (Recommended for Getting Started)
Vue.js:
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
Alpine.js:
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
Local Installation
npm install vue@3
npm install alpinejs
Then bundle with your build process (requires additional configuration).
Additional Resources
Vue.js
Alpine.js
CampsiteJS
- Partials - Server-side includes
- Make Commands - Create components with CLI
- Configuration - Enable integrations
Next Steps:
- Partials - Learn about server-side includes
- Working with Assets - CSS, JavaScript, and static files
- Make Commands - Create components with CLI