Passing Data from Server to Client
Work in progress
This page is a work in progress. Help complete it by contributing on GitHub!
- Understanding the Client-Server Boundary
- Approaches to Passing Data
- Choosing the Right Approach
- Modern ES Modules and TypeScript
- Security Considerations
- Debugging Data Transfer
- What's Next?
Modern web applications need to pass data from the backend (PHP) to the frontend (JavaScript/Vue). UserFrosting provides several strategies for this, depending on your needs.
This page covers the main approaches: global configuration objects, page-specific data, component props, and data attributes. Choose the right approach based on your use case.
Understanding the Client-Server Boundary
Before we dive in, remember that JavaScript cannot directly access PHP variables. When your browser loads a page:
- Server renders: PHP/Twig generates HTML with embedded data
- Client receives: Browser gets a static HTML document
- JavaScript runs: Your code accesses the embedded data
Any data you need on the client must be included in the HTML response. Changes in JavaScript don't affect PHP variables unless you make an API request.
Note
This is a fundamental limitation of web architecture, not a UserFrosting quirk. The server and client are separate programs running on different machines.
Approaches to Passing Data
1. Global Configuration Object (Best for Site-Wide Settings)
UserFrosting creates a global site JavaScript object on every page containing configuration values. This is ideal for settings you'll need across multiple pages.
Available in: window.site or just site
Common values:
site.uri.public- Base URL of your site (e.g.,https://example.com)site.debug.ajax- Whether AJAX debugging is enabledsite.csrf.name- CSRF token namesite.csrf.value- CSRF token value
Example usage in TypeScript:
// Making an API request with base URL
import axios from 'axios'
const response = await axios.get(`${site.uri.public}/api/users`)
// Including CSRF token in POST requests
await axios.post(`${site.uri.public}/api/users`, userData, {
headers: {
[site.csrf.keys.name]: site.csrf.name,
[site.csrf.keys.value]: site.csrf.value
}
})
How it works: The site object is generated by core/templates/pages/partials/config.js.twig and pulls values from your configuration files.
Customizing: To add custom values to site:
-
Option A: Extend your sprinkle's configuration:
// app/sprinkles/mysprinkle/config/default.php return [ 'site' => [ 'myapp' => [ 'feature_flag' => true, 'api_version' => 'v2' ] ] ]; -
Option B: Override
config.js.twigin your sprinkle to add custom logic
Warning
Everything in site is visible in the page source! Never put sensitive information (API keys, passwords, private user data) in this object.
2. Page-Specific Data (Best for Page Initialization)
For data specific to a single page, use the page global object. This is populated by passing data to the page key when rendering your Twig template.
PHP Controller:
public function displayUsers(Request $request, Response $response): Response
{
return $this->view->render($response, 'pages/users.html.twig', [
'page' => [
'api_endpoint' => '/api/users',
'per_page' => 25,
'initial_data' => $this->userRepository->all()
]
]);
}
Twig Template (include in your page):
{% block scripts_page %}
<script>
{% include "pages/partials/page.js.twig" %}
</script>
{% endblock %}
JavaScript/TypeScript Access:
// The page object is now available globally
console.log(page.api_endpoint) // "/api/users"
console.log(page.per_page) // 25
// Use it in your Vue components or vanilla JS
const users = page.initial_data
This approach works well for validation rules, API endpoints, initial data loads, and feature flags specific to a page.
3. Vue Component Props (Best for Component Data)
When using Vue 3 components, pass data directly as props through the component's mounting HTML.
Twig Template:
<div id="user-profile"
data-user-id="{{ user.id }}"
data-username="{{ user.username }}"
data-email="{{ user.email }}">
</div>
Vue Component (UserProfile.vue):
<template>
<div>
<h2>{{ username }}</h2>
<p>Email: {{ email }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const userId = ref<number>(0)
const username = ref<string>('')
const email = ref<string>('')
onMounted(() => {
const el = document.getElementById('user-profile')
if (el) {
userId.value = parseInt(el.dataset.userId || '0')
username.value = el.dataset.username || ''
email.value = el.dataset.email || ''
}
})
</script>
Or, better yet, pass props when creating the Vue app:
Twig Template:
<div id="user-profile"></div>
<script>
window.userProfileData = {
userId: {{ user.id }},
username: "{{ user.username }}",
email: "{{ user.email }}"
};
</script>
TypeScript:
import { createApp } from 'vue'
import UserProfile from './components/UserProfile.vue'
createApp(UserProfile, {
userId: window.userProfileData.userId,
username: window.userProfileData.username,
email: window.userProfileData.email
}).mount('#user-profile')
4. Data Attributes (Best for Small Amounts of Data)
For simple, element-specific data, use HTML5 data-* attributes. This is ideal when your JavaScript needs to know about specific elements on the page.
Twig Template:
<button
class="delete-user"
data-user-id="{{ user.id }}"
data-user-name="{{ user.username }}">
Delete User
</button>
TypeScript:
document.querySelectorAll('.delete-user').forEach(button => {
button.addEventListener('click', async (e) => {
const target = e.currentTarget as HTMLButtonElement
const userId = target.dataset.userId
const userName = target.dataset.userName
if (confirm(`Delete user ${userName}?`)) {
await deleteUser(userId)
}
})
})
Vue 3 Alternative (using template refs):
<template>
<button @click="handleDelete" :data-user-id="userId">
Delete User
</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
userId: number
userName: string
}>()
const handleDelete = async () => {
if (confirm(`Delete user ${props.userName}?`)) {
await deleteUser(props.userId)
}
}
</script>
5. API Requests (Best for Dynamic Data)
For data that changes frequently or is too large to embed, fetch it via API calls after the page loads.
TypeScript:
import axios from 'axios'
interface User {
id: number
username: string
email: string
}
// Fetch data after page load
async function loadUsers(): Promise<User[]> {
const response = await axios.get<User[]>(`${site.uri.public}/api/users`)
return response.data
}
// Use in your code
const users = await loadUsers()
Vue 3 Composable (reusable logic):
// composables/useUsers.ts
import { ref, Ref } from 'vue'
import axios from 'axios'
export function useUsers() {
const users: Ref<User[]> = ref([])
const loading = ref(false)
const error = ref<Error | null>(null)
async function fetchUsers() {
loading.value = true
error.value = null
try {
const response = await axios.get(`${site.uri.public}/api/users`)
users.value = response.data
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
return { users, loading, error, fetchUsers }
}
Choosing the Right Approach
Use this decision tree:
| Data Type | Best Approach | Why |
|---|---|---|
| Site-wide config (URLs, CSRF) | Global site object |
Available everywhere, cached |
| Page-specific settings | Global page object |
Page scope, easy access |
| Component initialization | Vue props | Type-safe, reactive |
| Element metadata | Data attributes | Semantic, standard HTML |
| Large/dynamic data | API requests | Reduces initial page size |
| Real-time data | API + polling/WebSockets | Always current |
Modern ES Modules and TypeScript
With Vite and modern JavaScript, avoid global variables when possible. Instead, export and import values:
config.ts:
// Re-export site config with types
export interface SiteConfig {
uri: {
public: string
}
csrf: {
name: string
value: string
keys: {
name: string
value: string
}
}
}
// Access global site object with type safety
export const siteConfig: SiteConfig = (window as any).site
Usage:
import { siteConfig } from './config'
// Now fully typed!
const url = `${siteConfig.uri.public}/api/users`
Security Considerations
Warning
Never expose sensitive data to the client:
- Database credentials
- API keys or secrets
- Other users' private information
- Internal system paths
- Unfiltered user input (XSS risk)
Safe to expose:
- Public URLs and endpoints
- CSRF tokens (designed for client use)
- Current user's own data
- Public configuration settings
Remember: Anything in the HTML can be viewed by the user. Treat all client-side data as potentially compromised.
Debugging Data Transfer
View Available Data
Open your browser's console and type:
console.log('Site config:', site)
console.log('Page data:', page)
Validate JSON Structure
If data isn't working as expected, check that your PHP array converts correctly to JSON:
// Good - simple types convert cleanly
'count' => 42,
'name' => 'John',
'active' => true
// Problematic - resource types don't serialize
'database' => $pdo, // ❌ Won't work
// Solution - extract only the data you need
'user' => [
'id' => $user->id,
'name' => $user->username
]
What's Next?
Now that you know how to pass data from server to client, learn how to use it in:
- Vue Components: Build reactive UIs with the data
- Forms: Submit data back to the server
- Tables: Display collections of data
Tip
Start simple with global objects (site, page), then graduate to Vue props and API calls as your application grows in complexity.