Component System

FyneJS Components

Build modular, reusable UI components with lifecycle hooks, reactive props, and flexible composition patterns.

Lifecycle Hooks
Reactive Props
Flexible Slots

Component Registration

Define reusable components that can be used throughout your application with a simple registration API.

Component Workflow

Register Once

Define component with template, data, and methods

Use Anywhere

Insert with <component> tag throughout your app

Independent Instances

Each component has its own reactive state

// Register a reusable counter component
XTool.registerComponent({
  name: 'counter',
  template: `<div class="counter">
    <h3>Count: \{{ count }}</h3>
    <button x-on:click="increment">
      +1
    </button>
    <button x-on:click="reset">
      Reset
    </button>
  </div>`,
  
  makeData: (props) => ({
    count: Number(props.start || 0)
  }),
  
  methods: {
    increment() {
      this.count++;
    },
    reset() {
      this.count = 0;
    }
  }
});
<!-- Use the component multiple times -->
<div x-data="{ message: 'Hello Components!' }">
  <h1 x-text="message"></h1>
  
  <!-- Counter starting at 0 -->
  <component source="counter"></component>
  
  <!-- Counter starting at 10 -->
  <component source="counter" 
             x-prop="{ start: 10 }">
  </component>
  
  <!-- Counter starting at 100 -->
  <component source="counter" 
             x-prop="{ start: 100 }">
  </component>
</div>

Dynamic Component Switching

Swap components on the fly by binding the source attribute—just like changing an image src.

<div x-data="{ selected: 'mini-counter' }">
  <select x-model="selected">
    <option value="mini-counter">Counter</option>
    <option value="mini-quote">Quote</option>
  </select>
  <component x:source="selected"></component>
</div>
  • Supports lazy loading via XTool.loadComponents({ mode: 'lazy' }).
  • Keeps instances isolated; switching remounts the chosen component.
  • Great for tabs, previewers, and dashboards.

Readonly Freeze Mode

Freeze a component to disable state changes and renders—perfect for previews, embeds, and audits.

Attribute & Binding

<!-- Static freeze -->
<component source="stats-card" readonly></component>

<!-- Reactive toggle -->
<component source="mini-counter" x-bind:readonly="isFrozen"></component>

Behavior

  • Blocks updates and event effects; state is preserved while frozen.
  • Remove readonly to resume interactivity seamlessly.
  • Pairs nicely with dynamic switching for safe previews.

Lifecycle Hooks

Control component behavior at key moments with 7 lifecycle hooks for complete lifecycle management.

Mount Phase

beforeMount

Setup before DOM

mounted

Component ready

Update Phase

updated

Data changed

↻ Repeatable

Unmount Phase

beforeUnmount

Cleanup prep

unmounted

Removed from DOM

Destroy Phase

beforeDestroy

Final cleanup

destroyed

Fully destroyed

Lifecycle Execution Flow

1
beforeMount
2
mounted
updated
3
beforeUnmount
4
unmounted
5
beforeDestroy
6
destroyed

Interactive Examples

Timer Component

Basic lifecycle with interval management

Data Fetcher

Advanced async operations with cleanup

Complete Reference

All 7 lifecycle hooks demonstrated

