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).

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

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

  • The Widgetbook UI โ€“ 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.
@override
List<Field> get fields {
  return [
    IntSliderField(
      name: 'width',
      initialValue: initialValue.width,
      min: 1,
      max: 10,
    ),
    ColorField(
      name: 'color',
      initialValue: initialValue.color,
    ),
  ];
}

If your addon just has a single filed, you can use the SingleFieldOnly mixin to reduce boilerplate. With that, you can skip steps 2 and 3 as a default valueFromQueryGroup and valueToQueryGroup implementations will be provided for you.

class BorderAddon extends Addon<BorderSetting> with SingleFieldOnly {
  // ...
}

2. valueFromQueryGroup method

This method is responsible for parsing the query parameters back to the addon setting (i.e. BorderSetting). This can be done by using the valueOf helper method to extract the values from the query group by giving it the field's name.

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

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

3. valueToQueryGroup method

This 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.

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

4. apply method

This 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.

@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.

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

import 'addons/border_addon.dart';  

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

Mode

To use this addon for testing, you can enable the BorderMode in your stories:

class BorderMode extends Mode<BorderSetting> {
  BorderMode(double width, Color color)
    : super(
        BorderSetting(width, color),
        BorderAddon(),
    );
}
final $Default = _Story(
  scenarios: [
    _Scenario(
      name: 'Red Border',
      modes: [
        BorderMode(2, Colors.red), 
      ]
    ),
  ],
)