Visualizer Declarations
A visualizer declaration tells a viewer how to present one declaration. A viewer is a tool that
shows NX source to people, such as the <nx-viewer> reading view from @nx-lang/viewer that the
Visual view of the playground shows. It reads each element as a row, its kind and the
one value that names it, and opens an inspector of its properties under the row you select. Without
visualizer declarations it reads every element the same way, labelling each property after its name.
A library author knows better, and a visualizer declaration says so:
import "@nx/visualizer"
type Choice = { value:string label:string description?:string }
visualizer Choice = <ElementVisualizer title=label id=value idPosition=end icon="circle-dot" />Now each <Choice value="eng" label="Engineering" /> in a list of choices reads as one row: a
circle-dot icon, “Engineering”, and eng as a quiet identifier after it. A visualizer can also give each
property a label for the inspector; see @nx/visualizer.
A visualizer declaration is applied, not weighed. The compiler resolves every visualizer declaration that applies to a declaration into one result, the same for every reader, and a viewer presents the declaration exactly as that result says. It changes how source reads, never what it means: a visualizer declaration runs nothing, and it does not reach NX IR, generated code or a declaration schema.
Writing one
Section titled “Writing one”A visualizer declaration is a module member, visualizer Target = Value:
Targetnames the declaration the visualizer describes. It is resolved like any other name in the module, so it can be qualified (Form.Submitted) or imported.Valueis built from the records of the@nx/visualizerstandard library, so the module mustimport "@nx/visualizer".visualizeris a keyword only as the first token of a module member. A prop, field, binding or function namedvisualizerkeeps working.- A visualizer declaration declares no name, takes no visibility modifier, and cannot be referred to. It applies wherever its module is loaded, without being exported or imported.
Which record the value takes depends on what the target is:
| Target | Record |
|---|---|
| A record, an action, an inline emitted action, or a component, concrete, abstract or external | ElementVisualizer |
| An alias of a primitive type | ValueVisualizer |
An element-style function (let <Name …/>) |
FunctionVisualizer |
Anything else cannot have a visualizer: a union, a paren-style function, a value, an alias of anything but a
primitive type, a generic record or component, and a derived declaration such as User.Update,
User.Property or Card.Props.
import "@nx/visualizer"
type Range2 = { T:type start:T end:T }
visualizer Range2 = <ElementVisualizer /> // error: a generic record cannot have a visualizer yetA survey library
Section titled “A survey library”The visualizer declarations below sit beside the types they describe, in a library of survey questions:
import "@nx/visualizer"
/// A color, as CSS writes one.export type Color = stringexport type ChoiceLayout = list | grid | chipsexport type Choice = { value:string label:string tint?:Color }
export abstract component <Question id:string label:string help?:string required:boolean = true />export component <SingleChoice extends Question layout:ChoiceLayout = list allowsOther:boolean = false content choices:Choice+ /> = { <div /> }
visualizer Color = <ValueVisualizer format=color />visualizer Choice = <ElementVisualizer title=label id=value idPosition=end icon="circle-dot" />visualizer Question = <ElementVisualizer title=label id=id icon="circle-help" />
let roleQuestion = <SingleChoice id="role" label="Which describes your work best?" layout=chips> <Choice value="eng" label="Engineering" tint="#3b82f6" /> <Choice value="design" label="Design" /> </SingleChoice>The compiler resolves these into one visualizer for each declaration. SingleChoice has no visualizer
declaration of its own, so the visualizer for Question applies to it: its title is label, its id
id, and its icon circle-help. Each choice’s title is its label, its id its value, read after
its title, and every Color reads with a swatch. In the playground’s Visual view, roleQuestion
reads as a row headed SingleChoice role, and each choice as a row under it, such as
Choice Engineering eng Tint ■ #3b82f6.
The playground’s Visualizer declarations example is a smaller library of the same kind. Open it in the playground and switch to Visual to see the reading follow its visualizer declarations.
The target is bound for you
Section titled “The target is bound for you”Every @nx/visualizer record takes the declaration it describes as its type parameter Target, and
names that declaration’s properties with fields typed Target.Property. Inside a visualizer
declaration, Target is bound for every @nx/visualizer construction that leaves it unset, children
included, so title=label and <PropertyVisualizer property=required … /> need no Target=. A property
name is checked against the target, so a misspelling is a type error, and a rename in a library
breaks its visualizer declarations loudly:
import "@nx/visualizer"
type Choice = { label:string }
visualizer Choice = <ElementVisualizer title=lable /> // error: `lable` is not a case of `Choice.Property`A construction that binds Target itself must bind it to the visualizer declaration’s target. Outside
a visualizer declaration nothing is bound, and <ElementVisualizer /> is rejected for leaving Target
unspecified, as any generic record would be.
Components name their props
Section titled “Components name their props”A component is not a record, so it has no T.Property. A visualizer declaration on a component binds
Target to the component’s props record,
T.Props, and names its props through
T.Props.Property. In the survey library, title=label on Question is checked against
Question.Props.Property, which lists id, label, help and required. State fields are not
props, so a visualizer declaration cannot name them.
Values are written out
Section titled “Values are written out”A visualizer declaration’s value may hold only @nx/visualizer constructions, string, boolean and number
literals, and bare names of properties or cases. A braced expression, a reference to a value, a conditional, a
loop, an interpolated string and a call are each rejected. That lets the compiler read every visualizer
declaration without evaluating it:
import "@nx/visualizer"
type Choice = { label:string }let prefix = "Pick"
visualizer Choice = <ElementVisualizer icon={prefix} /> // error: a visualizer's values must be written outWhat a reading shows
Section titled “What a reading shows”The source tree a viewer reads carries each declaration’s resolved visualizer: the properties bound
to title and id, where the id reads, its icon, optional preview renderer descriptor, and each property’s format and editor hints. A visualizer
declaration changes how source reads, never what it means. The playground’s Visual view uses it this
way:
- Title. The value of the property bound to
titlereads alone on an element’s line, without its name:Engineeringrather thanlabel=Engineering. A folded element shows it after its header. - Id. The value of the property bound to
idreads as one quiet token: after the element’s name, or withidPosition=end, last on its line, so a list’s labels come first. - Icon.
iconnames a Lucide icon that an element starts with when it starts a line of its own. A kind that extends another inherits it, and an element with none takes one for its kind of declaration. - Formats. A string bound to a property with the format
idreads as a quiet token, and one with the formatcolorreads with a small square of its color before it. A format changes how a value looks, never what it is, and does nothing to a value that is not a string literal. - Editor hints. A number hinted with a range steps within it; a
number or a string with
ValueChoicecontent offers those values to pick under its field; a union hintedsegmentededits as a row of buttons; a string hintedmultilinetakes line breaks. See editor hints.
A property’s format, and each of its editor hints, comes from its PropertyVisualizer when that sets one, and otherwise from the
ValueVisualizer of the alias its type names, read as written, through ?, + and *. In the survey
library every property typed Color reads with a swatch, and
<PropertyVisualizer property=value format=id /> would read a choice’s value as an id wherever it
is written.
Visualizer declarations in another module
Section titled “Visualizer declarations in another module”A visualizer declaration does not have to sit beside the declaration it describes. The same syntax works in any module that can see the target, so a team can set its own presentation for a library it does not own:
// visualizers.nx, in the team's own programimport "@nx/visualizer"import "../survey"
visualizer SingleChoice = <ElementVisualizer icon="list-checks" />visualizer Choice = <ElementVisualizer idPosition=start />The visualizer declarations that apply to a program are those in all of its modules: the entry module, every module it imports, directly or through others, and the standard libraries. A visualizer declaration in a module the program does not load does not apply.
A host can also supply visualizer modules the program never imports, such as its own presentation for
libraries its users write against. It names them by module identity in the visualizers option of a
sourceTree request, and they are analyzed like any module, so a host visualizer declaration against a
renamed property fails to type-check:
viewer.tree = snapshot.sourceTree("nx:///survey/form.nx", { visualizers: { modules: ["tenant/visualizers.nx"] }});Without the visualizers option the answer carries no visualizers, exactly as before. A module the request
names but the workspace does not hold is reported as missing, and the program’s own visualizer
declarations still apply.
How visualizer declarations combine
Section titled “How visualizer declarations combine”A declaration’s resolved visualizer is built by applying visualizer declarations in order, each later one overriding only what it sets. The order is:
- Along inheritance, from the furthest base to the declaration itself.
- For each of those, first the visualizer declaration in the module that declares it, then the one in another module of the program, then those in the host’s visualizer modules, in the order the host names them.
Overriding works attribute by attribute: an attribute a later visualizer declaration sets replaces the
earlier value, and one it leaves unset keeps it. Every attribute is inherited. PropertyVisualizers
merge by their property, attribute by attribute too. With the survey library above and the module
before this section, a SingleChoice takes its title and id from Question and its own icon,
list-checks, and a Choice keeps its title, id and icon from the library while its id moves to the
start of its line.
Property order follows the later visualizer declaration: its PropertyVisualizers in its order, then
the earlier ones it does not name, in theirs, then the rest of the properties in declaration order.
Duplicate visualizer declarations
Section titled “Duplicate visualizer declarations”A module declares at most one visualizer for a declaration. A second one is a duplicate-visualizer error,
naming the first:
import "@nx/visualizer"
type Choice = { label:string }
visualizer Choice = <ElementVisualizer title=label />visualizer Choice = <ElementVisualizer icon="list" /> // error: duplicate-visualizerTwo modules of the program that do not declare Choice may not both declare a visualizer for it
either, since neither has a better claim to apply later. That is a duplicate-visualizer error naming
both, reported on each document whose program loads both: at the visualizer declaration when the
document holds one, and otherwise at the import that brings in the other. Neither of the two
applies. The declaring module, another program module and the host’s modules never conflict,
because they apply in order, and two host modules may both declare a visualizer for one target: the one
named later applies later.
See also
Section titled “See also”- Reference:
@nx/visualizer - Reference: Modules
- Reference: Component props and state
- Grammar: nx-grammar.md – Visualizer Declarations