156 lines
4.4 KiB
Markdown
156 lines
4.4 KiB
Markdown
|
|
# Component Organization Guide
|
||
|
|
|
||
|
|
Guide for splitting large components and modals into manageable files.
|
||
|
|
|
||
|
|
## When to Split Components
|
||
|
|
|
||
|
|
Split a component when:
|
||
|
|
- File exceeds 200 lines
|
||
|
|
- Component has multiple distinct sections (tabs, steps, panels)
|
||
|
|
- Logic becomes hard to follow
|
||
|
|
- Multiple developers work on different parts
|
||
|
|
|
||
|
|
## Modal Organization Pattern
|
||
|
|
|
||
|
|
### Structure for Large Modals
|
||
|
|
|
||
|
|
```
|
||
|
|
src/routes/(app)/feature/
|
||
|
|
├── +page.svelte # Main page
|
||
|
|
├── FeatureModal.svelte # Main modal container
|
||
|
|
└── feature-modal/ # Modal sub-components (kebab-case folder)
|
||
|
|
├── modals/ # Nested modals
|
||
|
|
│ └── PickerModal.svelte
|
||
|
|
└── tabs/ # Tab content components
|
||
|
|
├── BasicInfoTab.svelte
|
||
|
|
├── SettingsTab.svelte
|
||
|
|
└── AdvancedTab.svelte
|
||
|
|
```
|
||
|
|
|
||
|
|
### Example: Test Form Modal
|
||
|
|
|
||
|
|
**Location**: `src/routes/(app)/master-data/tests/test-modal/`
|
||
|
|
|
||
|
|
```svelte
|
||
|
|
<!-- TestFormModal.svelte -->
|
||
|
|
<script>
|
||
|
|
import Modal from '$lib/components/Modal.svelte';
|
||
|
|
import BasicInfoTab from './test-modal/tabs/BasicInfoTab.svelte';
|
||
|
|
import TechDetailsTab from './test-modal/tabs/TechDetailsTab.svelte';
|
||
|
|
import CalcDetailsTab from './test-modal/tabs/CalcDetailsTab.svelte';
|
||
|
|
|
||
|
|
let { open = $bindable(false), test = null } = $props();
|
||
|
|
let activeTab = $state('basic');
|
||
|
|
let formData = $state({});
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<Modal bind:open title={test ? 'Edit Test' : 'New Test'} size="xl">
|
||
|
|
{#snippet children()}
|
||
|
|
<div class="tabs tabs-boxed mb-4">
|
||
|
|
<button class="tab" class:tab-active={activeTab === 'basic'} onclick={() => activeTab = 'basic'}>Basic</button>
|
||
|
|
<button class="tab" class:tab-active={activeTab === 'technical'} onclick={() => activeTab = 'technical'}>Technical</button>
|
||
|
|
<button class="tab" class:tab-active={activeTab === 'calculation'} onclick={() => activeTab = 'calculation'}>Calculation</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{#if activeTab === 'basic'}
|
||
|
|
<BasicInfoTab bind:formData />
|
||
|
|
{:else if activeTab === 'technical'}
|
||
|
|
<TechDetailsTab bind:formData />
|
||
|
|
{:else if activeTab === 'calculation'}
|
||
|
|
<CalcDetailsTab bind:formData />
|
||
|
|
{/if}
|
||
|
|
{/snippet}
|
||
|
|
</Modal>
|
||
|
|
```
|
||
|
|
|
||
|
|
```svelte
|
||
|
|
<!-- test-modal/tabs/BasicInfoTab.svelte -->
|
||
|
|
<script>
|
||
|
|
let { formData = $bindable({}) } = $props();
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<div class="space-y-4">
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label">Test Name</label>
|
||
|
|
<input class="input input-bordered" bind:value={formData.name} />
|
||
|
|
</div>
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label">Description</label>
|
||
|
|
<textarea class="textarea textarea-bordered" bind:value={formData.description}></textarea>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
## Data Flow
|
||
|
|
|
||
|
|
### Parent to Child
|
||
|
|
- Pass data via props (`bind:formData`)
|
||
|
|
- Use `$bindable()` for two-way binding
|
||
|
|
- Keep state in parent when shared across tabs
|
||
|
|
|
||
|
|
### Child to Parent
|
||
|
|
- Use callbacks for actions (`onSave`, `onClose`)
|
||
|
|
- Modify bound data directly (with `$bindable`)
|
||
|
|
- Emit events for complex interactions
|
||
|
|
|
||
|
|
## Props Interface Pattern
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// Define props with JSDoc
|
||
|
|
/** @type {{ formData: Object, onValidate: Function, readonly: boolean }} */
|
||
|
|
let {
|
||
|
|
formData = $bindable({}),
|
||
|
|
onValidate = () => true,
|
||
|
|
readonly = false
|
||
|
|
} = $props();
|
||
|
|
```
|
||
|
|
|
||
|
|
## Naming Conventions
|
||
|
|
|
||
|
|
- **Main modal**: `{Feature}Modal.svelte` (e.g., `TestFormModal.svelte`)
|
||
|
|
- **Tab components**: `{TabName}Tab.svelte` (e.g., `BasicInfoTab.svelte`)
|
||
|
|
- **Nested modals**: `{Action}Modal.svelte` (e.g., `ConfirmDeleteModal.svelte`)
|
||
|
|
- **Folder names**: kebab-case matching the modal name (e.g., `test-modal/`)
|
||
|
|
|
||
|
|
## Shared State Management
|
||
|
|
|
||
|
|
For complex modals with shared state across tabs:
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// In main modal
|
||
|
|
let sharedState = $state({
|
||
|
|
dirty: false,
|
||
|
|
errors: {},
|
||
|
|
selectedItems: []
|
||
|
|
});
|
||
|
|
|
||
|
|
// Pass to tabs
|
||
|
|
<TabName formData={formData} sharedState={sharedState} />
|
||
|
|
```
|
||
|
|
|
||
|
|
## Import Order in Sub-components
|
||
|
|
|
||
|
|
Same as main components:
|
||
|
|
1. Svelte imports
|
||
|
|
2. `$lib/*` imports
|
||
|
|
3. External libraries
|
||
|
|
4. Relative imports (other tabs/modals)
|
||
|
|
|
||
|
|
## Testing Split Components
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Test individual tab component
|
||
|
|
vitest run src/routes/feature/modal-tabs/BasicInfoTab.test.js
|
||
|
|
|
||
|
|
# Test main modal integration
|
||
|
|
vitest run src/routes/feature/FeatureModal.test.js
|
||
|
|
```
|
||
|
|
|
||
|
|
## Benefits
|
||
|
|
|
||
|
|
- **Maintainability**: Each file has single responsibility
|
||
|
|
- **Collaboration**: Multiple developers can work on different tabs
|
||
|
|
- **Testing**: Test individual sections in isolation
|
||
|
|
- **Performance**: Only render visible tab content
|
||
|
|
- **Reusability**: Tabs can be used in different modals
|