npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@itemconsulting/xp-storybook-utils

v0.3.0

Published

Preset for using Storybook with Enonic XP

Downloads

156

Readme

Utilities for Storybook Server integration with Enonic XP

Helper library for using Storybook with Enonic XP. This library helps you prepare the data from your stories before sending it to the Storybook XP-application.

The XP-Storybook-app helps you test your FreeMarker-templates, your CSS and your frontend JavaScript. It does not help you test any serverside JavaScript!

You can mount the templates for your Parts, Layouts and Pages to create stories. Or you can use inline templates which can import FreeMarker Macros to create stories for individual components/partial-templates.

npm version

Installation

  1. Install the xp-storybook-application in your local XP sandbox.
  2. Install Storybook-server in your XP-project
    npx storybook@latest init --type server
    rm -r src/stories
  3. Install this package and preset-enonic-xp in your project.
    npm i --save-dev @itemconsulting/xp-storybook-utils @itemconsulting/preset-enonic-xp
  4. Add preset-enonic-xp to addons in .storybook/main.ts
    import type { StorybookConfig } from "@storybook/server-webpack5";
    const config: StorybookConfig = {
      addons: [
        "@storybook/addon-links", 
        "@storybook/addon-essentials",
    +   "@itemconsulting/preset-enonic-xp"
      ],
    };
    export default config;
  5. Add import support for *.ftl and *.html files by adding the global types from this library to the types array under compilerOptions in tsconfig.json
    {
        "compilerOptions": {
        "types": ["@itemconsulting/xp-storybook-utils/global"]
      }
    }

Expose the webapp path in the vhost config

The rendering endpoint will be exposed at http://localhost:8080/webapp/no.item.storybook. If you have enabled local vhost routing through com.enonic.xp.web.vhost.cfg, you need to create a mapping that exposes the webapp endpoints:

enabled = true

mapping.webapp.host = localhost
mapping.webapp.source = /webapp
mapping.webapp.target = /webapp
mapping.webapp.idProvider.system = default

Configuring the preview

In .storybook/preview.{ts,js} we can create a configuration common for all stories.

You can use DEFAULT_XP_SERVER to use the default server configuration when integrating with the Enonic XP Storybook-application.

import { DEFAULT_XP_SERVER, type Preview } from "@itemconsulting/xp-storybook-utils";

export default {
  parameters: {
    server: DEFAULT_XP_SERVER,
    controls: {
      matchers: {
        date: /Date$/,
      },
    },
  },
} satisfies Preview;

The default server config will make it so that the keys in args ending with Date or Time will be deserialized as the java class java.time.ZonedDateTime and args ending with Region will be deserialized as
com.enonic.xp.region.Region.

[!TIP] We recommend the wonderful lib-time if you need to work with dates in Enonic XP-projects.

Stories

File structure

We prefer to have our stories.{ts,js}-files together with the XP-components they are previewing.

[!IMPORTANT]
Enonic XP and Storybook are not running in the same environment (even if they share the same file structure). XP-controllers are running on Nashorn JS, and Storybook is running in NodeJS.

You can not import {js,ts}-files between these environments. But they can have shared {js,ts}-dependencies that doesn't depend on 3rd-party imports.

Story for a part

import id from "./article-header.ftl"; // 1
import type { Meta, StoryObj } from "@itemconsulting/xp-storybook-utils";
import "./article-header.css"; // 2

type FreemarkerParams = {
  displayName: string;
  intro?: string;
  publishedDate?: ZonedDateTime;
  locale: string;
}

export default  {
  title: "Part/Article Header",
  parameters: {
    layout: "centered", // 3
    server: { id }, // 4
  },
} satisfies  Meta<FreemarkerParams>;

