npm package: @magic-spells/cart-item
Description: Cart item web component.
Author: Cory Schulz
Latest version: 0.4.2

# Cart Item Web Component

A professional, highly-customizable Web Component for creating smooth cart item interactions in e-commerce applications. Features elegant processing states, dramatic destruction animations, and seamless integration with any cart management system.

[**Live Demo**](https://magic-spells.github.io/cart-item/demo/)

## Features

- ✨ **Smooth state transitions** - Processing and destroying animations
- 🎬 **Cinematic destruction** - Height collapse with blur, scale, and desaturation effects
- 🎯 **Modern 3-dot loader** - Sleek bouncing animation replaces outdated spinners
- 🎛️ **16+ CSS custom properties** - Complete visual customization
- 📡 **Event-driven architecture** - Clean separation between UI and logic
- 🎲 **Quantity modifier integration** - Built-in support for @magic-spells/quantity-modifier
- 🔧 **Minimal dependencies** - Only includes quantity-modifier for enhanced UX
- ⚡ **Lightweight and performant** - Optimized animations and memory management
- 📱 **Framework agnostic** - Pure Web Components work with any framework
- 🛒 **Shopify-ready** - Designed for real-world e-commerce applications
- 🎨 **Template system** - Client-side rendering from Shopify cart JSON

## Installation

```bash
npm install @magic-spells/cart-item
```

```javascript
// Import the component (includes quantity-modifier automatically)
import '@magic-spells/cart-item';
import '@magic-spells/cart-item/css/min'; // Include styles
```

Or include directly in your HTML:

```html
<script src="https://unpkg.com/@magic-spells/cart-item"></script>
<link rel="stylesheet" href="https://unpkg.com/@magic-spells/cart-item/dist/cart-item.min.css" />
```

## Usage

### Server-Side Rendered (Pre-rendered HTML)

```html
<cart-item key="shopify-line-item-123">
	<cart-item-content>
		<div class="product-image">
			<img src="product.jpg" alt="Product" />
		</div>
		<div class="product-details">
			<h4>Awesome T-Shirt</h4>
			<div class="price">$29.99</div>
			<div class="quantity">
				<label>Qty:</label>
				<quantity-modifier value="1" min="1" max="99"></quantity-modifier>
			</div>
		</div>
		<div class="actions">
			<button data-action-remove-item>Remove</button>
			<div class="total">$29.99</div>
		</div>
	</cart-item-content>

	<cart-item-processing>
		<div class="cart-item-loader"></div>
	</cart-item-processing>
</cart-item>
```

### Client-Side Rendered (Template-based)

```javascript
// Set up template once
CartItem.setTemplate((item) => {
	return `
    <div class="product-image">
      <img src="${item.featured_image}" alt="${item.product_title}">
    </div>
    <div class="product-details">
      <h4>${item.product_title}</h4>
      <div class="price">$${(item.line_price / 100).toFixed(2)}</div>
      <div class="quantity">
        <label>Qty:</label>
        <quantity-modifier value="${item.quantity}" min="1" max="99"></quantity-modifier>
      </div>
    </div>
    <div class="actions">
      <button data-action-remove-item>Remove</button>
      <div class="total">$${((item.line_price * item.quantity) / 100).toFixed(2)}</div>
    </div>
  `;
});

// Optionally customize the processing overlay (defaults to modern 3-dot loader)
CartItem.setProcessingTemplate(() => {
	return `
    <div class="custom-loader"></div>
    <span>Processing...</span>
  `;
});

// Create new cart item from Shopify cart data
const cartItem = new CartItem(shopifyItemData);
document.querySelector('#cart').appendChild(cartItem);

// Create new cart item with appearing animation
const animatedCartItem = CartItem.createAnimated(shopifyItemData);
document.querySelector('#cart').appendChild(animatedCartItem);
```

## How It Works

The cart item component manages three distinct states with smooth animations:

- **Ready**: Normal state with full interactivity
- **Processing**: Slightly scaled and blurred content with processing overlay visible
- **Destroying**: Dramatic visual effects (scale, blur, desaturate) while height animates to zero

The component emits events that bubble up to parent components (like `cart-panel`) for centralized cart management, while handling all visual states and animations internally.

## Configuration

### Attributes

| Attribute | Description                                                            | Required |
| --------- | ---------------------------------------------------------------------- | -------- |
| `key`     | Unique identifier for the cart item (Shopify line item key)            | Yes      |
| `state`   | Current visual state: `ready`, `processing`, `destroying`, `appearing` | No       |

### Required HTML Structure

| Element                  | Description                                                                    | Required |
| ------------------------ | ------------------------------------------------------------------------------ | -------- |
| `<cart-item>`            | Main container with `key` attribute                                            | Yes      |
| `<cart-item-content>`    | Content area (product info, buttons) - auto-generated when using templates     | Yes      |
| `<cart-item-processing>` | Processing overlay (modern 3-dot loader) - auto-generated when using templates | No       |

### Quantity Inputs

Cart items support two types of quantity controls that both trigger the same `cart-item:quantity-change` event:

1. **Traditional input**: `<input data-cart-quantity>` - Standard HTML number input
2. **Quantity modifier**: `<quantity-modifier>` - Enhanced component with increment/decrement buttons

The quantity-modifier component provides better UX with:

- Increment/decrement buttons for easier mobile interaction
- Built-in min/max validation
- Consistent styling and behavior
- No browser spin buttons (automatically hidden)

### Interactive Elements

| Selector                    | Description                 | Event Triggered             |
| --------------------------- | --------------------------- | --------------------------- |
| `[data-action-remove-item]` | Remove button               | `cart-item:remove`          |
| `[data-cart-quantity]`      | Quantity input field        | `cart-item:quantity-change` |
| `<quantity-modifier>`       | Quantity modifier component | `cart-item:quantity-change` |

### Data Content Elements

| Selector                    | Description                   | Auto-Updated |
| --------------------------- | ----------------------------- | ------------ |
| `[data-content-line-price]` | Line total (quantity × price) | Yes          |

These elements are automatically updated when cart data changes via `setData()` method.

Example:

```html
<!-- Minimal pre-rendered cart item -->
<cart-item key="item-123">
	<cart-item-content>
		<h4>Product Name</h4>
		<quantity-modifier value="1" min="1" max="99"></quantity-modifier>
		<button data-action-remove-item>Remove</button>
		<div data-content-line-price>$29.99</div>
	</cart-item-content>
</cart-item>

<!-- Full-featured pre-rendered cart item -->
<cart-item key="shopify-40123456789" state="ready">
	<cart-item-content>
		<!-- Your product layout here -->
	</cart-item-content>
	<cart-item-processing>
		<div class="cart-item-loader"></div>
	</cart-item-processing>
</cart-item>

<!-- Template-based cart item (JavaScript) -->
<script>
	CartItem.setTemplate(
		(item) => `
    <h4>${item.product_title}</h4>
    <quantity-modifier value="${item.quantity}" min="1" max="99"></quantity-modifier>
    <button data-action-remove-item>Remove</button>
    <div data-content-line-price>$${(item.line_price / 100).toFixed(2)}</div>
  `
	);

	// Optionally set custom processing template
	CartItem.setProcessingTemplate(() => `<div class="custom-loader">Loading...</div>`);

	const newItem = new CartItem(shopifyCartItemData);
	document.body.appendChild(newItem);
</script>
```

## Customization

### Styling

The component provides complete styling control through CSS custom properties. Style the content elements however you like:

```css
/* Customize animation timings */
cart-item {
	--cart-item-processing-duration: 300ms;
	--cart-item-destroying-duration: 800ms;
}

/* Customize visual effects */
cart-item {
	--cart-item-processing-scale: 0.95;
	--cart-item-destroying-scale: 0.8;
	--cart-item-destroying-blur: 15px;
	--cart-item-destroying-opacity: 0.1;
}

/* Style your content layout */
cart-item-content {
	display: grid;
	grid-template-columns: auto 1fr auto;
	gap: 1rem;
	padding: 1rem;
	background: white;
	border: 1px solid #e0e0e0;
	border-radius: 8px;
}

/* Custom processing overlay */
cart-item-processing {
	background: rgba(255, 255, 255, 0.95);
	backdrop-filter: blur(4px);
}
```

### CSS Variables & SCSS Variables

The component supports both CSS custom properties and SCSS variables for maximum flexibility:

| CSS Variable                      | SCSS Variable                    | Description                     | Default          |
| --------------------------------- | -------------------------------- | ------------------------------- | ---------------- |
| `--cart-item-processing-duration` | `$cart-item-processing-duration` | Processing animation duration   | 250ms            |
| `--cart-item-destroying-duration` | `$cart-item-destroying-duration` | Destroying animation duration   | 600ms            |
| `--cart-item-appearing-duration`  | `$cart-item-appearing-duration`  | Appearing animation duration    | 400ms            |
| `--cart-item-processing-scale`    | `$cart-item-processing-scale`    | Scale during processing state   | 0.98             |
| `--cart-item-destroying-scale`    | `$cart-item-destroying-scale`    | Scale during destroying state   | 0.85             |
| `--cart-item-appearing-scale`     | `$cart-item-appearing-scale`     | Scale during appearing state    | 0.9              |
| `--cart-item-processing-blur`     | `$cart-item-processing-blur`     | Blur during processing state    | 1px              |
| `--cart-item-destroying-blur`     | `$cart-item-destroying-blur`     | Blur during destroying state    | 10px             |
| `--cart-item-appearing-blur`      | `$cart-item-appearing-blur`      | Blur during appearing state     | 2px              |
| `--cart-item-destroying-opacity`  | `$cart-item-destroying-opacity`  | Opacity during destroying state | 0.2              |
| `--cart-item-appearing-opacity`   | `$cart-item-appearing-opacity`   | Opacity during appearing state  | 0.5              |
| `--cart-item-shadow-color`        | `$cart-item-shadow-color`        | Processing shadow color         | rgba(0,0,0,0.15) |
| `--cart-item-shadow-color-strong` | `$cart-item-shadow-color-strong` | Destroying shadow color         | rgba(0,0,0,0.5)  |
| `--cart-item-destroying-bg`       | `$cart-item-destroying-bg`       | Destroying background color     | rgba(0,0,0,0.1)  |

#### CSS Override Examples:

```css
/* Dramatic destruction effect */
.dramatic-cart {
	--cart-item-destroying-duration: 1200ms;
	--cart-item-destroying-scale: 0.3;
	--cart-item-destroying-blur: 20px;
	--cart-item-destroying-opacity: 0;
	--cart-item-destroying-saturate: 0;
}

/* Subtle processing state */
.subtle-cart {
	--cart-item-processing-scale: 0.99;
	--cart-item-processing-blur: 0.5px;
	--cart-item-processing-duration: 150ms;
}
```

#### SCSS Override Examples:

```scss
// Override SCSS variables before importing
$cart-item-destroying-duration: 800ms;
$cart-item-appearing-duration: 600ms;
$cart-item-processing-scale: 0.95;
$cart-item-destroying-blur: 15px;

// Import the component styles
@import '@magic-spells/cart-item/scss';

// Or import the CSS and override with CSS custom properties
@import '@magic-spells/cart-item/css';

.my-cart cart-item {
	--cart-item-destroying-duration: 800ms;
	--cart-item-appearing-duration: 600ms;
}
```

### JavaScript API

#### Static Methods

- `CartItem.setTemplate(templateFn)`: Set the template function for all cart items
- `CartItem.setProcessingTemplate(templateFn)`: Set the processing overlay template (optional, defaults to modern 3-dot bouncing loader)
- `CartItem.createAnimated(itemData)`: Create a new cart item with appearing animation

#### Instance Methods

- `setState(state)`: Change the visual state (`'ready'`, `'processing'`, `'destroying'`, `'appearing'`)
- `destroyYourself()`: Trigger the destruction animation and remove from DOM
- `setData(itemData)`: Update the cart item with new Shopify cart data
- `cartKey`: Get the cart item's unique identifier (getter)
- `state`: Get the current state (getter)
- `itemData`: Get the current item data (getter)

#### Events

The component emits custom events that bubble up for parent components to handle:

**`cart-item:remove`**

- Triggered when a remove button (`[data-action-remove]`) is clicked
- `event.detail`: `{ cartKey, element }`
- Works with both pre-rendered and template-based items

**`cart-item:quantity-change`**

- Triggered when a quantity input (`[data-cart-quantity]`) value changes
- Also triggered when a quantity-modifier component (`<quantity-modifier>`) value changes
- `event.detail`: `{ cartKey, quantity, element }`
- Works with both pre-rendered and template-based items

#### Programmatic Control

```javascript
// Set up template (do this once, before creating items)
CartItem.setTemplate((item) => {
	return `
    <div class="product-info">
      <h4>${item.product_title}</h4>
      <input data-cart-quantity value="${item.quantity}">
      <button data-action-remove-item>Remove</button>
    </div>
  `;
});

// Optionally customize processing overlay (defaults to modern 3-dot loader)
CartItem.setProcessingTemplate(() => {
	return `
    <div class="custom-loader"></div>
    <span>Processing...</span>
  `;
});

// Create from Shopify cart data
const cartItem = new CartItem(shopifyItemData);
document.querySelector('#cart').appendChild(cartItem);

// Create with appearing animation
const animatedCartItem = CartItem.createAnimated(shopifyItemData);
document.querySelector('#cart').appendChild(animatedCartItem);

// Or work with existing pre-rendered items
const existingItem = document.querySelector('cart-item');

// Change states
existingItem.setState('processing'); // Show processing overlay
existingItem.setState('ready'); // Return to normal
existingItem.setState('destroying'); // Apply destruction effects

// Trigger destruction (includes animation + DOM removal)
existingItem.destroyYourself();

// Update with new data
existingItem.setData(updatedShopifyData);

// Get item information
console.log(existingItem.cartKey); // "shopify-line-item-123"
console.log(existingItem.state); // "ready"
console.log(existingItem.itemData); // Original Shopify item data

// Listen for events
document.addEventListener('cart-item:remove', (e) => {
	console.log('Remove requested:', e.detail.cartKey);

	// Set processing state
	e.detail.element.setState('processing');

	// Make AJAX call, then destroy
	fetch('/cart/remove', {
		/* ... */
	}).then(() => e.detail.element.destroyYourself());
});

document.addEventListener('cart-item:quantity-change', (e) => {
	console.log('Quantity changed:', e.detail.quantity);
	// Handle quantity update...
});
```

#### Performance & Architecture

The component is optimized for:

- **Smooth animations**: CSS transforms and transitions with `requestAnimationFrame`
- **Memory management**: Proper event listener cleanup and duplicate call prevention
- **Separation of concerns**: UI/animation vs business logic separation
- **Event delegation**: Efficient event handling for dynamic content
- **Hardware acceleration**: CSS transforms for smooth visual effects

## Integration Examples

### Shopify Integration

```javascript
// Example cart panel integration
class CartPanel {
	constructor() {
		// Set up template for dynamically added items
		CartItem.setTemplate((item) => {
			return `
        <div class="product-image">
          <img src="${item.featured_image}" alt="${item.product_title}">
        </div>
        <div class="product-details">
          <h4>${item.product_title}</h4>
          <p>${item.variant_title || ''}</p>
          <quantity-modifier value="${item.quantity}" min="1" max="99"></quantity-modifier>
        </div>
        <div class="product-actions">
          <button data-action-remove-item>Remove</button>
          <span>$${(item.line_price / 100).toFixed(2)}</span>
        </div>
      `;
		});

		document.addEventListener('cart-item:remove', this.handleRemove.bind(this));
		document.addEventListener('cart-item:quantity-change', this.handleQuantityChange.bind(this));
	}

	async handleRemove(e) {
		const { cartKey, element } = e.detail;

		// Show processing state
		element.setState('processing');

		try {
			// Shopify AJAX API call
			await fetch('/cart/change.js', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ id: cartKey, quantity: 0 }),
			});

			// Animate out and remove
			element.destroyYourself();
		} catch (error) {
			// Revert to ready state on error
			element.setState('ready');
			console.error('Failed to remove item:', error);
		}
	}

	// Add new item from cart response
	addCartItem(shopifyItem, options = {}) {
		const cartItem = options.animate
			? CartItem.createAnimated(shopifyItem)
			: new CartItem(shopifyItem);
		this.cartContainer.appendChild(cartItem);
	}

	async handleQuantityChange(e) {
		const { cartKey, quantity, element } = e.detail;

		// Optional: Show processing state for quantity changes
		// element.setState('processing');

		try {
			await fetch('/cart/change.js', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ id: cartKey, quantity }),
			});

			// Update UI with new totals
			this.updateCartTotals();
		} catch (error) {
			console.error('Failed to update quantity:', error);
		}
	}
}

new CartPanel();
```

### Vanilla JavaScript Integration

```javascript
// Example without any framework
class SimpleCartManager {
	constructor() {
		// Set up template for new items
		CartItem.setTemplate((item) => {
			return `
        <div class="item-info">
          <h4>${item.product_title}</h4>
          <quantity-modifier value="${item.quantity}" min="1" max="99"></quantity-modifier>
          <button data-action-remove-item>Remove</button>
          <span>$${((item.line_price * item.quantity) / 100).toFixed(2)}</span>
        </div>
      `;
		});

		document.addEventListener('cart-item:remove', this.handleRemove.bind(this));
		document.addEventListener('cart-item:quantity-change', this.handleQuantityChange.bind(this));
	}

	handleRemove(e) {
		const { cartKey, element } = e.detail;
		console.log(`Removing item: ${cartKey}`);

		// Show processing state
		element.setState('processing');

		// Simulate API call
		setTimeout(() => {
			element.destroyYourself();
		}, 1000);
	}

	handleQuantityChange(e) {
		const { cartKey, quantity } = e.detail;
		console.log(`Item ${cartKey} quantity changed to ${quantity}`);

		// Update your cart state/UI as needed
		this.updateCartDisplay();
	}

	// Add new item from cart data
	addNewItem(cartItemData, options = {}) {
		const cartItem = options.animate
			? CartItem.createAnimated(cartItemData)
			: new CartItem(cartItemData);
		document.querySelector('#cart-container').appendChild(cartItem);
	}

	updateCartDisplay() {
		// Your cart update logic here
	}
}

// Initialize
new SimpleCartManager();
```

## Browser Support

- Chrome 54+
- Firefox 63+
- Safari 10.1+
- Edge 79+

All modern browsers with Web Components support.

## License

MIT