box-me
v0.0.5
Published
[](https://badge.fury.io/js/box-me) [](https://opensource.org/licenses/MIT) [="onItemsChange($event)"
(itemClick)="onItemClick($event)">
<ng-template #itemTemplate let-item let-rowIndex="rowIndex" let-colIndex="colIndex">
<div class="custom-item" [class.selected]="item.isVisible">
<img [src]="item.imageUrl" *ngIf="item.isVisible" [alt]="item.id">
<div class="placeholder" *ngIf="!item.isVisible">
{{ rowIndex }},{{ colIndex }}
</div>
</div>
</ng-template>
</lib-box>
`,
styles: [`
.custom-item {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ccc;
border-radius: 4px;
}
.custom-item.selected {
border-color: #007bff;
background-color: #f8f9fa;
}
.placeholder {
color: #6c757d;
font-size: 12px;
}
`]
})
export class ExampleComponent {
myBox: Box;
selectedItem: Item | null = {
id: 'item-1',
imageUrl: 'https://via.placeholder.com/50',
isVisible: true
};
constructor(private boxFactory: BoxFactoryService) {
this.myBox = this.boxFactory.createBox(
'My First Box',
2,
2,
'A simple 2x2 grid box'
);
}
onItemsChange(box: Box) {
console.log('Box updated:', box);
console.log('Total spots:', box.totalSpots);
console.log('Current spots:', box.spots);
}
onItemClick(event: any) {
console.log('Item clicked:', event);
}
}Using Demo Presets
import { Component } from '@angular/core';
import { BoxComponent } from 'box-me';
import { Box, DemoBoxesService } from 'box-me';
@Component({
selector: 'app-demo',
standalone: true,
imports: [BoxComponent],
template: `
<div class="demo-container">
<h2>Demo Layouts</h2>
<div class="demo-section">
<h3>Complex Layout with Styling</h3>
<lib-box [box]="complexBox" (itemsChange)="onBoxChange($event)">
<ng-template #itemTemplate let-item>
<div class="demo-item">{{ item.name || 'Empty' }}</div>
</ng-template>
</lib-box>
</div>
<div class="demo-section">
<h3>Clean Layout</h3>
<lib-box [box]="cleanBox" (itemsChange)="onBoxChange($event)">
<ng-template #itemTemplate let-item>
<div class="demo-item clean">{{ item.name || 'Empty' }}</div>
</ng-template>
</lib-box>
</div>
<div class="demo-section">
<h3>Simple 5x5 Grid</h3>
<lib-box [box]="simpleBox" (itemsChange)="onBoxChange($event)">
<ng-template #itemTemplate let-item>
<div class="demo-item simple">{{ item.name || 'Empty' }}</div>
</ng-template>
</lib-box>
</div>
</div>
`,
styles: [`
.demo-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
margin-bottom: 40px;
}
.demo-item {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
background: #f8f9fa;
}
.demo-item.clean {
background: #e9ecef;
border-color: #adb5bd;
}
.demo-item.simple {
background: #fff;
border-color: #007bff;
}
`]
})
export class DemoComponent {
complexBox: Box;
cleanBox: Box;
simpleBox: Box;
constructor(private demoBoxes: DemoBoxesService) {
this.complexBox = this.demoBoxes.getDemoBigMiddleSpotBox();
this.cleanBox = this.demoBoxes.getBigMiddleSpotBox();
this.simpleBox = this.demoBoxes.get5x5Box();
}
onBoxChange(box: Box) {
console.log('Box changed:', box.name, 'Total spots:', box.totalSpots);
}
}📋 Data Models
Box Interface
interface Box {
name: string; // Display name for the box
description?: string; // Optional description
rows: number; // Number of rows in the grid
cols: number; // Number of columns in the grid
spots: number; // Current box capacity (rows * cols)
totalSpots: number; // Total capacity including nested boxes
items: BoxItem[][]; // 2D array of items (Item or nested Box)
config?: BoxConfig; // Optional configuration
rowStyles?: { [rowIndex: number]: RowColumnStyle };
colStyles?: { [rowIndex: number]: { [colIndex: number]: RowColumnStyle } };
itemStyles?: { [rowIndex: number]: { [colIndex: number]: RowColumnStyle } };
placeholderStyles?: { [rowIndex: number]: { [colIndex: number]: RowColumnStyle } };
}Item Interface
interface Item {
id: string; // Unique identifier
imageUrl: string; // URL for item image
isVisible: boolean; // Visibility state
data?: any; // Custom data
}BoxConfig Interface
interface BoxConfig {
general?: GeneralConfig;
single?: SingleSelectionConfig;
multiple?: MultipleSelectionConfig;
debug?: boolean;
}
interface GeneralConfig {
selectionMode?: 'single' | 'multiple';
maxItemSize?: string;
boxBorderRadius?: string;
boxPadding?: string;
rowsGap?: string;
colsGap?: string;
rowMinHeight?: string;
}
interface SingleSelectionConfig {
clearSpotOnClickWhenSelected?: boolean;
}
interface MultipleSelectionConfig {
enableCrossBoxHighlighting?: boolean;
continueHighlightingAfterCompletingTheSelection?: boolean;
}🎨 Advanced Configuration Examples
Single Selection Mode
const singleSelectionConfig: BoxConfig = {
general: {
selectionMode: 'single',
maxItemSize: '80px',
boxBorderRadius: '8px',
boxPadding: '12px',
rowsGap: '8px',
colsGap: '8px',
rowMinHeight: '60px'
},
single: {
clearSpotOnClickWhenSelected: true
}
};
const singleSelectionBox = this.boxFactory.createBox(
'Single Selection Box',
3,
3,
'Box with single selection mode',
singleSelectionConfig
);Multiple Selection with Cross-Grid Highlighting
const multipleSelectionConfig: BoxConfig = {
general: {
selectionMode: 'multiple',
maxItemSize: '60px',
boxBorderRadius: '4px',
boxPadding: '8px',
rowsGap: '4px',
colsGap: '4px',
rowMinHeight: '50px'
},
multiple: {
enableCrossBoxHighlighting: true,
continueHighlightingAfterCompletingTheSelection: false
}
};
const multipleSelectionBox = this.boxFactory.createBox(
'Multiple Selection Box',
4,
4,
'Box with cross-grid highlighting',
multipleSelectionConfig
);🔄 Nested Grids
Creating Nested Structures
// Create child boxes
const childBox1 = this.boxFactory.createBox(
'Child Box 1',
2,
2,
'A nested 2x2 grid',
{ general: { selectionMode: 'multiple' } }
);
const childBox2 = this.boxFactory.createBox(
'Child Box 2',
1,
3,
'A nested 1x3 grid',
{ general: { selectionMode: 'single' } }
);
// Create parent box with nested children
const parentBox = this.boxFactory.createBox(
'Parent Box',
2,
3,
'Parent container with nested boxes',
{ general: { selectionMode: 'multiple' } },
[
[0, 0, childBox1], // Place childBox1 at row 0, column 0
[1, 1, childBox2] // Place childBox2 at row 1, column 1
]
);Complex Nested Layout
// Create a complex dashboard layout
const headerBox = this.boxFactory.createBox(
'Header',
1,
4,
'Dashboard header',
{ general: { selectionMode: 'single' } }
);
const sidebarBox = this.boxFactory.createBox(
'Sidebar',
3,
1,
'Navigation sidebar',
{ general: { selectionMode: 'single' } }
);
const contentBox = this.boxFactory.createBox(
'Content',
2,
2,
'Main content area',
{ general: { selectionMode: 'multiple' } }
);
const footerBox = this.boxFactory.createBox(
'Footer',
1,
4,
'Dashboard footer',
{ general: { selectionMode: 'single' } }
);
const dashboardBox = this.boxFactory.createBox(
'Dashboard',
3,
3,
'Complete dashboard layout',
{ general: { selectionMode: 'multiple' } },
[
[0, 0, headerBox], // Header spans top row
[1, 0, sidebarBox], // Sidebar on left
[1, 1, contentBox], // Content in center
[2, 0, footerBox] // Footer spans bottom row
]
);🎨 Advanced Styling
Row and Column Styling
const styledBox = this.boxFactory.createBox(
'Styled Box',
3,
3,
'Box with custom row and column styling',
this.defaultConfig,
[],
// Row styles
{
0: {
classes: ['header-row'],
styles: { 'background-color': '#f8f9fa', 'font-weight': 'bold' }
},
2: {
classes: ['footer-row'],
styles: { 'background-color': '#e9ecef', 'border-top': '2px solid #dee2e6' }
}
},
// Column styles
{
0: {
0: { classes: ['first-col'], styles: { 'border-left': '3px solid #007bff' } },
1: { classes: ['first-col'], styles: { 'border-left': '3px solid #007bff' } },
2: { classes: ['first-col'], styles: { 'border-left': '3px solid #007bff' } }
}
}
);Item and Placeholder Styling
const itemStyledBox = this.boxFactory.createBox(
'Item Styled Box',
2,
2,
'Box with custom item and placeholder styling',
this.defaultConfig,
[],
{}, // Row styles
{}, // Column styles
// Item styles
{
0: {
0: {
classes: ['featured-item'],
styles: {
'background': 'linear-gradient(45deg, #ff6b6b, #feca57)',
'color': 'white',
'font-weight': 'bold'
}
}
}
},
// Placeholder styles
{
1: {
1: {
classes: ['special-placeholder'],
styles: {
'background-color': '#e3f2fd',
'border': '2px dashed #2196f3'
}
}
}
}
);🎪 Demo Boxes Service
The DemoBoxesService provides ready-to-use preset layouts for quick prototyping:
Available Demo Methods
// Complex layouts
getDemoBigMiddleSpotBox() // Complex layout with colorful styling
getBigMiddleSpotBox() // Clean layout with minimal styling
getBigMiddleSpotBoxNoBorders() // Borderless layout
// Simple grids
get5x5Box() // Simple 5x5 grid
getZBox() // 5x5 grid with Z-pattern styling
get2x6Box() // 2x6 grid for medium content
get4x12Box() // 4x12 grid for large content areas
// Specialized layouts
getGifBox() // Gift box layout with nested boxesUsing Demo Boxes
import { DemoBoxesService } from 'box-me';
@Component({
// ... component configuration
})
export class DemoComponent {
constructor(private demoBoxes: DemoBoxesService) {}
// Get different demo layouts
getComplexLayout() {
return this.demoBoxes.getDemoBigMiddleSpotBox();
}
getCleanLayout() {
return this.demoBoxes.getBigMiddleSpotBox();
}
getSimpleGrid() {
return this.demoBoxes.get5x5Box();
}
getGiftBox() {
return this.demoBoxes.getGifBox();
}
}🎨 Custom Styling
CSS Custom Properties
Box-Me uses CSS custom properties for easy theming. Override these in your global styles:
:root {
/* Box container */
--max-item-size: 120px;
--box-border-radius: 16px;
--box-padding: 8px;
/* Grid spacing */
--row-gap: 8px;
--col-gap: 8px;
--row-min-height: 80px;
/* Colors */
--box-background: #ffffff;
--box-border-color: #dee2e6;
--item-background: #f8f9fa;
--item-border-color: #adb5bd;
--selected-item-background: #e3f2fd;
--selected-item-border-color: #2196f3;
/* Transitions */
--transition-duration: 0.2s;
--transition-timing: ease-in-out;
}Custom Component Styling
@Component({
selector: 'app-custom-box',
template: `
<lib-box [box]="myBox" class="custom-box-theme">
<ng-template #itemTemplate let-item let-rowIndex="rowIndex" let-colIndex="colIndex">
<div class="custom-item" [class.selected]="item.isVisible">
<div class="item-content">
<img [src]="item.imageUrl" *ngIf="item.isVisible" [alt]="item.id">
<div class="placeholder" *ngIf="!item.isVisible">
<span class="coordinates">{{ rowIndex }},{{ colIndex }}</span>
</div>
</div>
</div>
</ng-template>
</lib-box>
`,
styles: [`
.custom-box-theme {
--max-item-size: 100px;
--box-border-radius: 12px;
--box-padding: 12px;
--row-gap: 6px;
--col-gap: 6px;
}
.custom-item {
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
}
.custom-item:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.custom-item.selected {
border: 2px solid #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.item-content {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
.placeholder {
color: #6c757d;
font-size: 14px;
font-weight: 500;
}
.coordinates {
background: rgba(0, 0, 0, 0.1);
padding: 4px 8px;
border-radius: 4px;
}
`]
})
export class CustomBoxComponent {
// ... component logic
}🔧 Advanced Usage Patterns
Dynamic Box Creation
@Component({
selector: 'app-dynamic-box',
template: `
<div class="controls">
<button (click)="addRow()">Add Row</button>
<button (click)="addColumn()">Add Column</button>
<button (click)="removeRow()">Remove Row</button>
<button (click)="removeColumn()">Remove Column</button>
</div>
<lib-box [box]="dynamicBox" (itemsChange)="onBoxChange($event)">
<ng-template #itemTemplate let-item let-rowIndex="rowIndex" let-colIndex="colIndex">
<div class="dynamic-item">
{{ rowIndex }},{{ colIndex }}
</div>
</ng-template>
</lib-box>
`
})
export class DynamicBoxComponent {
dynamicBox: Box;
currentRows = 3;
currentCols = 3;
constructor(private boxFactory: BoxFactoryService) {
this.updateBox();
}
updateBox() {
this.dynamicBox = this.boxFactory.createBox(
'Dynamic Box',
this.currentRows,
this.currentCols,
`Dynamic ${this.currentRows}x${this.currentCols} grid`
);
}
addRow() {
this.currentRows++;
this.updateBox();
}
addColumn() {
this.currentCols++;
this.updateBox();
}
removeRow() {
if (this.currentRows > 1) {
this.currentRows--;
this.updateBox();
}
}
removeColumn() {
if (this.currentCols > 1) {
this.currentCols--;
this.updateBox();
}
}
onBoxChange(box: Box) {
console.log('Dynamic box updated:', box);
}
}Event Handling
@Component({
selector: 'app-event-handling',
template: `
<lib-box
[box]="myBox"
[selectedItem]="selectedItem"
(itemsChange)="onItemsChange($event)"
(itemClick)="onItemClick($event)"
(itemHover)="onItemHover($event)"
(itemDragStart)="onItemDragStart($event)"
(itemDragEnd)="onItemDragEnd($event)">
<ng-template #itemTemplate let-item let-rowIndex="rowIndex" let-colIndex="colIndex">
<div class="interactive-item"
[class.selected]="item.isVisible"
[class.hovered]="hoveredItem === item.id">
<img [src]="item.imageUrl" *ngIf="item.isVisible" [alt]="item.id">
<div class="placeholder" *ngIf="!item.isVisible">
{{ rowIndex }},{{ colIndex }}
</div>
</div>
</ng-template>
</lib-box>
`
})
export class EventHandlingComponent {
myBox: Box;
selectedItem: Item | null = null;
hoveredItem: string | null = null;
constructor(private boxFactory: BoxFactoryService) {
this.myBox = this.boxFactory.createBox('Event Box', 3, 3, 'Box with event handling');
}
onItemsChange(box: Box) {
console.log('Box items changed:', box);
// Handle item changes, save to backend, etc.
}
onItemClick(event: any) {
console.log('Item clicked:', event);
this.selectedItem = event.item;
}
onItemHover(event: any) {
console.log('Item hovered:', event);
this.hoveredItem = event.item?.id || null;
}
onItemDragStart(event: any) {
console.log('Item drag started:', event);
// Handle drag start
}
onItemDragEnd(event: any) {
console.log('Item drag ended:', event);
// Handle drag end
}
}🧪 Testing
Unit Testing
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BoxComponent } from 'box-me';
import { BoxFactoryService } from 'box-me';
describe('BoxComponent', () => {
let component: BoxComponent;
let fixture: ComponentFixture<BoxComponent>;
let boxFactory: BoxFactoryService;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [BoxComponent],
providers: [BoxFactoryService]
}).compileComponents();
fixture = TestBed.createComponent(BoxComponent);
component = fixture.componentInstance;
boxFactory = TestBed.inject(BoxFactoryService);
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render correct number of cells', () => {
const testBox = boxFactory.createBox('Test Box', 2, 3, 'Test grid');
component.box = testBox;
fixture.detectChanges();
const cells = fixture.nativeElement.querySelectorAll('.box-cell');
expect(cells.length).toBe(6); // 2 rows * 3 columns
});
it('should emit itemsChange event when items are modified', () => {
const testBox = boxFactory.createBox('Test Box', 2, 2, 'Test grid');
component.box = testBox;
spyOn(component.itemsChange, 'emit');
// Simulate item change
component.onItemClick({ rowIndex: 0, colIndex: 0 });
expect(component.itemsChange.emit).toHaveBeenCalled();
});
});🚀 Performance Optimization
Lazy Loading
@Component({
selector: 'app-lazy-box',
template: `
<div *ngIf="isLoaded; else loading">
<lib-box [box]="lazyBox" (itemsChange)="onBoxChange($event)">
<ng-template #itemTemplate let-item>
<div class="lazy-item">{{ item.name || 'Empty' }}</div>
</ng-template>
</lib-box>
</div>
<ng-template #loading>
<div class="loading">Loading box layout...</div>
</ng-template>
`
})
export class LazyBoxComponent implements OnInit {
lazyBox: Box | null = null;
isLoaded = false;
constructor(private boxFactory: BoxFactoryService) {}
ngOnInit() {
// Simulate async loading
setTimeout(() => {
this.lazyBox = this.boxFactory.createBox(
'Lazy Loaded Box',
4,
4,
'Box loaded asynchronously'
);
this.isLoaded = true;
}, 1000);
}
onBoxChange(box: Box) {
console.log('Lazy box changed:', box);
}
}OnPush Change Detection
@Component({
selector: 'app-optimized-box',
template: `
<lib-box [box]="optimizedBox" (itemsChange)="onBoxChange($event)">
<ng-template #itemTemplate let-item>
<div class="optimized-item">{{ item.name || 'Empty' }}</div>
</ng-template>
</lib-box>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedBoxComponent {
optimizedBox: Box;
constructor(private boxFactory: BoxFactoryService) {
this.optimizedBox = this.boxFactory.createBox(
'Optimized Box',
3,
3,
'Box with OnPush change detection'
);
}
onBoxChange(box: Box) {
// Handle changes efficiently
console.log('Optimized box changed:', box);
}
}🔧 Troubleshooting
Common Issues
- Box not rendering: Ensure you've imported
BoxComponentin your module/standalone component - Styling not applied: Check that CSS custom properties are defined in your global styles
- Events not firing: Verify that event handlers are properly bound in the template
- Nested boxes not working: Ensure child boxes are properly placed in the nested array
Debug Mode
Enable debug mode to see positioning information:
const debugConfig: BoxConfig = {
debug: true,
general: {
selectionMode: 'multiple'
}
};
const debugBox = this.boxFactory.createBox(
'Debug Box',
2,
2,
'Box with debug information',
debugConfig
);🤝 Contributing
We welcome contributions! Please see our Contributing Guide for details.
Development Setup
# Clone the repository
git clone https://github.com/your-org/box-me.git
cd box-me
# Install dependencies
npm install
# Build the library
ng build box-me
# Run the demo app
ng serve
# Run tests
ng test box-me
# Run linting
ng lint box-meCode Style
- Follow Angular style guide
- Use TypeScript strict mode
- Write comprehensive tests
- Add JSDoc comments for public APIs
- Keep components and services focused and single-purpose
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
🙏 Acknowledgments
- Built with Angular
- Inspired by modern grid layout systems
- Community feedback and contributions
📞 Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 📖 Documentation: Full Documentation
- 💬 Discussions: GitHub Discussions
Made with ❤️ by the Box-Me Team