export const articleHeader: StoryObj<FreemarkerParams> = { // 5
  name: "Article Header",
  args: {
    displayName: "This is a typical title of a blog article",
    intro: "The intro can be relevant some times. It happens that some editors write a whole article here.",
    publishedDate: "2023-05-23T10:41:37.212Z", // 6
    locale: "en"
  },
};
  1. We import the template-file we want to test in the story. The addon preset-enonic-xp provides support for *.ftl/*.html-files. The value of id is your local path on disk to template-file relative to the resource-directory.
  2. We can import css-files used by the story
  3. It's possible to change Storybooks layout (legal values are: "padded" (default), "fullscreen", "centered").
  4. We pass in the id to tell the xp-storybook-app which local file it should use to render the story.
  5. When we create a story object we can pass in a type that defines the shape of data the ftl-file expects. If you are writing your controller in TypeScript, you can use this type both in the controller and the story.
  6. If preview.ts is configured like above publishedDate will give a date picker-input in Storybook, but be parsed into a java.time.ZonedDateTime serverside before being passed into the ftl-file. This is because publishedDate ends with the word Date which triggers the regex' in preview.ts.

Story for a partial template

import id from "./accordion.ftl";
import { renderOnServer, type Meta, type StoryObj } from "@itemconsulting/xp-storybook-utils";
import "./accordion.css";

type FreemarkerParams = {
  id: string;
  items: {
    title: string;
    body: string;
  }[];
};

const meta: Meta<FreemarkerParams> = {
  title: "Component/Accordion",
  parameters: renderOnServer({
    template: `
      [#import "/site/views/partials/accordion/accordion.ftl" as a]
      [@a.accordion id=id items=items /]
    `, // 1
    id, // 2
  }),
};

export default meta;

export const Accordion: StoryObj<FreemarkerParams> = {
  args: {
    id: "my-accordion-part",
    items: [
      {
        title: "First accordion",
        text: "This is my first test",
      },
      {
        title: "Second accordion",
        text: "This is my second test",
      },
    ],
  },
};
  1. When rendering a partial template (in this example a Freemarker Macro named accordion), we can't pass the parameters into the view directly – because that would be like having a function that is never called. We can instead define an inline template using the template property. This inline template can import the macro we want to test and call it with the correct parameters.
  2. It is optional to pass in the id, but the file extension can be used as a hint to the renderMode. It's important to import the id from the file, because it gives a hint to Webpack to reload the preview when the file changes.

Story for a layout or page

It is possible to create composite stories where pages or layouts display parts inside.

This can even be used to compose a story containing an entire page, as it would look deployed in Enonic XP.

import { renderOnServer, hideControls, type Meta, type StoryObj } from "@itemconsulting/xp-storybook-utils";
import id from "./default.ftl";
import layout1ColId from "../../layouts/layout-1-col/layout-1-col.ftl";
import articleHeaderId from "../../parts/article-header/article-header.ftl";
import { articleHeader } from "../../parts/article-header/article-header.stories"; // 1
import "../../../assets/styles/main.css";

const meta: Meta = {
  title: "Page/Article",
  argTypes: {
    ...hideControls({ // 2
      id: "text",
      headerMenu: "object",
      homeUrl: "text",
      searchUrl: "text",
      themeColor: "color",
    }),
  },
  parameters: renderOnServer({
    layout: "fullscreen",
    id,
    "com.example:layout-1-col": layout1ColId, // 3
    "com.example:article-header": articleHeaderId,
    "com.example:echo": "<h2>${title}</h2>", // 4
  }),
};

export default meta;

export const Article: StoryObj = {
  args: {
    displayName: "My article page",
    id: "4e34b299-85ef-4684-b941-03ac83aa385e",
    homeUrl: "#",
    headerMenu: {
      menuItems: [],
    },
    themeColor: "#ebfffb",
    searchUrl: "#",
    headerRegion: { // 5
      name: "header",
      components: [
        {
          type: "layout",
          descriptor: "com.example:layout-1-col",
          path: "/header/0",
          config: {
            containerClass: "container-l",
            mainRegion: {
              name: "main",
              components: [
                {
                  type: "part",
                  descriptor: "com.example:article-header",
                  path: "/header/0/main/0",
                  config: articleHeader.args,
                },
              ],
            },
          },
          regions: {},
        },
      ],
    },
    mainRegion: {
      name: "main",
      components: [
        {
          type: "layout",
          descriptor: "com.example:layout-1-col",
          path: "/main/0",
          config: {
            containerClass: "container-m",
            mainRegion: {
              name: "main",
              components: [
                {
                  type: "part",
                  descriptor: "com.example:echo", // 6
                  path: "/main/0/main/0",
                  config: {
                    title: "Echo this title"
                  },
                },
              ],
            },
          },
          regions: {},
        },
      ],
    },
  },
};
  1. We can import other stories to reuse their args
  2. The hideControls() utility function lets us remove noisy Storybook controls that doesn't provide any value to the tester. You must still specify the type of control it would have been, to ensure that the data is still serialized correctly when sent to the server as part of the data model.
  3. We can specify more named views that can be used to render components inside the page or layout.
  4. Named views can also be inline templates (instead of imported views)
  5. In the preview.ts-file above we specified that args that ends with "Region" should be handled as a com.enonic.xp.region.Region by the renderer on the server. The server will render the views named by descriptor with config as its data, and populate this region in the parent view.
  6. Inline templates are rendered using config as data in the same way.

Java Types

Since all the args are sent to the server as query parameters their type can be lost on the way. If you need to ensure that an arg is deserialized to the correct Java class you can use the javaTypes field to specify which class a string version of this value should be deserialized into.

This is a more fine-grained version of the same mechanism use in createPreviewServerParams().

import id from "./timeline.ftl";
import { renderOnServer, type Meta, type StoryObj } from "@itemconsulting/storybook-xp";

type FreemarkerParams = {
  startYear: number;
  endYear: number;
}

const meta: Meta<FreemarkerParams> = {
  title: "Part/Timeline",
  parameters: renderOnServer({
    id,
    javaTypes: { // 1
      startYear: "number",
      endYear: "number",
    },
  }),
};
export default meta;

export const standardMeasureList: StoryObj<FreemarkerParams> = {
  name: "Standard measure list",
  args: {
    title: "This is a timeline",
    startYear: 2018,
    endYear: 2023,
  },
};
  1. We can explicitly specify which Java-classes the value of an arg should be deserialized as using the javaTypes-property.

Keys and their corresponding Java-class

| Key | Java type | |-------------------|-------------------------------| | "zonedDateTime" | java.time.ZonedDateTime | | "localDateTime" | java.time.LocalDateTime | | "number" | java.lang.Integer | | "region" | com.enonic.xp.region.Region | | "string" | java.lang.String |

Building

To build he project run the following command

npm run build