DEV Community

Alex
Alex

Posted on

Not everything in an Angular library is a component: what's new in ng-prism 22.7

Not everything in a library is a component: what's new in ng-prism 22.7

ng-prism 22.7 is out. If you haven't come across it yet: ng-prism is a component lab for Angular libraries, a Storybook alternative without story files. You put a @Showcase decorator on a component, the TypeScript compiler picks up inputs, outputs and types at build time, and the components render in the same document as the styleguide, without an iframe. The full tour has the details.

Voice: elevenlabs.io

This release got bigger than I planned. Most of it is about the parts of a library that aren't a single component: services, pipes, components that only work together with others, and components that only exist inside another one. Besides that the styleguide works on a phone now, it has a new look, accessibility violations can be accepted with a reason, and the perf plugin measures what you actually ship.

Services and pipes get their own pages

A real library usually has more than components. A service that shows notifications, a few pipes, provider functions that configure them. Until now none of that had a place in the styleguide. If you put @Showcase on an @Injectable, ng-prism treated it as a component and tried to render it, which never made much sense.

In 22.7 services and pipes are their own kind:

@Showcase({
  title: 'Time Ago',
  category: 'Formatting',
  setup: () => [provideTimeAgo],
})
@Pipe({ name: 'timeAgo' })
export class TimeAgoPipe implements PipeTransform {
Enter fullscreen mode Exit fullscreen mode

They get their own sections in the sidebar, Services and Pipes, and their page has no canvas. There's nothing to render, so it's an API page: the description, a usage snippet you can copy, the examples from your JSDoc, the properties and methods of a service or the transform() overloads of a pipe, and cards for the types they use. {@link} and @see jump to those cards.

setup is the part I care about most. It lists the provider functions and injection tokens a consumer needs, and the usage snippet calls them in providers: [...]. So the page doesn't only tell you what the service has, it also tells you how to wire it up.

Components and directives get the same API page now. Inputs link to their types, outputs show their payload, documented methods are listed, and there's an "On this page" list next to it.

the Time Ago pipe's API page (?pipe=TimeAgoPipe&view=jsdoc) with the usage snippet, the setup and the transform overloads.

Have a look at the Notification Service and the Time Ago pipe in the demo.

Two things to know. The pages come from the JSDoc plugin, so you need jsDocPlugin() 22.7 or newer, otherwise services and pipes are only counted and you get a notice in the build. And a pipe has to be standalone with a readable name, otherwise it's left out with a warning.

Components that only work together

Some components don't make sense on their own. A form field without an input is an empty frame, and an input directive without its form field is a bare <input>. So far the best you could do was project some HTML into the form field. The input in there was plain markup, not your directive, so the form field never found its control.

In 22.7 a showcase can contain a real Angular template, and ng-prism compiles it. This is how the input directive in the demo shows up inside its form field:

@Showcase<InputDirective>({
  title: 'Input',
  category: 'Forms',
  host: { template: '<lib-form-field label="Name"><input libInput type="text" /></lib-form-field>' },
  variants: [
    { name: 'Default' },
    { name: 'Required', inputs: { required: true } },
    {
      name: 'With hint',
      host: { template: '<lib-form-field label="Name" hint="As on your ID"><input libInput /></lib-form-field>' },
    },
  ],
})
Enter fullscreen mode Exit fullscreen mode

The directive is the subject, and the form field around it is real. Content queries and DI work the way they do in your app, so the form field finds its input like it would in a form. The controls still belong to the subject.

There are three fields for this:

  • template on a variant is the content of the subject, compiled instead of pasted in as HTML.
  • host: { template } puts the subject inside other markup. If more than one element matches, prism-subject marks the one you mean.
  • imports brings in classes from outside your library. Classes from your own library are imported automatically.

A related fix: a directive without a host now renders on the element its selector names. input[libInput] becomes <input libInput>, and void elements like <input> or <img> no longer break the build.

the Input directive inside its form field (?component=InputDirective), the "Error" variant.

The limits: expressions in these templates have no context, so only literals and template reference variables work. Overlays that render into <body> aren't part of the composition. And if you have visual regression baselines for directives, look at them after the update, because some directives now render on a different element than before.

In the demo: the Input directive and the Form Field.

Parts

The opposite case are components that only ever live inside another one, like a list item or an option in a select. Their own page would show a list item floating alone on the canvas, which helps nobody.

Now such a component can say whose part it is:

@Showcase<ListItemComponent>({
  title: 'List Item',
  partOf: 'lib-list',
  description: 'One entry of a lib-list.',
})
Enter fullscreen mode Exit fullscreen mode

A part has no page of its own. The parent's head shows a Parts chip that opens a card with the part's inputs and outputs, and the parent's API tab gets a Parts section. If somebody opens the part's old URL, they land on the parent.

partOf takes the parent's selector as a string, not the class. That way the part doesn't have to import its parent, which would end in an import cycle pretty quickly. A part has exactly one parent, there are no chains.

The List with its item in the demo.

The styleguide on a phone

Until now the styleguide was hard to use on a phone, which was annoying whenever somebody sent a link to a component. Up to 900 px width, and on touch screens up to 500 px height (a phone in landscape), the layout changes now:

