interactive-components


# Interactive Components

Build interactive UI components with Marko’s powerful reactivity system.

# Overview

MarkoPress is built on Marko.js v6, which provides a powerful reactive component system. This guide shows you how to build interactive components with:

  • State management - Reactive data that updates the UI
  • Event handling - Respond to user interactions
  • Forms & validation - Collect and validate user input
  • Animations - Smooth transitions and effects
  • Data fetching - Load data from APIs

# State Management

Marko’s reactive state system automatically updates your UI when data changes.

# Basic State

class {
  onCreate() {
    this.state = {
      count: 0,
      message: 'Hello, World!',
    };
  }

  increment() {
    this.state.count++;
  }
}

<div>
  <h1>${state.message}</h1>
  <p>Count: ${state.count}</p>
  <button onClick=>increment()>Increment</button>
</div>

# Computed State

class {
  onCreate() {
    this.state = {
      firstName: 'John',
      lastName: 'Doe',
    };
  }

  get fullName() {
    return `${this.state.firstName} ${this.state.lastName}`;
  }
}

<div>
  <p>Full name: ${fullName}</p>
</div>

# State from Props

class {
  onCreate() {
    this.state = {
      expanded: input.initiallyExpanded || false,
    };
  }

  toggle() {
    this.state.expanded = !this.state.expanded;
  }
}

<div class=${['accordion', { expanded: state.expanded }]}>
  <button onClick=>toggle()>
    ${state.expanded ? 'Collapse' : 'Expand'}
  </button>
  <if(state.expanded)>
    <p>${input.content}</p>
  </if>
</div>

# Event Handling

Marko provides easy-to-use event handlers for all DOM events.

# Click Events

class {
  handleClick() {
    alert('Button clicked!');
  }

  handleDoubleClick() {
    console.log('Double clicked!');
  }
}

<button onClick=>handleClick()>
  Click Me
</button>

<button onDblClick=>handleDoubleClick()>
  Double Click Me
</button>

# Input Events

class {
  onCreate() {
    this.state = {
      text: '',
      email: '',
    };
  }

  handleTextInput(e) {
    this.state.text = e.target.value;
  }

  handleEmailInput(e) {
    this.state.email = e.target.value;
  }

  handleSubmit() {
    console.log({
      text: this.state.text,
      email: this.state.email,
    });
  }
}

<div>
  <input type="text" value=state.text onInput=>handleTextInput(e) />
  <p>You typed: ${state.text}</p>

  <input type="email" value=state.email onInput=>handleEmailInput(e) />
  <p>Email: ${state.email}</p>

  <button onClick=>handleSubmit()>Submit</button>
</div>

# Keyboard Events

class {
  handleKeyDown(e) {
    if (e.key === 'Enter') {
      console.log('Enter pressed!');
    }
  }

  handleKeyPress(e) {
    console.log(`Key pressed: ${e.key}`);
  }

  handleKeyUp(e) {
    console.log(`Key released: ${e.key}`);
  }
}

<input
  type="text"
  onKeyDown=>handleKeyDown(e)
  onKeyPress=>handleKeyPress(e)
  onKeyUp=>handleKeyUp(e)
/>

# Mouse Events

class {
  handleMouseEnter() {
    console.log('Mouse entered');
  }

  handleMouseLeave() {
    console.log('Mouse left');
  }

  handleMouseMove(e) {
    console.log(`Mouse position: ${e.clientX}, ${e.clientY}`);
  }
}

<div
  onMouseEnter=>handleMouseEnter()
  onMouseLeave=>handleMouseLeave()
  onMouseMove=>handleMouseMove(e)
>
  Hover over me!
</div>

# Forms

Build interactive forms with validation and state management.

# Controlled Inputs

class {
  onCreate() {
    this.state = {
      name: '',
      email: '',
      message: '',
      subscribed: false,
    };
  }

  handleSubmit(e) {
    e.preventDefault();
    console.log(this.state);
  }
}

<form onSubmit=>handleSubmit(e)>
  <div class="form-group">
    <label>Name</label>
    <input
      type="text"
      value=state.name
      onInput=>state.name = e.target.value
    />
  </div>

  <div class="form-group">
    <label>Email</label>
    <input
      type="email"
      value=state.email
      onInput=>state.email = e.target.value
    />
  </div>

  <div class="form-group">
    <label>Message</label>
    <textarea
      value=state.message
      onInput=>state.message = e.target.value
    />
  </div>

  <div class="form-group">
    <label>
      <input
        type="checkbox"
        checked=state.subscribed
        onChange=>state.subscribed = e.target.checked
      />
      Subscribe to newsletter
    </label>
  </div>

  <button type="submit">Submit</button>
