Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for appearances, body classes #127

Merged
merged 6 commits into from
Jun 6, 2024

Conversation

eyelidlessness
Copy link
Member

Closes #56.

This is primarily intended to introduce feature support for appearance, from parse through to the engine's client interface, represented by an appearances property on each node. Nodes which do not support appearances are assigned appearances: null for object shape consistency (as is typically the case across node types, with some exceptions for RootNode; more on that below).

Nodes that do support appearances currently include:

  • GroupNode
  • RepeatInstanceNode*
  • RepeatRangeNode*
  • SelectNode
  • StringNode**

*: Presently both repeat node types share the same appearances definition. There are some JSDoc comments and commit notes discussing the details of this.

**: A StringNode's appearance will be present if the node's definition has a non-null bodyElement (presently an InputDefinition). This may be less confusing if we pursue additional node types, as discussed in #73 (i.e. we could better distinguish between a "string node" that's associated with an <input>, versus a "string node" that's only associated with a model node and/or <bind>).

The value for each node's appearances are produced by a node-specific TokenListParser into a TokenList1 with roughly the same behavior:

  • For each of that node's appearances (as defined as a whitespace separated list in an appearance attribute, on that node's body element), node.appearances.$appearanceName will produce true (and will produce false for any*** appearance not present on the element in the form definition). For clients which are concerned with specific appearances (i.e. branching on those specific appearances to produce differences in rendering, presentation, interactive behavior), this is likely to be the most convenient way to access a node's appearances.

    *** Excepting aliases, see the last bullet point in this list.

  • The appearances object is also an Iterable, yielding each such appearance from its definition in the form (e.g. for (const appearance of node.appearances) { ... }). It's likely this will have more utility for testing purposes.

  • There is some variance in the appearances types for each respective node type. These are primarily intended to provide additional developer convenience in clients. For each underlying body element type, there are a set of "canonical" (known, first class, explicitly documented, and so on). These are specified as part of body parsing logic, and those appearances are promoted in the node appearances type to provide additional editor support (e.g. autosuggest on dot access and/or when referencing an iterated appearance).

  • All form-defined appearances are passed through in the runtime value, even if they are not declared as "canonical". These are not included in type suggestions, because they're not known upfront. But if present in the form definition, non-canonical/unknown appearances will also produce true on dot access and will be yielded on iteration.

  • Node specific aliases: mostly included as a proof of concept (for now), each node type's TokenListParser may specify an aliases option, mapping any particular appearance to another "canonical" appearance. The behavior of aliases is additive: if a form definition specifies an appearance which is aliased to a "canonical" appearance, both the specified appearance and its corresponding "canonical" appearance will produce true on dot access, and both will be yielded on iteration (with the "canonical" appearance yielded first). This is currently only used to map a single select appearance (search -> autocomplete), but is intended to demonstrate the generalization. Ideally we will expand on these so that the engine can act as a source of truth for all known/supported appearance types, without pushing that responsibility separately onto every client.

Minor tech debt cleanup, related open question

Elimination of "repeat group"/"repeat" pairing in the parsed body representation

