# Views

Views extend `MagicView` or `MagicStatefulView` to give each screen a typed controller reference, async-state rendering helpers, and a clean MVC separation between UI and business logic.

- [Introduction](#introduction)
- [Creating Views](#creating-views)
    - [Stateless Views](#stateless-views)
    - [Stateful Views](#stateful-views)
    - [Rebuilds While a Route Is Covered](#covered-routes)
- [Form Handling](#form-handling)
- [Rendering Async State](#rendering-async-state)
- [Refetching Data on Mount](#refetching-on-mount)
- [Guarding a Submit Against a Double Tap](#submits-once)
- [Responsive Views](#responsive-views)
- [Generating Views](#generating-views)

## Introduction

Magic Views provide a structured way to separate your UI from your business logic, following the MVC pattern familiar to Laravel developers. Instead of standard Flutter widgets, Magic views extend `MagicView` or `MagicStatefulView`.

### Why Use MagicView?

- **Auto-Injection**: The controller is automatically available via `controller` property
- **Type Safety**: The controller is fully typed (`MagicView<UserController>`)
- **Consistency**: Matches Laravel's MVC structure
- **Wind UI Integration**: Build UIs with utility-first classes

## Creating Views

To generate a new view, use the Magic CLI:

```bash
dart run magic:artisan make:view Dashboard
```

### Stateless Views

Most of your pages will be stateless, relying on the controller for state management:

```dart
import 'package:flutter/material.dart';
import 'package:magic/magic.dart';

class DashboardView extends MagicView {
  const DashboardView({super.key});

  @override
  Widget build(BuildContext context) {
    return WDiv(
      className: 'p-6 flex flex-col gap-4',
      children: [
        WText('Dashboard', className: 'text-2xl font-bold text-white'),
        WText('Welcome back!', className: 'text-gray-400'),
      ],
    );
  }
}
```

### Stateful Views

Use `MagicStatefulView` when you need local widget state or form handling:

```dart
class LoginView extends MagicStatefulView<AuthController> {
  const LoginView({super.key});

  @override
  State<LoginView> createState() => _LoginViewState();
}

class _LoginViewState extends MagicStatefulViewState<AuthController, LoginView> {
  late final form = MagicFormData({
    'email': '',
    'password': '',
    'remember_me': false,
  }, controller: controller);

  @override
  void onClose() => form.dispose();

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: controller,
      builder: (context, _) => _buildForm(),
    );
  }
  
  Widget _buildForm() {
    return MagicForm(
      formData: form,
      child: WDiv(
        className: 'max-w-md p-6 bg-slate-900 rounded-xl',
        children: [
          WFormInput(
            controller: form['email'],
            label: trans('attributes.email'),
            placeholder: trans('fields.email_placeholder'),
            type: InputType.email,
            validator: rules([Required(), Email()], field: 'email'),
          ),
          WFormInput(
            controller: form['password'],
            label: trans('attributes.password'),
            type: InputType.password,
            validator: rules([Required(), Min(8)], field: 'password'),
          ),
          WButton(
            isLoading: controller.isLoading,
            onTap: _submit,
            className: 'w-full bg-primary p-4 rounded-lg',
            child: WText(trans('auth.login'), className: 'text-white text-center'),
          ),
        ],
      ),
    );
  }
  
  void _submit() {
    final data = form.validated();
    if (data.isNotEmpty) {
      controller.login(data);
    }
  }
}
```

### Rebuilds While a Route Is Covered

A `MagicStatefulViewState` rebuilds on every controller notification, except while nobody can see it. Flutter disables the tickers of content it does not paint: when an opaque route is pushed over the view (a detail page over a list), the `Overlay` disables them for everything underneath, and go_router does the same for an inactive `StatefulShellRoute` branch. The view reads that `TickerMode`, and while it is disabled a notification only marks the view as stale. When the route is uncovered, the view rebuilds once with the controller's current state, however many notifications arrived in between, and not at all when none did.

A detail page that notifies its controller six times during a "check now" therefore no longer rebuilds the fifty-row list underneath six times; it rebuilds it once, as the detail page pops.

A dialog, a bottom sheet or a popover is not opaque: the page under it stays painted, keeps its tickers and rebuilds on every notification as before. So does a view outside any `Navigator`.

> [!WARNING]
> The signal is the ticker mode, not visibility. If you wrap a painted view in `TickerMode(enabled: false)` to freeze its animations, its controller rebuilds freeze with them until the tickers are re-enabled.

> [!NOTE]
> Only the view's own rebuild is deferred. A `MagicSelector`, `MagicBuilder` or `ListenableBuilder` inside the view listens for itself and still rebuilds its own subtree while covered.

## Form Handling

Views use `MagicFormData` for centralized form management:

```dart
late final form = MagicFormData({
  'name': '',
  'email': '',
  'age': 0,
  'active': true,
}, controller: controller);

// Access values
form.get('name');              // String value
form.value<bool>('active');    // Typed value
form['email'];                 // TextEditingController

// Set values
form.setValue('name', 'John');

// Validation
final data = form.validated(); // Returns {} if invalid
form.validate();               // Returns bool

// Cleanup
@override
void onClose() => form.dispose();
```

### Validation Rules

Use the `rules()` helper within views for client-side validation:

```dart
WFormInput(
  controller: form['email'],
  validator: rules([Required(), Email()], field: 'email'),
);

WFormInput(
  controller: form['password_confirmation'],
  validator: rules([
    Required(),
    Same('password', valueGetter: () => form['password'].text),
  ], field: 'password_confirmation'),
);
```

## Rendering Async State

Use `controller.renderState()` to elegantly handle loading, error, and success states:

```dart
@override
Widget build(BuildContext context) {
  return controller.renderState(
    (users) => UserList(users: users),
    onLoading: Center(child: CircularProgressIndicator()),
    onError: (msg) => ErrorWidget(message: msg),
    onEmpty: EmptyState(message: trans('users.empty')),
  );
}
```

Each callback is optional. Magic provides sensible defaults if omitted.

## Refetching Data on Mount

Controllers are Type-keyed singletons and fire `onInit` ONCE per controller instance, not once per view mount, so a controller that loads its data in `onInit` fetches on the first view that resolves it and never again for the lifetime of the app. Navigating away and back re-renders the same cached rows, which reads as stale or fabricated data rather than as a stale screen.

Mix `RefetchesOnMount<Controller, View>` onto a `MagicStatefulViewState` and point `refetch` at a load method your controller defines (`ensureFresh` below). That method must JOIN a load already in flight rather than start a second one: the mount that creates the controller has already started the same load from `onInit`, so a refetch that always fires a new request sends every request twice.

```dart
class _ItemsListViewState
    extends MagicStatefulViewState<ItemsController, ItemsListView>
    with RefetchesOnMount<ItemsController, ItemsListView> {
  @override
  Future<void> refetch() => controller.ensureFresh();
}
```

The refetch is fire-and-forget: `build()` renders the cached data immediately and the view rebuilds once the fresh data lands, so a mount never blocks on the network. Have the load keep its last-known-good data on failure, so a failed refetch leaves the screen as it was. Keep a separate method for a refresh after a mutation: it must not join an older in-flight request, or it returns a snapshot without the row the user just created.

## Guarding a Submit Against a Double Tap

An `async` submit handler wired straight to a button (`onTap: _onSubmit`) leaves nothing disabling the button for the duration of the await, so a double tap fires the write twice. On a create path that is not idempotent, two taps create two records.

Mix `SubmitsOnce<W>` onto the form's `State`, route the handler through `submitOnce`, and feed `isSubmitting` to the button's `isLoading`:

```dart
class _RegisterFormState extends State<RegisterForm> with SubmitsOnce<RegisterForm> {
  Future<void> _onSubmit() async {
    await Http.post('/register', data: form.data);
  }

  @override
  Widget build(BuildContext context) {
    return WButton(
      isLoading: isSubmitting,
      onTap: () => submitOnce(_onSubmit),
      child: WText(trans('auth.register')),
    );
  }
}
```

Feeding `isSubmitting` to the button's `isLoading` is what actually blocks the second tap: a well-behaved button computes `isInteractive = !isLoading && !disabled` and passes `null` for `onTap` when that is false, so the spinner and the guard are the same switch. A throwing submit re-arms the button rather than leaving it spinning forever, since the reset runs in a `finally`.

## Responsive Views

For layouts that adapt to screen size, use `LayoutBuilder` with Wind's responsive helpers:

```dart
class DashboardView extends MagicView {
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final isDesktop = wScreenIs(context, 'lg');
        
        if (isDesktop) {
          return _buildDesktopLayout();
        }
        return _buildMobileLayout();
      },
    );
  }
}
```

Or use Wind's responsive prefixes:

```dart
WDiv(
  className: '''
    flex flex-col gap-4
    md:flex-row md:gap-6
    lg:gap-8
  ''',
  children: [...],
)
```

## Generating Views

The Magic CLI can generate different types of views:

```bash
# Basic stateless view
dart run magic:artisan make:view Dashboard

# Stateful view with form support
dart run magic:artisan make:view Auth/Login --stateful

# Nested in subfolder
dart run magic:artisan make:view Admin/Users/Index
```

### Command Options

| Option | Description |
|--------|-------------|
| `--stateful` | Create stateful view with `MagicFormData` support |

**Output:** Creates `lib/resources/views/<name>_view.dart`