</form>

# Form Validation

class {
  onCreate() {
    this.state = {
      email: '',
      password: '',
      errors: {},
    };
  }

  validate() {
    const errors = {};

    if (!this.state.email) {
      errors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(this.state.email)) {
      errors.email = 'Invalid email format';
    }

    if (!this.state.password) {
      errors.password = 'Password is required';
    } else if (this.state.password.length < 8) {
      errors.password = 'Password must be at least 8 characters';
    }

    this.state.errors = errors;
    return Object.keys(errors).length === 0;
  }

  handleSubmit(e) {
    e.preventDefault();

    if (this.validate()) {
      console.log('Form is valid!', this.state);
    }
  }
}

<form onSubmit=>handleSubmit(e)>
  <div class="form-group">
    <label>Email</label>
    <input
      type="email"
      value=state.email
      onInput=>state.email = e.target.value
    />
    <if(state.errors.email)>
      <span class="error">${state.errors.email}</span>
    </if>
  </div>

  <div class="form-group">
    <label>Password</label>
    <input
      type="password"
      value=state.password
      onInput=>state.password = e.target.value
    />
    <if(state.errors.password)>
      <span class="error">${state.errors.password}</span>
    </if>
  </div>

  <button type="submit">Sign In</button>
</form>

# Radio Buttons & Selects

class {
  onCreate() {
    this.state = {
      plan: 'free',
      country: 'us',
      notifications: 'instant',
    };
  }
}

<div>
  <h3>Select a Plan</h3>
  <label>
    <input
      type="radio"
      name="plan"
      value="free"
      checked=state.plan === 'free'
      onChange=>state.plan = 'free'
    />
    Free
  </label>
  <label>
    <input
      type="radio"
      name="plan"
      value="pro"
      checked=state.plan === 'pro'
      onChange=>state.plan = 'pro'
    />
    Pro
  </label>
  <label>
    <input
      type="radio"
      name="plan"
      value="enterprise"
      checked=state.plan === 'enterprise'
      onChange=>state.plan = 'enterprise'
    />
    Enterprise
  </label>

  <h3>Country</h3>
  <select value=state.country onChange=>state.country = e.target.value>
    <option value="us">United States</option>
    <option value="uk">United Kingdom</option>
    <option value="ca">Canada</option>
    <option value="au">Australia</option>
  </select>
</div>

# Conditional Rendering

Show or hide content based on state or props.

# If/Else

class {
  onCreate() {
    this.state = {
      isLoggedIn: false,
      user: null,
    };
  }

  login() {
    this.state.isLoggedIn = true;
    this.state.user = { name: 'John' };
  }

  logout() {
    this.state.isLoggedIn = false;
    this.state.user = null;
  }
}

<div>
  <if(state.isLoggedIn)>
    <p>Welcome, ${state.user.name}!</p>
    <button onClick=>logout()>Logout<div data-marko-tag="0"></div>
    <p>Please log in</p>
    <button onClick=>login()>Login</button>
  </if>
</div>

# Ternary Operator

<div>
  <p>${state.isLoggedIn ? `Welcome, ${state.user.name}` : 'Please log in'}</p>
</div>

# Show/Hide Pattern

class {
  onCreate() {
    this.state = {
      modalOpen: false,
    };
  }

  openModal() {
    this.state.modalOpen = true;
  }

  closeModal() {
    this.state.modalOpen = false;
  }
}

<div>
  <button onClick=>openModal()>Open Modal</button>

  <if(state.modalOpen)>
    <div class="modal-overlay" onClick=>closeModal()>
      <div class="modal">
        <h2>Modal Title</h2>
        <p>Modal content goes here...</p>
        <button onClick=>closeModal()>Close<div data-marko-tag="1"></div>
  </ul>
</div>

# List with Index

<ul>
  <for|item, index| of=state.items>
    <li>${index + 1}. ${item}</li>
  </for>
</ul>

# Dynamic List

class {
  onCreate() {
    this.state = {
      todos: [
        { id: 1, text: 'Learn Marko', done: false },
        { id: 2, text: 'Build something', done: false },
      ],
      newTodoText: '',
    };
  }

  addTodo() {
    if (!this.state.newTodoText.trim()) return;

    this.state.todos = [
      ...this.state.todos,
      {
        id: Date.now(),
        text: this.state.newTodoText,
        done: false,
      },
    ];
    this.state.newTodoText = '';
  }

  toggleTodo(id) {
    this.state.todos = this.state.todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    );
  }

  deleteTodo(id) {
    this.state.todos = this.state.todos.filter(todo => todo.id !== id);
  }
}

