search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs

WDynamic

WDynamic renders a Flutter widget tree from a JSON/Map configuration at runtime, with built-in action handling and form state management.

WDynamic(
  json: const {
    'type': 'WDiv',
    'props': {
      'className': 'p-6 bg-white rounded-xl shadow-sm'
    },
    'children': [
      {
        'type': 'WText',
        'props': {
          'text': 'Hello from JSON!',
          'className': 'text-xl font-bold text-gray-800'
        }
      },
    ],
  },
)

Basic Usage

The WDynamic widget converts JSON/Map structures into live Flutter widgets. This enables server-driven UI, dynamic form generation, and runtime-configurable layouts without rebuilding your app.

WDynamic(
  json: const {
    'type': 'WDiv',
    'props': {'className': 'flex gap-4 p-4'},
    'children': [
      {
        'type': 'WButton',
        'props': {
          'className': 'bg-blue-500 text-white px-4 py-2 rounded',
          'onTap': {'action': 'submit'},
        },
        'children': [
          {'type': 'WText', 'props': {'text': 'Submit'}}
        ],
      }
    ],
  },
  actions: {
    'submit': (args) => print('Button tapped'),
  },
)

Constructor

const WDynamic({
  Key? key,
  required Map json,
  Map actions = const {},
  WDynamicController? controller,
  Set? denyWidgets,
  Map? builders,
  Map? customIcons,
  int maxDepth = 50,
  Widget Function(String type, Object error)? onError,
  Widget Function(String type, Map props)? onUnknownWidget,
})

Props

Prop Type Default Description
json Map Required JSON configuration defining the widget tree. Must include type, optional props and children.
actions Map {} Action handlers keyed by name. Called when interactive widgets trigger events.
controller WDynamicController? null Optional controller for external state access (read/write form values).
denyWidgets Set? null Widget types to remove from the default whitelist.
builders Map? null Custom widget builders keyed by type name. Signature: Widget Function(Map props, List children).
customIcons Map? null Custom icon mappings for WIcon widgets. Keys are icon names used in the JSON icon prop, values are IconData instances. Extends the default Material icon map.
maxDepth int 50 Maximum recursion depth for nested widgets. Prevents infinite loops.
onError Widget Function(String, Object)? null Error handler for widget build failures. Returns a fallback widget.
onUnknownWidget Widget Function(String, Map)? null Handler for unknown or denied widget types. Returns a fallback widget.

JSON Schema

Every widget node in the JSON structure follows this format:

{
  'type': 'WidgetType',      // Required: Wind widget, Flutter widget, or custom builder
  'props': {                 // Optional: properties passed to the widget
    'className': 'flex gap-4',
    'text': 'Hello',
    'onTap': {'action': 'handleTap', 'args': {'id': 1}},
    'id': 'username',        // Auto-tracked form state key
  },
  'children': [              // Optional: nested widget array
    { /* child node */ }
  ]
}

Type Field

The type field determines which widget to render:

  • Wind Widgets: WDiv, WText, WButton, WInput, WCheckbox, WSelect, WDatePicker, WIcon, WImage, WSvg, WPopover, WAnchor, WSpacer
  • Flutter Widgets: Column, Row, Center, SizedBox, Expanded, Container, Wrap, Stack, Positioned, Padding, Align, Opacity, AspectRatio, FittedBox, ClipRRect, Spacer
  • Custom Widgets: Any key from the builders map

Props Field

Widget properties are passed directly to the constructor. Common props include:

{
  'type': 'WDiv',
  'props': {
    'className': 'flex gap-4 p-6 bg-white rounded-xl',  // Wind utilities
    'id': 'formContainer',                              // Auto-tracked state key
  }
}
{
  'type': 'WText',
  'props': {
    'text': 'Welcome!',                    // Text content
    'className': 'text-lg font-bold',      // Wind utilities
  }
}
{
  'type': 'WInput',
  'props': {
    'id': 'email',                         // Auto-tracked in controller state
    'placeholder': 'Enter email...',
    'className': 'border rounded-lg',
  }
}

Children Field

An array of nested widget nodes. Used by layout widgets like WDiv, Column, Row, etc.

{
  'type': 'WDiv',
  'props': {'className': 'flex gap-2'},
  'children': [
    {'type': 'WText', 'props': {'text': 'First'}},
    {'type': 'WText', 'props': {'text': 'Second'}},
  ]
}

Action Handling

Interactive widgets like WButton can trigger actions defined in the actions map. Actions are referenced in the JSON using an object format.

Action Format

{
  'type': 'WButton',
  'props': {
    'onTap': {
      'action': 'actionName',          // Required: key from actions map
      'args': {'key': 'value'}         // Optional: arguments passed to handler
    }
  }
}

Action Signatures

Actions can accept arguments only, or both arguments and state:

// Simple: (Map args)
WDynamic(
  actions: {
    'showAlert': (Map args) {
      print('Alert: ${args['message']}');
    },
  },
)
// With state access: (Map args, WDynamicState state)
WDynamic(
  actions: {
    'submitForm': (Map args, WDynamicState state) {
      final email = state.get('email');
      final password = state.get('password');
      print('Submitting: $email');
    },
  },
)

Example

