Custom Addon

If the built-in addons do not meet your needs, you can create your own custom Addons. This allows you to extend the functionality of Widgetbook and tailor it to your specific requirements.

Guide

In this guide, we will be creating a "border addon" that allows you to put a border around your story.

0. Base structure

Start by creating a new file in your widgetbook project (e.g. border_addon.dart).

dart
widgetbook/lib/addons/border_addon.dart
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';

class BorderSetting {
  const BorderSetting(this.width, this.color);

  final int width;
  final Color color;
}

class BorderAddon extends Addon<BorderSetting> {
  BorderAddon()
    : super(
        name: 'Border',
        initialValue: const BorderSetting(1, Color(0xFF000000)),
      );

  @override
  List<Field<dynamic>> get fields => [
    // ...
  ]

  @override
  BorderSetting valueFromQueryGroup(QueryGroup? group) {
    // TODO
  }

  @override
  QueryGroup valueToQueryGroup(BorderSetting value) {
    // TODO
  }

  @override
  Widget apply(
    BuildContext context,
    Widget child,
    BorderSetting setting,
  ) {
    // TODO
  }
}

Now let's start by implementing each of the class members step by step.

1. fields getter

The fields getter is a list of fields that describe how the addon will be represented in both:

  • The Widgetbook UI, in the "Addons" panel.
  • The URL query parameters.

In this case we just need two fields to represent the BorderSetting:

  1. IntSliderField - to pick an integer value for BorderSetting.width.
  2. ColorField - to pick a color value for BorderSetting.color.
dart
@override
List<Field> get fields {
  return [
    IntSliderField(
      name: 'width',
      initialValue: initialValue.width,
      min: 1,
      max: 10,
    ),
    ColorField(
      name: 'color',
      initialValue: initialValue.color,
    ),
  ];
}

2. valueFromQueryGroup method

The valueFromQueryGroup method is responsible for parsing the query parameters back to the addon setting (i.e. BorderSetting). Use the valueOf helper method to extract the values from the query group by giving it the field's name.

dart
@override
BorderSetting valueFromQueryGroup(QueryGroup? group) {
  if (group == null) return initialValue;

  return BorderSetting(
    valueOf('width', group)!,
    valueOf('color', group)!,
  );
}

3. valueToQueryGroup method

The valueToQueryGroup method is used to be able to convert Modes to a serializable QueryGroup, so that it can be stored in the URL. To use the same encoding technique used in the fields, you can use the paramOf helper method.

dart
@override
QueryGroup valueToQueryGroup(BorderSetting value) {
  return QueryGroup({
    'width': paramOf('width', value.width),
    'color': paramOf('color', value.color),
  });
}

4. apply method

The apply method is responsible for building the story widget with the addon applied. In this case, we will wrap the child widget with a Container that has a border with the given width and color.

dart
@override
Widget apply(
  BuildContext context,
  Widget child,
  BorderSetting setting,
) {
  return Container(
    decoration: BoxDecoration(
      border: Border.all(
        color: setting.color,
        width: setting.width.toDouble(),
      ),
    ),
    child: child,
  );
}

5. Using the Addon

Now that we have implemented the BorderAddon, we can use it in our Widgetbook project. To do this, we need to add it to the addons list in the Widgetbook widget.

dart
widgetbook/lib/widgetbook.config.dart
import 'package:flutter/widgets.dart';

import 'addons/border_addon.dart';  

final config = Config(
  // ...
  addons: [BorderAddon()], 
);

Mode

Each addon has a corresponding Mode to lock its value in scenarios. For a custom addon, create your own Mode subclass:

dart
class BorderMode extends Mode<BorderSetting> {
  BorderMode(int width, Color color)
    : super(
        BorderSetting(width, color),
        BorderAddon(),
    );

  @override
  String get formattedValue => '${value.width}px';
}
dart
final $Default = _Story(
  scenarios: [
    _Scenario(
      name: 'Red Border',
      modes: [
        BorderMode(2, Colors.red), 
      ]
    ),
  ],
)