// Timer component with lifecycle management
XTool.registerComponent({
  name: 'timer',
  template: `<div>
    <p>Seconds: \{{ seconds }}</p>
    <button x-on:click="toggle">
      \{{ isRunning ? 'Pause' : 'Start' }}
    </button>
  </div>`,
  
  makeData: () => ({
    seconds: 0,
    interval: null,
    isRunning: false
  }),
  
  beforeMount() {
    console.log('Timer initializing...');
  },
  
  mounted() {
    console.log('Timer ready!');
    this.start();
  },
  
  updated() {
    console.log('Timer updated:', this.seconds);
  },
  
  beforeUnmount() {
    this.stop();
    console.log('Timer cleanup complete');
  },
  
  methods: {
    start() {
      if (!this.interval) {
        this.interval = setInterval(() => {
          this.seconds++;
        }, 1000);
        this.isRunning = true;
      }
    },
    stop() {
      if (this.interval) {
        clearInterval(this.interval);
        this.interval = null;
        this.isRunning = false;
      }
    },
    toggle() {
      this.isRunning ? this.stop() : this.start();
    }
  }
});
// Data fetcher with complete lifecycle
XTool.registerComponent({
  name: 'data-fetcher',
  template: `<div>
    <div x-show="loading">Loading...</div>
    <div x-show="error" x-text="error"></div>
    <div x-show="data" x-text="data"></div>
  </div>`,
  
  makeData: (props) => ({
    data: null,
    loading: false,
    error: null,
    controller: null
  }),
  
  beforeMount() {
    // Initialize AbortController
    this.controller = new AbortController();
  },
  
  mounted() {
    // Start data fetching
    this.fetchData();
  },
  
  updated() {
    // Log state changes
    if (this.error) console.error('Fetch error:', this.error);
    if (this.data) console.log('Data received:', this.data);
  },
  
  beforeUnmount() {
    // Cancel any pending requests
    if (this.controller) {
      this.controller.abort();
    }
  },
  
  unmounted() {
    console.log('Data fetcher cleaned up');
  },
  
  methods: {
    async fetchData() {
      this.loading = true;
      this.error = null;
      
      try {
        const response = await fetch('/api/data', {
          signal: this.controller.signal
        });
        this.data = await response.json();
      } catch (err) {
        if (err.name !== 'AbortError') {
          this.error = err.message;
        }
      } finally {
        this.loading = false;
      }
    }
  }
});
// Component with all 7 lifecycle hooks
XTool.registerComponent({
  name: 'full-lifecycle',
  template: `<div><p>Check console for lifecycle logs</p></div>`,
  
  makeData: () => ({ value: 'Hello' }),
  
  // 1. Before component is mounted to DOM
  beforeMount() {
    console.log('1. beforeMount: Component created, not in DOM yet');
  },
  
  // 2. After component is mounted to DOM
  mounted() {
    console.log('2. mounted: Component in DOM, directives active');
  },
  
  // 3. After reactive data changes (can happen multiple times)
  updated() {
    console.log('3. updated: Data changed, DOM updated');
  },
  
  // 4. Before component is removed from DOM
  beforeUnmount() {
    console.log('4. beforeUnmount: About to remove from DOM');
  },
  
  // 5. After component is removed from DOM
  unmounted() {
    console.log('5. unmounted: Removed from DOM');
  },
  
  // 6. Before component is completely destroyed
  beforeDestroy() {
    console.log('6. beforeDestroy: About to destroy component');
  },
  
  // 7. After component is completely destroyed
  destroyed() {
    console.log('7. destroyed: Component fully destroyed');
  }
});

Reactive Props

Pass reactive data between components. Props automatically update when parent data changes.

Parent to Child

Data flows down via x-prop

x-prop="{ name: userName, count: userCount }"

Auto Updates

Child rerenders when props change

this.name // Always current value

Usage Pattern

User Card Example

Props are passed as an object and automatically become available in the component.

See Complete Example
// Component receives props automatically
XTool.registerComponent({
  name: 'user-card',
  template: `<div><h3>\{{ name }}</h3><span>\{{ count }}</span></div>`
});

// Parent passes data via x-prop
<component source="user-card" x-prop="{ name: userName, count: userCount }"></component>

Prop Effects

React to specific prop changes with custom logic and side effects.

Reactive Side Effects

Message Changes

Show notifications when new messages arrive

User ID Updates

Fetch fresh user data when ID changes

Theme Changes

Update styles and animations dynamically