There has been some lingering awkwardness in the form parsing logic around the concept of "repeat groups". Earlier development introduced a pre-parse, in-memory normalization to ensure that every <repeat nodeset> element is wrapped in a corresponding <group ref> (where the nodeset/ref are the same. This is the most common production from XLSForms as produced by pyxform. The intent was to simplify the parsed data model by producing a consistent structure for repeats regardless of whether the form itself specifies a <group ref><repeat nodeset> pair. Even at the time, it was noted that the resulting data model was somewhat awkward, with the parsed representation of a repeat's body always including a corresponding pair of objects for what is ultimately treated as a singular thing.

Starting on the work to support appearances, this awkwardness resurfaced. I had also recognized that the awkwardness overlaps with other coming feature work (support for jr:count and jr:noAddRemove come to mind, and from recent prototyping on those I anticipate this change will bring some meaningful simplicity).

Rename remaining references to repeat "sequence" to "range"

Before #67, the concept of a set of zero-or-more contiguous repeat instances was called a "sequence". In that PR, we settled on the name "range" (RepeatRange &co). Since I was already doing some minor refactoring on the above "repeat group" concept, it felt appropriate to tackle this lingering naming inconsistency. So all references to the same "bag or vat of repeat instances" are now consistently referenced with the "range" terminology.

Open question regarding repeat appearances

As I understand the spec, appearances may be specified only on form controls and groups. In practice, I have noted many form fixtures (particularly in Enketo) define appearances directly on <repeat> elements. With the aforementioned change to normalize "repeat groups" into a singular concept, both of these would be treated as interchangeable:

<group ref="/data/rep" appearance="foo"><repeat nodeset="/data/rep"/></group>
<group ref="/data/rep"><repeat nodeset="/data/rep"/ appearance="foo"></group>

My instinct is that this is the most likely expectation, although that can also be framed as a question here. So: is that what users would most likely expect?

Perhaps more ambiguous in intent: what would users expect if both nodes specify appearances? I.e.

<group ref="/data/rep" appearance="..."><repeat nodeset="/data/rep"/ appearance="..."></group>

This is presently treated as an error. It's conceivable that:

  • They should be treated differently no matter what
  • They should be treated differently iff the containing group has a <label> (this is currently a presentational difference in the web-forms Vue UI client)
  • They should be treated as a single definition iff they match
  • They should be treated as a single definition and merged/concatenated
  • ??? There's probably some other option I haven't even thought of

Landing sequence

I'm opening in earnest, as I understand it will unblock quite a bit of UI client work. I'm opening it as draft, because I would like to land these other PRs first:

Footnotes

  1. As discussed in more detail in the JSDoc for TokenListParser/TokenList, these names are a reference to a web standard DOMTokenList, as it is very similar conceptually. But I'm totally open to bikeshedding these names if they don't feel right.

Note that this introduces an in-memory `<label form-definition-source=“repeat-group”>` to distinguish outer/inner labels for such structures.

Example: for this form definition structure…

```xml
<group ref="/root/rep">
  <label>Repeat/group label</label>
  <repeat nodeset="/root/rep">
    <label>Repeat label</label>
  </repeat>
</group>
```

… this would be the normalized structure:

```xml
<repeat nodeset="/root/rep">
  <label form-definition-source=“repeat-group”>Repeat/group label</label>
  <label>Repeat label</label>
</repeat>
```
Note that `RepeatGroupDefinition` previously had two responsibilities:

1. To provide access to its `repeat`
2. To provide access to a label defined in the repeat’s containing group

The first is no longer necessary because the repeat is accessed directly.

The second is accommodated by defining the `RepeatElementDefinition`’s label from the special-case `<label form-definition-source=“repeat-group”>` as produced in the previous commit.

Also note that this still does not deal with the unhandled repeat labeling engine responsibility: <repeat><group><label/></group></repeat>. A TODO is added specifically so it can be traced back to this commit, as it will likely help shorten the path to reabsorbing the pertinent code/implementation details back into active brain memory.
Automated testing is pending. Some manual validation has been done to verify that this likely works as intended. The intent is to make this first pass available for client iteration as quickly as possible.

While it’s possible to include unit tests for `TokenListParser`, it seems more likely we’ll want to add integration tests in the `scenario` package. Given there’s a ton of unaddressed feedback in #110, it seems most prudent to get this into draft first, and bring in integration tests once that  lands.
@eyelidlessness eyelidlessness marked this pull request as ready for review May 31, 2024 21:34
@eyelidlessness eyelidlessness changed the title Initial support for appearances Initial support for appearances, body classes May 31, 2024
@eyelidlessness
Copy link
Member Author

Somehow I forgot to mention in the initial writeup that this also adds support for body classes. The functionality is so conceptually similar that it made sense to me to generalize it and tackle both together.

It feels a bit awkward at the RootNode client interface to have appearances: null; classes: EffectivelyTheSameDataType;, but they're named different things in the spec and have slightly different semantic implications, so I erred on the side of "let's not be too cute with naming".

@lognaturel
Copy link
Member

is that what users would most likely expect?

Yes, that sounds right to me.

what would users expect if both nodes specify appearances?

I think starting with an error is a good idea because it will allow us to know whether it actually happens. As far as I know, the only appearance that can apply to groups or repeats is field-list so it shouldn't really be an issue currently. Unless maybe Enketo has a concept of minimal to show a group or repeat as collapsed initially? This is something I suddenly have a fuzzy memory of. Does it ring a bell, @eyelidlessness? I don't think that changes my answer here, though.

this also adds support for body classes

I never registered that class was mentioned in https://getodk.github.io/xforms-spec/#body-attributes! As far as I know Collect would ignore those. Does Enketo apply them? Either way, seems something useful for Web Forms to support and the approach sounds good to me. I don't think it has to be restricted to only the body element necessarily.

@lognaturel
Copy link
Member

Re minimal: it looks like XForms recommends supporting minimal and compact for every control type as described in https://www.w3.org/MarkUp/Forms/wiki/Metadata_Appearance. I can't find that exact wording in the XForms 1.1 spec but I do see that most controls explicitly list full|minimal|compact as supported appearances. No action needed now I don't think but wanted to mention I'd noticed this. It could be relevant to upcoming work like maybe figuring out what we want to do about grids (#16)

@eyelidlessness
Copy link
Member Author

eyelidlessness commented May 31, 2024

is that what users would most likely expect?

Yes, that sounds right to me.

what would users expect if both nodes specify appearances?

I think starting with an error is a good idea because it will allow us to know whether it actually happens.

👍

As far as I know, the only appearance that can apply to groups or repeats is field-list so it shouldn't really be an issue currently. Unless maybe Enketo has a concept of minimal to show a group or repeat as collapsed initially? This is something I suddenly have a fuzzy memory of. Does it ring a bell, @eyelidlessness? I don't think that changes my answer here, though.

The other case I found is table-list.

The design is such that anything defined will be passed through, so anything that Enketo supports will be present, it just won't necessarily be explicit in the type-driven suggestions.

this also adds support for body classes

I never registered that class was mentioned in https://getodk.github.io/xforms-spec/#body-attributes! As far as I know Collect would ignore those. Does Enketo apply them? Either way, seems something useful for Web Forms to support and the approach sounds good to me. I don't think it has to be restricted to only the body element necessarily.

Enketo uses body class to specify pages, as well as themes (theme-*; this also includes theme-grid). If I recall, it also applies all classes as defined in the transform, regardless of whether they have any special meaning. For the latter purpose, I agree it would potentially make sense to apply consistently across node types with UI. For the former, it feels more like the kind of functionality we could align with a hypothetical appearance on body (or have some other mechanism entirely, but consistent use of appearance would make the difference between node types feel a lot less awkward/go away).

@lognaturel
Copy link
Member

The design is such that anything defined will be passed through, so anything that Enketo supports will be present, it just won't necessarily be explicit in the type-driven suggestions.

Right, that's perfect.

Enketo uses body class to specify pages, as well as themes (theme-*; this also includes theme-grid).

Ah, right, of course.

@lognaturel
Copy link
Member

One thing that just came to mind is that labels cannot be directly nested in repeats according to the spec: https://getodk.github.io/xforms-spec/#repeats That’s why repeats are generally nested in a group bound to the same node.

I think this just means that some of the work in the first commit won’t be used in practice. Seems ok to leave in for now, though.

Copy link
Contributor

@sadiqkhoja sadiqkhoja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. I really like the approach taken for handling appearance aliases 🎉

There are few inline comments, one about missing appearances of select may require a change, rest are just questions.

};

