# 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)
- [Gotchas](#gotchas)
- [Related Documentation](#related-documentation)

## 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_kit` Integration**: Build UIs with utility-first classes

## Creating Views

To generate a new view, use dowel:

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

### Stateless Views

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

```dart
import 'package:material_ui/material_ui.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.set('name', 'John');       // text field
form.setValue('active', false); // any other type

// 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 handle loading, error, empty and success states:

```dart
@override
Widget build(BuildContext context) {
  return controller.renderState(
    (users) => UserList(users: users),
    onLoading: Center(child: CircularProgressIndicator()),
    onError: (msg) => WText(msg, className: 'text-red-500'),
    onEmpty: WText(trans('users.empty')),
  );
}
```

`onLoading`, `onError` and `onEmpty` are optional; Magic renders a default widget for any you omit.

## 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 the `wind_kit` 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 the `wind_kit` responsive prefixes:

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

## Generating Views

Dowel can generate different types of views:

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

# Stateful view wired to LoginController
dart run magic:dowel make:view Auth/Login --stateful

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

# List view that refetches on every mount, with a form object
dart run magic:dowel make:view Monitors/List --controller=Monitor --list --form=MonitorFormObject
```

### Command Options

| Option | Description |
|:---|:---|
| `--stateful` | Create a stateful view bound to a controller (derived from the view's name) |
| `--controller` | Bind the view to this controller (suffix optional); implies `--stateful` |
| `--list` | Add `RefetchesOnMount`, which calls the controller's `ensureFresh()` (a controller made with `make:controller --resource` has one); implies `--stateful` |
| `--form` | Add a form object field owned by the state and disposed in `onClose`; implies `--stateful` |
| `--test` | Also scaffold the matching test |
| `--force` | Overwrite an existing file |

**Output:** Creates `lib/resources/views/<name>_view.dart`; the `View` suffix is appended when omitted.

## Gotchas

| Mistake | Fix |
|:---|:---|
| Keeping a `TextEditingController` or `MagicFormData` in a `MagicView` | `MagicView` is stateless and has no lifecycle; use `MagicStatefulView` with a `MagicStatefulViewState` and dispose the form from `onClose` |
| A view whose controller was never registered | `controller` is `Magic.find<T>()` and throws when nothing is registered; reach the controller through its `Magic.findOrPut` accessor first |
| Loading data in `onInit` and expecting a refetch when the screen is revisited | `onInit` runs once per controller instance; mix `RefetchesOnMount` into the state |
| Wrapping a visible view in `TickerMode(enabled: false)` | Its controller rebuilds freeze with the tickers; the view only defers rebuilds while a route covers it |
| Passing `_onSubmit` straight to `onTap` | Route it through `submitOnce` and feed `isSubmitting` to `isLoading`, or a double tap writes twice |

## Related Documentation

- [Controllers](https://anilcancakir.com/magic/docs/basics/controllers.md)
- [Forms](https://anilcancakir.com/magic/docs/basics/forms.md)
- [UI Helpers](https://anilcancakir.com/magic/docs/basics/ui-helpers.md)
- [Routing](https://anilcancakir.com/magic/docs/basics/routing.md)
- [Magic CLI](https://anilcancakir.com/magic/docs/packages/magic-cli.md): `make:view` and its options.