// Notification component with auto-hide
XTool.registerComponent({
  name: 'notification',
  template: `<div x-show="visible" 
              class="notification">
    <span x-text="message"></span>
    <button x-on:click="hide">✕</button>
  </div>`,
  
  makeData: () => ({
    visible: false,
    timeout: null
  }),
  
  propEffects: {
    message(newMessage, oldMessage) {
      if (newMessage && newMessage !== oldMessage) {
        this.visible = true;
        this.autoHide();
      }
    }
  },
  
  methods: {
    hide() {
      this.visible = false;
    },
    autoHide() {
      clearTimeout(this.timeout);
      this.timeout = setTimeout(() => {
        this.visible = false;
      }, 3000);
    }
  }
});
// User profile with data fetching
XTool.registerComponent({
  name: 'user-profile',
  template: `<div>
    <div x-show="loading">Loading...</div>
    <div x-show="!loading && user">
      <h3 x-text="user.name"></h3>
      <p x-text="user.email"></p>
    </div>
  </div>`,
  
  makeData: () => ({
    user: null,
    loading: false
  }),
  
  propEffects: {
    userId(newUserId, oldUserId) {
      if (newUserId && newUserId !== oldUserId) {
        this.fetchUser(newUserId);
      }
    }
  },
  
  methods: {
    async fetchUser(id) {
      this.loading = true;
      try {
        const response = await fetch(`/api/users/${id}`);
        this.user = await response.json();
      } finally {
        this.loading = false;
      }
    }
  }
});

Slots & Content Projection

Create flexible, reusable components by allowing custom content insertion.

Default Slot

Main content area

<slot></slot>

Named Slots

Specific content areas

slot="header"

Flexible

Any HTML content

Components, text, etc.

Card Component Pattern

Header + Body + Footer

Common pattern: define slots for header, body, and footer content areas.

See Card Implementation
// Define slots in template
template: `<div><header><slot name="header"></slot></header><main><slot></slot></main></div>`

// Use with custom content
<component source="card">
  <h2 slot="header">Title</h2>
  <p>Body content goes here</p>
</component>

Methods & Computed Properties

Add interactive behavior and smart calculated values to your components.

Methods

Event handlers & actions

Button click handlers
Form submission logic
Data manipulation

Computed

Smart cached values

Calculated totals
Filtered/sorted lists
Formatted values

Component Logic

Methods

Handle user interactions, form submissions, and data manipulation

Computed Properties

Smart cached values that update automatically when dependencies change

// Todo List Component
XTool.registerComponent({
  name: 'todo-list',
  template: `<div>
    <input x-model="newTodo" x-on:keyup.enter="addTodo">
    <button x-on:click="addTodo">Add</button>
    <ul>
      <li x-for="todo in todos" x-text="todo"></li>
    </ul>
  </div>`,
  
  makeData: () => ({
    todos: [],
    newTodo: ''
  }),
  
  methods: {
    addTodo() {
      if (this.newTodo.trim()) {
        this.todos.push(this.newTodo);
        this.newTodo = '';
      }
    },
    removeTodo(index) {
      this.todos.splice(index, 1);
    }
  }
});
// Shopping Cart Component
XTool.registerComponent({
  name: 'shopping-cart',
  template: `<div>
    <div x-for="item in items">
      \{{ item.name }}: $\{{ item.price }}
    </div>
    <p>Total: $\{{ total }}</p>
    <p>Tax: $\{{ tax }}</p>
    <p>Grand Total: $\{{ grandTotal }}</p>
  </div>`,
  
  makeData: () => ({
    items: [
      { name: 'Apple', price: 1.50 },
      { name: 'Banana', price: 0.75 }
    ],
    taxRate: 0.08
  }),
  
  computed: {
    total() {
      return this.items.reduce((sum, item) => 
        sum + item.price, 0);
    },
    tax() {
      return this.total * this.taxRate;
    },
    grandTotal() {
      return this.total + this.tax;
    }
  }
});

What's Next?