const normalizeRepeatGroupLabel = (group: Element, repeat: Element): void => {
const groupLabel = Array.from(group.children).find((child) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ should we throw an error if there are more than one label nodes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably warn, and probably everywhere we look for 0-1 nodes (all label lookups, hints also come to mind). My understanding of the spec is that only one is “properly supported”, which to me strongly implies ignoring >1.

Feels like a good issue to file, and/or category of things to add to a general discussion of how we’ll convey errors/warnings/notices about form definitions to clients.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to #80

'quickcompact',
'map',
// "quick map"
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓I am assuming that appearance (W1, W2... Wn) related to theme-grid of Enketo will be handled separately later on.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, they’re handled here already. They’ll be passed through if present. They’re not included in the types, but they can added be if/whenever we want to make them first class.

'label',
'list-nolabel',
'minimal',

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see following appearances from XLSForm template missing here:

  • no-buttons
  • list
  • image-map

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, compact is deprecated. It was split up into horizontal-compact and no-buttons which can be combined if desired.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are any known/canonical appearances (or aliases to them) that we want to add right away, please feel free to add them in! My hope is that this approach makes that trivial. I’d be happy to add these when my power is back, but you’re welcome to add them at your convenience as well.

@eyelidlessness
Copy link
Member Author

One thing that just came to mind is that labels cannot be directly nested in repeats according to the spec: https://getodk.github.io/xforms-spec/#repeats That’s why repeats are generally nested in a group bound to the same node.

I think this just means that some of the work in the first commit won’t be used in practice. Seems ok to leave in for now, though.

Ah I thought this was more common than it is. Turns out the only case I could find in Enketo was one I added myself 🙃, presumably when I was less familiar with these details of the spec.

@lognaturel
Copy link
Member

Have added changeset (that was fun), over to you @sadiqkhoja to either merge or align select appearances, up to you. 😊

@sadiqkhoja sadiqkhoja merged commit e7bef0c into main Jun 6, 2024
86 checks passed
@sadiqkhoja sadiqkhoja deleted the features/appearances-init branch June 6, 2024 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Represent appearances (parse, client interface)
3 participants