WDynamic(
  json: const {
    'type': 'WButton',
    'props': {
      'className': 'bg-blue-500 text-white px-4 py-2 rounded',
      'onTap': {
        'action': 'increment',
        'args': {'step': 1}
      },
    },
    'children': [
      {'type': 'WText', 'props': {'text': 'Tap Me'}}
    ],
  },
  actions: {
    'increment': (Map args, WDynamicState state) {
      final step = args['step'] ?? 1;
      setState(() => count += step);
    },
  },
)

State Management

WDynamic automatically tracks form widget values using the id prop. Values are stored in an internal state object accessible via WDynamicController or action handlers.

Auto-Tracking with ID

Any widget with an id prop automatically stores its value in state:

{
  'type': 'WInput',
  'props': {
    'id': 'username',              // Auto-tracked
    'placeholder': 'Username...',
  }
}

Using the Controller

Create a WDynamicController to access state externally:

final controller = WDynamicController();

// Pre-fill values
controller.setValue('email', '[email protected]');

// Read values
final email = controller.getValue('email');
final allValues = controller.getAll();

// Listen for changes
final dispose = controller.addListener('email', (value) {
  print('Email changed: $value');
});

// Pass to WDynamic
WDynamic(
  controller: controller,
  json: myJson,
)

// Clean up
controller.dispose();

Accessing State in Actions

Action handlers receive the state object as a second parameter:

WDynamic(
  json: const {
    'type': 'WDiv',
    'children': [
      {
        'type': 'WInput',
        'props': {'id': 'name', 'placeholder': 'Your name...'}
      },
      {
        'type': 'WButton',
        'props': {
          'onTap': {'action': 'greet'},
          'className': 'bg-green-500 text-white px-4 py-2 rounded',
        },
        'children': [
          {'type': 'WText', 'props': {'text': 'Greet'}}
        ],
      }
    ],
  },
  actions: {
    'greet': (args, state) {
      final name = state.get('name') ?? 'World';
      print('Hello, $name!');
    },
  },
)

Custom Builders

Extend WDynamic with custom widget types using the builders prop.

WWidgetBuilder Signature

typedef WWidgetBuilder = Widget Function(
  Map props,
  List children,
);

Example

WDynamic(
  json: const {
    'type': 'InfoCard',
    'props': {
      'title': 'Custom Widget',
      'subtitle': 'Built with a custom builder'
    },
  },
  builders: {
    'InfoCard': (Map props, List children) {
      return WDiv(
        className: 'p-4 bg-blue-50 rounded-xl border border-blue-200',
        child: WDiv(
          className: 'flex items-center gap-3',
          children: [
            WIcon(Icons.info, className: 'text-blue-500 text-2xl'),
            WDiv(
              className: 'flex flex-col',
              children: [
                WText(props['title'] ?? '', className: 'font-bold text-blue-800'),
                WText(props['subtitle'] ?? '', className: 'text-sm text-blue-600'),
              ],
            ),
          ],
        ),
      );
    },
  },
)

Custom builders have priority over default widgets. You can override built-in widgets by providing a builder with the same type name.

Custom Icons

Map string icon names to IconData for use in JSON WIcon widget nodes. Custom icons extend (not replace) the built-in Material icon map.

WDynamic(
  json: const {
    'type': 'WDiv',
    'props': {'className': 'flex gap-4 items-center'},
    'children': [
      {
        'type': 'WIcon',
        'props': {'icon': 'heart', 'className': 'text-red-500 text-2xl'},
      },
      {
        'type': 'WIcon',
        'props': {'icon': 'star', 'className': 'text-yellow-500 text-2xl'},
      },
    ],
  },
  customIcons: {
    'heart': Icons.favorite,
    'star': Icons.star,
    'settings': Icons.settings,
  },
)

Security

WDynamic uses a whitelist approach for security. Only explicitly allowed widget types can be rendered.

Default Whitelist

Wind Widgets:

  • WDiv, WText, WButton, WImage, WIcon, WAnchor, WInput, WCheckbox, WSvg, WSelect, WPopover, WDatePicker, WSpacer

Flutter Widgets:

  • Column, Row, Center, SizedBox, Expanded, Container, Wrap, Stack, Positioned, Padding, Align, Opacity, AspectRatio, FittedBox, ClipRRect, Spacer

Denying Widgets

Remove specific widget types from the whitelist:

WDynamic(
  json: myJson,
  denyWidgets: const {'WButton', 'WInput'},  // Block buttons and inputs
)

Unknown Widget Handler

Provide a fallback for unknown or denied widget types:

WDynamic(
  json: myJson,
  onUnknownWidget: (String type, Map props) {
    return WDiv(
      className: 'p-2 bg-red-100 border border-red-300 rounded',
      child: WText('Unknown widget: $type', className: 'text-red-700 text-sm'),
    );
  },
)

Error Handling

Handle widget build failures gracefully with the onError callback.

WDynamic(
  json: myJson,
  onError: (String type, Object error) {
    return WDiv(
      className: 'p-4 bg-yellow-50 border border-yellow-300 rounded',
      child: WDiv(
        className: 'flex flex-col gap-2',
        children: [
          WText('Error rendering $type', className: 'font-bold text-yellow-800'),
          WText(error.toString(), className: 'text-sm text-yellow-700'),
        ],
      ),
    );
  },
)

Max Depth Protection

Prevent infinite recursion with the maxDepth prop (default: 50):

WDynamic(
  json: deeplyNestedJson,
  maxDepth: 100,  // Allow deeper nesting
)

When max depth is exceeded, rendering stops and returns an empty SizedBox.shrink() or calls onError if provided.