  • The navigation opens full screen from a menu button.
  • The canvas gets the whole width, and the panel sits below it at about half the height. Tap the active tab and it folds away, drag the grip to resize it.
  • The header keeps what matters, the theme switch and the build info move into the menu.
  • Controls stack, touch targets are at least 40 px, and text fields use 16 px so iOS doesn't zoom in when you tap them.

The measuring tools stay out on a phone. Guides, rulers, the measuring tool and the viewport width need a mouse and a bigger screen to be useful.

the Button in the demo on a phone, panel open.

The easiest way to try it is to open the demo on your phone.

Accepting an accessibility violation

ng-prism runs axe on every variant. Sometimes a violation is known and you've decided to keep it, for now or for good. So far the only option was to disable the rule, and then it disappeared from the panel. In 22.7 you can accept it, with a reason:

meta: {
  a11y: {
    accept: [
      {
        rule: 'color-contrast',
        reason: 'The hint grey (#6b7280) reaches 4.47:1 on the light ground, a hair under the 4.5:1 WCAG AA asks of small text.',
      },
    ],
  },
},
Enter fullscreen mode Exit fullscreen mode

The reason is required. Accepted violations are still audited and listed under Accepted with their reason, they just don't count against the score. That's the difference to disable: you still see them, and so does the next person who looks at the component. meta.a11y is typed now too, so rules, disable and accept are checked by the compiler.

What axe can't decide on its own now shows up under Needs review instead of being dropped, and it doesn't count either. And the build gate is its own option, a11y.gate. If you leave it out, it's your thresholds like before. gate: false never fails the build, which is handy when your CI does the gating and the styleguide should still show the real numbers. The dev server doesn't stop over the gate anymore, it logs a warning and rebuilds when a new report comes in.

The Form Field's "With hint" variant in the demo has an accepted violation.

A new look

The colours changed. The primary colour is blue instead of violet, surfaces are slate and navy, and the header is a navy band in both themes. In the light theme the chrome is grey and the content white, it used to be the other way round. There's also a slow light moving through the header. If that bothers you, --prism-brand-motion: none stops it, and it holds still with reduced motion and in capture mode anyway.

the Button in the new look, dark and light side by side.

For plugin authors the building blocks of the built-in panels are exported from @ng-prism/core now: prism-badge, prism-tabs, prism-segmented, prism-panel-toolbar, prism-list-row, prism-metric-card and the button directives. A panel built from them looks like the rest of the styleguide without copying any CSS. All scores also follow one rule now. a11y, coverage, VRT and perf use the same statusFor(), so "danger" means below 75 % of the threshold everywhere, not only in the header.

If you override theme tokens, check the deprecations below.

Bundle size that matches what you ship

The perf plugin used to measure the raw .ts file. That included the @Showcase config, all the JSDoc and every comment, and @ng-prism/core showed up as an import. So the better documented a component was, the worse it scored, which is the opposite of what I want.

Now the file is measured without the decorator and without comments, stripped the same way your published build is. On a real library the largest component went from 77 kb (critical) to 35 kb (warning), and four components that only warned because of their docs are well below 20 kb now. It still measures TypeScript source and not compiled output, so it's an estimate, but at least it's an estimate of your code.

Smaller things

  • An active state shows up as a chip in the variant ribbon, and you can clear it from there.
  • Every component opens on the Playground. The view doesn't carry over from the last component anymore.
  • The sidebar scrolls the open component into view and expands its group after a reload or when you follow a link.
  • The controls panel shows a Reset button as soon as an input differs from the variant.
  • A directive output with a payload no longer breaks the build with TS2345.
  • Stretch showcases fill the viewport width, and absolutely positioned elements stay inside their Overview cell.
  • The resize grips are visible and work with touch.
  • The docs start page has a one-minute tour.

Deprecations before 23

As usual, breaking changes within 22 come as deprecations first. These go away in 23.0.0, and the build tells you if you use them:

  • The theme tokens --prism-mark-nominal, --prism-mark-attention and --prism-mark-critical. Use --prism-success, --prism-warn and --prism-danger. Your overrides still carry over until 23.
  • --prism-spectrum, --prism-primary-from and --prism-primary-to. Use --prism-brand-gradient, or --prism-primary-fill for fills. Overrides of these have no effect anymore.
  • The variant of a panel badge. Use kind, tone and label.
  • A service or pipe with component fields like variants or host is still treated as a component until 23. Remove those fields, or move the demo to a component page.

One more thing if you read __PRISM_MANIFEST__ or use onComponentScanned in a plugin: services, pipes and parts aren't in components anymore. Services and pipes have their own lists and hooks, and parts sit under their parent's parts.

Upgrading

ng update @ng-prism/core
Enter fullscreen mode Exit fullscreen mode

Update the plugins you use at the same time, and the JSDoc plugin in particular, because the new API pages come from there: ng update @ng-prism/core @ng-prism/plugin-jsdoc.

Which parts of your library still don't show up anywhere? Guards, interceptors, whole forms? I'd like to plan what comes next around real libraries, so let me know in the comments or open an issue.

Top comments (0)