<div>
  <input
    type="text"
    value=state.newTodoText
    placeholder="New todo..."
    onInput=>state.newTodoText = e.target.value
  />
  <button onClick=>addTodo()>Add</button>

  <ul>
    <for|todo| of=state.todos>
      <li class=${{ done: todo.done }}>
        <input
          type="checkbox"
          checked=todo.done
          onChange=>toggleTodo(todo.id)
        />
        <span>${todo.text}</span>
        <button onClick=>deleteTodo(todo.id)>Delete</button>
      </li>
    </for>
  </ul>
</div>

# Animations & Transitions

Add smooth animations with CSS and Marko’s reactivity.

# CSS Transitions

class {
  onCreate() {
    this.state = {
      visible: true,
    };
  }

  toggle() {
    this.state.visible = !this.state.visible;
  }
}

<style>`
  .box {
    width: 100px;
    height: 100px;
    background: var(--accent-color);
    transition: all 0.3s ease;
    opacity: 1;
    transform: scale(1);
  }

  .box.hidden {
    opacity: 0;
    transform: scale(0.8);
  }
`</style>

<div>
  <button onClick=>toggle()>Toggle</button>
  <div class=${['box', { hidden: !state.visible }]}>
    Animated Box
  </div>
</div>

# Animated Counter

class {
  onCreate() {
    this.state = {
      count: 0,
    };
  }

  increment() {
    // Animate the count
    const start = this.state.count;
    const end = start + 1;
    const duration = 300;
    const startTime = Date.now();

    const animate = () => {
      const elapsed = Date.now() - startTime;
      const progress = Math.min(elapsed / duration, 1);

      // Ease out cubic
      const easeProgress = 1 - Math.pow(1 - progress, 3);

      this.state.count = start + (end - start) * easeProgress;

      if (progress < 1) {
        requestAnimationFrame(animate);
      }
    };

    animate();
  }
}

<div>
  <p>Count: ${Math.round(state.count)}</p>
  <button onClick=>increment()>Increment</button>
</div>

# Data Fetching

Load data from APIs asynchronously.

# Basic Fetch

class {
  onCreate() {
    this.state = {
      data: null,
      loading: true,
      error: null,
    };

    this.fetchData();
  }

  async fetchData() {
    try {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      this.state.data = data;
    } catch (error) {
      this.state.error = error.message;
    } finally {
      this.state.loading = false;
    }
  }
}

<div>
  <if(state.loading)>
    <p>Loading...</p>
  <else-if(state.error)>
    <p>Error: ${state.error}<div data-marko-tag="2"></div>
    <pre>${JSON.stringify(state.data, null, 2)}</pre>
  </if>
</div>

# Search with Debounce

class {
  onCreate() {
    this.state = {
      query: '',
      results: [],
      loading: false,
    };
    this.timeout = null;
  }

  search() {
    clearTimeout(this.timeout);

    if (!this.state.query.trim()) {
      this.state.results = [];
      return;
    }

    this.state.loading = true;

    this.timeout = setTimeout(async () => {
      try {
        const response = await fetch(
          `https://api.example.com/search?q=${encodeURIComponent(this.state.query)}`
        );
        const results = await response.json();
        this.state.results = results;
      } catch (error) {
        console.error(error);
      } finally {
        this.state.loading = false;
      }
    }, 300);
  }

  onQueryChange(e) {
    this.state.query = e.target.value;
    this.search();
  }
}

<div>
  <input
    type="text"
    value=state.query
    placeholder="Search..."
    onInput=>onQueryChange(e)
  />

  <if(state.loading)>
    <p>Searching...<div data-marko-tag="3"></div>
    <ul>
      <for|result| of=state.results>
        <li>${result.title}</li>
      </for>
    </ul>
  </if>
</div>

# Best Practices

# 1. Keep Components Small

// ❌ Bad: Large component
class {
  // 500 lines of code
}

// ✅ Good: Small, focused components
class {
  // 50 lines of code
}

# 2. Use Props for Configuration

// ✅ Good: Configurable via props
<components.Button
  text="Click Me"
  variant="primary"
  size="large"
/>

# 3. Handle Loading States

class {
  async loadData() {
    this.state.loading = true;
    this.state.error = null;

    try {
      this.state.data = await fetchData();
    } catch (error) {
      this.state.error = error;
    } finally {
      this.state.loading = false;
    }
  }
}

# 4. Clean Up Side Effects

class {
  onCreate() {
    this.interval = setInterval(() => {
      console.log('Tick');
    }, 1000);
  }

  onDestroy() {
    clearInterval(this.interval);
  }
}

# Examples

See the MarkoPress source code for more examples:

# Next Steps