Live Demo
This example demonstrates fetching and displaying user data from the JSONPlaceholder API.
Users
Implementation
<!-- User List Component -->
<div data-component="user-list">
<div class="toolbar">
<input type="search" data-action="search">
<button data-action="refresh">Refresh</button>
</div>
<div class="loading-indicator">
<div class="spinner"></div>
<span>Loading users...</span>
</div>
<div class="users-grid"></div>
<div class="pagination">
<button data-action="prev">Previous</button>
<span class="page-info">Page <span class="current-page">1</span></span>
<button data-action="next">Next</button>
</div>
</div>
<!-- User Details Component -->
<div data-component="user-detail">
<div class="user-info"></div>
<div class="user-posts">
<h4>Recent Posts</h4>
<div class="posts-list"></div>
</div>
</div>
// User List Component
Now.component('user-list', {
state: {
users: [],
loading: false,
error: null,
page: 1,
search: '',
perPage: 6
},
async created() {
await this.loadUsers();
},
methods: {
async loadUsers() {
this.state.loading = true;
this.state.error = null;
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users?_page=${this.state.page}`
);
if (!response.ok) throw new Error('Failed to load users');
const users = await response.json();
this.state.users = this.filterUsers(users);
} catch (error) {
this.state.error = error;
NotificationManager.error('Failed to load users');
} finally {
this.state.loading = false;
}
},
filterUsers(users) {
if (!this.state.search) return users;
const search = this.state.search.toLowerCase();
return users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
);
},
async refresh() {
await this.loadUsers();
NotificationManager.success('Users refreshed');
},
search(event) {
this.state.search = event.target.value;
this.state.page = 1;
this.loadUsers();
},
async changePage(direction) {
this.state.page += direction;
await this.loadUsers();
},
showUserDetail(userId) {
const userDetail = this.element.querySelector('[data-component="user-detail"]');
userDetail.setAttribute('data-user-id', userId);
EventManager.emit('user:selected', { userId });
}
}
});
// User Detail Component
Now.component('user-detail', {
state: {
user: null,
posts: [],
loading: false
},
created() {
EventManager.on('user:selected', this.loadUserData.bind(this));
},
methods: {
async loadUserData({ userId }) {
this.state.loading = true;
try {
// Load user details
const userResponse = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
this.state.user = await userResponse.json();
// Load user posts
const postsResponse = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`
);
this.state.posts = await postsResponse.json();
} catch (error) {
NotificationManager.error('Failed to load user details');
} finally {
this.state.loading = false;
}
},
back() {
this.state.user = null;
this.state.posts = [];
this.element.style.display = 'none';
}
}
});
.demo-app {
background: var(--color-surface);
border-radius: var(--border-radius-lg);
padding: var(--spacing-4);
margin-top: var(--spacing-4);
}
.toolbar {
display: flex;
gap: var(--spacing-4);
margin-bottom: var(--spacing-4);
}
.users-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: var(--spacing-4);
margin: var(--spacing-4) 0;
}
.user-card {
background: var(--color-background);
border-radius: var(--border-radius);
padding: var(--spacing-4);
cursor: pointer;
transition: all 0.2s;
}
.user-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-4);
margin-top: var(--spacing-4);
}
.loading-indicator {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-2);
padding: var(--spacing-4);
}
.error-message {
color: var(--color-error);
padding: var(--spacing-4);
text-align: center;
}
.error-message {
font-size: var(--font-size-lg);
color: var(--color-text-light);
margin-bottom: var(--spacing-6);
}
/* User Details */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-4);
}
.user-info {
background: var(--color-background);
border-radius: var(--border-radius);
padding: var(--spacing-4);
margin-bottom: var(--spacing-4);
}
.posts-list {
display: grid;
gap: var(--spacing-4);
}
.post-card {
background: var(--color-background);
border-radius: var(--border-radius);
padding: var(--spacing-4);
}
Key Features
Async/Await
Modern async/await syntax for clean API calls
Search & Filter
Real-time search with client-side filtering
Pagination
Server-side pagination support
Error Handling
Comprehensive error handling with notifications
Loading States
Loading indicators for better UX
Components
Modular component-based architecture
Best Practices
Error Handling
Always handle API errors gracefully and provide user feedback through notifications.
try {
const response = await fetch(url);
if (!response.ok) throw new Error('API Error');
return await response.json();
} catch (error) {
NotificationManager.error(error.message);
throw error;
}
Loading States
Show loading indicators during API calls to improve user experience.
async loadData() {
this.state.loading = true;
try {
await this.fetchData();
} finally {
this.state.loading = false;
}
}
Component Communication
Use events for component communication to maintain loose coupling.
// Emit event
EventManager.emit('user:selected', { userId });
// Listen for event
EventManager.on('user:selected', this.loadUserData);