Specifying fields
The previous section on declaring custom elements provided examples on how to declare fields to specify user-configurable data for your element. Here, we will go more in depth.
When specifying an element's fields, you should use the field
function for each field. It has the following arguments:
-
name
(string, positional): the field name, by which it will be accessed in the dictionary returned bye.fields(element instance)
. -
type_
(type / typeinfo, positional): the field's type, defines how to check whether user input to this field is valid (read more about types in the Type system chapter). -
doc
(string, named, optional): documentation for the field. It is recommended to always set this. The documentation (and other field metadata) can later be retrieved by accessinge.data(elem).all-fields
, and can be used to auto-generate documentation, for example. -
required
(boolean, named, optional): whether this field is required (and thus can only be specified at the constructor, as it will have no default). This defaults to false. -
named
(boolean orauto
, named, optional): whether this field should be specified positionally (false
- that is, without its name) to the constructor and set rules, or named (true
- the user must specify the field by name). By default, this isauto
, which istrue
for required fields andfalse
for optional fields (but you can have required named fields and optional positional fields by changing both parameters accordingly). -
default
(any, named, optional): the default value for this field ifrequired
isfalse
. If this argument is omitted, the type's default is used (as explained below), or otherwise the field has no default (only possible if it is required). If a value is specified, that value becomes the field's default value (it will be cast to the given field type). It was done this way so you can also specifynone
as a default (which is common).Note that many types have their own default values, so you can usually omit
default
entirely, as the field will then just use that type's default. For example, if the type isint
, thendefault
will automatically be set to0
for such optional fields. If the type doesn't have its own default and the field is optional, you must specify your own default. -
synthesized
(boolean, named, optional): this is used to indicate that this is an automatically generated field during synthesis. A synthesized field cannot be manually set by the user - be it through the constructor or through set rules. However, it can be matched on in filters, such as for selecting elements throughe.select
. For example, if you have a synthesized field calledlevel
, a user can match onelem.with(level: 10)
.Therefore, you should always list automatically generated fields as synthesized fields. Elembic won't forbid you from adding "hidden fields" during synthesis, but then they will be seen as inexistent by the framework, so it is recommended to always list auto-generated fields as synthesized fields, with proper documentation for them.
Read more about synthesis below.
Synthesizing fields
Some elements will have conveniently auto-generated fields, which are created after set rules are applied, but before show rules. To do this, there are two steps:
-
List those fields as "synthesized" fields in the
fields
array. Such fields cannot be manually specified by users, however they can be matched on by functions such ase.select
. -
Create a synthesis step for your element with
synthesize: fields => updated fields
. Here, you can, for example, access Typst context, as well as usee.counter(it)
to read the counter, and so on.
Here's a bit of an artificial example just to showcase the functionality: we combine a stroke
field and a body
field to generate an applied
field, which is what is effectively displayed.
#import "@local/elembic:0.0.1" as e: field, types
#let frame = e.element.declare(
"frame",
prefix: "@preview/my-package,v1",
display: it => it.applied,
fields: (
field("body", content, doc: "Body to display.", required: true),
field("stroke", types.option(stroke), doc: "Stroke to add around the body."),
field("applied", content, doc: "Frame applied to the body.", synthesized: true)
),
synthesize: it => {
it.applied = block(stroke: it.stroke, it.body)
it
}
)
#show e.selector(frame): it => {
let applied = e.fields(it).applied
[The applied field was #raw(repr(applied)), as seen below:]
it
}
#frame(stroke: red + 2pt)[abc]