Implement Dynamic Sort & Filter with Svelte
Overview
This specification outlines the migration of the TagColumn and filtering functionality from vanilla JavaScript in Astro to Svelte components. The goal is to leverage Svelte's reactive state management and component system to create a more maintainable and performant implementation.
Current Implementation Analysis
The current implementation in Astro uses vanilla JavaScript for:
- Managing tag selection state
- Updating the URL with selected tags
- Filtering and sorting cards based on tag matches
- Manual DOM manipulation for reordering cards
Key pain points:
- Manual DOM manipulation is error-prone and complex
- State management requires significant boilerplate
- Event handling is verbose and requires manual setup
- UI updates need explicit handling
Proposed Svelte Implementation
Component Structure
text
site/src/components/
├── tag-components/
│ ├── TagColumn.svelte # Main tag filtering component
│ ├── TagChip.svelte # Individual tag display
│ ├── TagSearch.svelte # Tag search input
│ └── SortControls.svelte # Sort type controls
└── tool-components/
├── CardGrid.svelte # Grid layout for tool cards
└── ToolCard.svelte # Individual tool card Component Interfaces
TagColumn.svelte
typescript
interface TagColumnProps {
tags: Array<{
tag: string;
count: number;
}>;
initialSelectedTags?: string[];
onTagsChange?: (selectedTags: string[]) => void;
} TagChip.svelte
typescript
interface TagChipProps {
tag: string;
count?: number;
selected?: boolean;
interactive?: boolean;
} CardGrid.svelte
typescript
interface CardGridProps {
tools: Tool[];
selectedTags: string[];
sortType: 'relevance' | 'alpha-asc' | 'alpha-desc' | 'date';
}
interface Tool {
id: string;
title: string;
description: string;
tags: string[];
// ... other tool properties
} State Management
Store Definition
typescript
// stores/tagStore.ts
import { writable, derived } from 'svelte/store';
interface TagState {
availableTags: Array<{tag: string; count: number}>;
selectedTags: string[];
sortType: 'alpha-asc' | 'alpha-desc' | 'frequency-asc' | 'frequency-desc';
}
export const tagState = writable<TagState>({
availableTags: [],
selectedTags: [],
sortType: 'frequency-desc'
});
// Derived store for sorted tags
export const sortedTags = derived(tagState, ($state) => {
const { availableTags, selectedTags, sortType } = $state;
return getSortedTags(availableTags, sortType, selectedTags);
}); Key Features
1. Reactive Tag Selection
svelte
<!-- TagColumn.svelte -->
<script lang="ts">
import { tagState } from '../stores/tagStore';
import TagChip from './TagChip.svelte';
$: sortedTags = getSortedTags(
$tagState.availableTags,
$tagState.sortType,
$tagState.selectedTags
);
function toggleTag(tag: string) {
tagState.update(state => ({
...state,
selectedTags: state.selectedTags.includes(tag)
? state.selectedTags.filter(t => t !== tag)
: [...state.selectedTags, tag]
}));
}
</script>
<div class="tag-column">
<SortControls />
<TagSearch />
{#each sortedTags as {tag, count}}
<TagChip
{tag}
{count}
selected={$tagState.selectedTags.includes(tag)}
on:click={() => toggleTag(tag)}
/>
{/each}
</div> 2. URL Synchronization
typescript
// utils/urlSync.ts
import { tagState } from '../stores/tagStore';
import { browser } from '$app/env';
// Subscribe to state changes and update URL
tagState.subscribe(state => {
if (!browser) return;
const url = new URL(window.location.href);
if (state.selectedTags.length > 0) {
url.searchParams.set('tags', state.selectedTags.join(','));
} else {
url.searchParams.delete('tags');
}
history.pushState({}, '', url);
});
// Initialize state from URL
export function initFromUrl() {
if (!browser) return;
const url = new URL(window.location.href);
const tags = url.searchParams.get('tags')?.split(',') || [];
if (tags.length > 0) {
tagState.update(state => ({
...state,
selectedTags: tags
}));
}
} 3. Dynamic Card Filtering
svelte
<!-- CardGrid.svelte -->
<script lang="ts">
import { tagState } from '../stores/tagStore';
import type { Tool } from '../types';
export let tools: Tool[];
$: filteredTools = filterTools(tools, $tagState.selectedTags);
$: sortedTools = sortToolsByRelevance(filteredTools, $tagState.selectedTags);
function filterTools(tools: Tool[], selectedTags: string[]): Tool[] {
if (selectedTags.length === 0) return tools;
return tools.filter(tool =>
selectedTags.some(tag => tool.tags.includes(tag))
);
}
function sortToolsByRelevance(tools: Tool[], selectedTags: string[]): Tool[] {
return [...tools].sort((a, b) => {
const aMatches = selectedTags.filter(tag => a.tags.includes(tag)).length;
const bMatches = selectedTags.filter(tag => b.tags.includes(tag)).length;
return bMatches - aMatches;
});
}
</script>
<div class="card-grid">
{#each sortedTools as tool (tool.id)}
<ToolCard {tool} matchCount={getMatchCount(tool.tags)} />
{/each}
</div> Integration with Astro
Astro Islands Architecture
The migration to Svelte components will leverage Astro's Islands Architecture, which provides several key benefits:
- Partial Hydration
- Only interactive components are hydrated with JavaScript
- Static content remains as lightweight HTML/CSS
- Each component is hydrated independently
- Client Directives
client:load- Hydrate component immediately on page loadclient:idle- Hydrate when browser is idleclient:visible- Hydrate when component enters viewport- Choose the most appropriate directive for each component's use case
- Component Implementation
astro
---
// TagColumn.astro
import TagColumnSvelte from './TagColumnSvelte.svelte';
---
<TagColumnSvelte client:load /> Svelte Integration Setup
- Installation and Configuration
bash
pnpm astro add svelte - Configuration Files
javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import svelte from '@astrojs/svelte';
export default defineConfig({
integrations: [svelte()]
});
// svelte.config.js
import { vitePreprocess } from '@astrojs/svelte';
export default {
preprocess: vitePreprocess()
}; - TypeScript Support
typescript
// src/env.d.ts
/// <reference types="astro/client" />
/// <reference types="svelte" /> Component Hydration Strategy
- TagColumn Component
- Use
client:loadfor immediate interactivity - Critical for user interaction with filtering
- TagChip Components
- Use
client:visiblefor performance optimization - Only hydrate chips when they enter viewport
- CardGrid Component
- Use
client:idlefor non-critical interactivity - Allow initial page load to prioritize tag interaction
astro
---
import TagColumn from '../components/TagColumn.svelte';
import CardGrid from '../components/CardGrid.svelte';
---
<div class="layout">
<TagColumn client:load />
<CardGrid client:idle tools={tools} />
</div> Accessibility Enhancements
- Keyboard Navigation
svelte
<!-- TagChip.svelte -->
<button
role="checkbox"
aria-checked={selected}
tabindex="0"
on:click
on:keydown={e => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
dispatch('click');
}
}}
>
{tag}
{#if count}
<span class="tag-count" aria-label="used {count} times">{count}</span>
{/if}
</button> - Screen Reader Support
svelte
<!-- TagColumn.svelte -->
<div
class="tag-column"
role="region"
aria-label="Tag filters"
>
<div class="selected-tags" role="group" aria-label="Selected tags">
{#each $tagState.selectedTags as tag}
<TagChip {tag} selected />
{/each}
</div>
</div> Styling and Animation Guidelines
Component Styling
- Direct Component Styling
scss
<!-- TagChip.svelte -->
<style lang="scss">
.tag-chip {
// Direct component styling for consistent behavior
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: var(--transform-elevation-medium);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
}
</style> - Container Spacing for Hover Effects
scss
<!-- TagColumn.svelte -->
<style lang="scss">
.tag-column {
// Ensure hover effects aren't clipped
padding: 1rem;
gap: 0.75rem;
overflow-y: auto;
// Prevent hover effects from being cut off
.tag-list {
padding: 0.5rem;
margin: -0.5rem;
}
}
</style> - Text Wrapping for Markdown Content
scss
<!-- ToolCard.svelte -->
<style lang="scss">
.text-wrapper {
display: inline-block;
width: 100%;
white-space: normal;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
.tool-card__header {
display: flex;
flex-direction: column;
width: 100%;
gap: 0.5em;
}
</style> Animation System
- Hover Animations
scss
<!-- animations.scss -->
:root {
--transform-elevation-medium: translateY(-4px);
--transform-elevation-small: translateY(-2px);
--shadow-elevation: 0 8px 16px rgba(0, 0, 0, 0.2);
}
.hover-elevate {
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: var(--transform-elevation-medium);
box-shadow: var(--shadow-elevation);
}
} - Transition Properties
scss
<!-- transitions.scss -->
.tag-transition {
transition:
transform 0.2s ease,
box-shadow 0.2s ease,
background-color 0.2s ease;
}
// Respect user preferences
@media (prefers-reduced-motion: reduce) {
.tag-transition {
transition: none;
}
} - Z-Index Hierarchy
scss
<!-- z-index.scss -->
:root {
--z-index-tag-hover: 2;
--z-index-tag-column: 1;
--z-index-base: 0;
}
.tag-chip {
position: relative;
z-index: var(--z-index-base);
&:hover {
z-index: var(--z-index-tag-hover);
}
} These styling guidelines ensure:
- Consistent hover effects across components
- Proper handling of Markdown content
- Accessible animations with reduced motion support
- Clear z-index hierarchy for nested components
- Proper spacing for hover effect visibility
Mobile Responsiveness
scss
.tag-column {
@media (max-width: 768px) {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
transform: translateY(calc(100% - 3rem));
transition: transform 0.3s ease;
&.expanded {
transform: translateY(0);
}
}
} Migration Strategy
- Phase 1: Component Setup
- Create new Svelte component files
- Set up TypeScript interfaces
- Implement basic store structure
- Phase 2: Core Functionality
- Implement tag selection and filtering
- Add sorting controls
- Set up URL synchronization
- Phase 3: UI Polish
- Add animations and transitions
- Implement mobile responsiveness
- Enhance accessibility features
- Phase 4: Testing & Integration
- Write unit tests for components
- Test browser compatibility
- Verify accessibility compliance
Benefits
- Development Experience
- Reduced boilerplate code
- Better type safety with TypeScript
- Easier state management
- More maintainable codebase
- Performance
- Less manual DOM manipulation
- Optimized reactivity
- Better memory management
- User Experience
- Smoother transitions
- More responsive interface
- Better accessibility
- Improved mobile experience
Dependencies
- Svelte
- TypeScript
- svelte-preprocess
- @sveltejs/kit (for routing and SSR)
Testing Requirements
- Unit Tests
- Component rendering
- State management
- URL synchronization
- Sorting and filtering logic
- Integration Tests
- Component interactions
- Store updates
- URL handling
- Mobile responsiveness
- Accessibility Tests
- Screen reader compatibility
- Keyboard navigation
- ARIA attributes
- Color contrast
Deployment Considerations
- Build Process
- Update vite.config.js for Svelte
- Configure TypeScript
- Set up component preprocessing
- Performance Monitoring
- Add metrics for component rendering
- Track state updates
- Monitor bundle size
- Browser Support
- Verify compatibility with target browsers
- Add necessary polyfills
- Test in different environments