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

[DOM Parts] Add new declarative syntax and update iterative proposal #1023

Open
wants to merge 2 commits into
base: gh-pages
Choose a base branch
from

Conversation

tbondwilkinson
Copy link
Contributor

This PR makes various changes against the current DOM parts proposal:

  1. Adds a prospective declarative proposal. The basic syntax is {{}} for a self-contained part, {{#}}{{/}} for part with content. Parsing is enabled with a parseparts attribute on any DOM node.
  2. DocumentPartRoot or ChildNodePart are now containers for other nested parts. Browsers will need to maintain the nested list of parts.
  3. Cloning operation exists on DocumentPartRoot and ChildNodePart and returns a new Part.

This removes some options that we have decided against.
1. DocumentPart and ChildNodePart cache child parts
2. DocumentPart and ChildNodePart clone
3. Partial attribute updating is supported with either array value or
     TemplateStringsArray
@tbondwilkinson tbondwilkinson changed the title Add new declarative syntax and update iterative proposal [DOM Parts] Add new declarative syntax and update iterative proposal Jul 31, 2023
proposals/DOM-Parts-Declarative.md Outdated Show resolved Hide resolved
proposals/DOM-Parts-Declarative.md Outdated Show resolved Hide resolved
proposals/DOM-Parts-Declarative.md Outdated Show resolved Hide resolved
```

This could be exposed on the imperative API to be consumed in JavaScript by
application logic.
Copy link
Contributor

Choose a reason for hiding this comment

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

Feel free to punt, but have we considered giving the metadata as a string rather than a string array? People may want to use a variety of metadata encodings, e.g. JSON, and it'd be easier to just pass along the raw contents and let them split by spaces if they'd like to do that.

Related, but we'll want to specify how to escape }} inside of part syntax, and how to escape {{ inside parsepart html

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yeah it's not clear, but the reason metadata is a string is not because we split the string, it's because you might actually have metadata in two places for the same part.

{{# metadata1}}foo{{/ metadata2}}

I'll call this out.

proposals/DOM-Parts-Declarative.md Outdated Show resolved Hide resolved
proposals/DOM-Parts-Imperative.md Show resolved Hide resolved
proposals/DOM-Parts-Imperative.md Show resolved Hide resolved
Comment on lines +215 to +222
Instead of `AttributePart` having a single string `value`, it could optionally
take an `Array` that contains values that should be concatenated together. This
allows updating individually parts of the attribute without needing to serialize
the entire string.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Instead of `AttributePart` having a single string `value`, it could optionally
take an `Array` that contains values that should be concatenated together. This
allows updating individually parts of the attribute without needing to serialize
the entire string.
Instead of `AttributePart` having a single string `value`, it could instead
take an `Array` with values that will be interleaved with those from the `strings` array.

If we go with the strings design, we might want to require that values be an array, to be interleaved with the strings. I suppose we could take a non-array value if there's only two strings, but in practice I expect template systems to just always pass through an array here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we need to think more about the DX of this.

proposals/DOM-Parts-Imperative.md Outdated Show resolved Hide resolved
@EisenbergEffect
Copy link

I'd like to get a bit of clarification on syntax around parts with content and metadata. So, imagine a part that represents email content. It wants to provide both the binding expression metadata and the SSR'd value of the email. Would that look like this?...

{{# email.expression.here}}[email protected]{{/}}

It might be good to clarify this in the document.

@sashafirsov
Copy link

If you agree, please downvote this proposal.

By imposing extra curly brace in attributes you are:

  • adding extra 2 characters with no advantage behind
  • increasing the reading complexity
  • introducing additional syntax/parser into platform where enough parsers already exist
  • adding more diversity to syntax where existing ones can be reused

From prospective of normal developer and manager {{ }} is additional cost for training and skills.
I do not see any positive features related to such syntax. But there are bunch of draw backs.

What is available on the platform as of now?

  • JS template syntax. ${jsExpr}. At least all who do the web development familiar with this syntax and parser is part of JS engine. The question of JS scope vs template context recognition is not related to template syntax, and in current state of things is allover industry where JS context been part of templates. Say in JSX. Not template-oriented though. Useful mostly in embedded context.
  • XSLT/XPath syntax. {xPathExpr} is a shorter and still quite powerful. My favorite due to template-oriented features.

Both syntax are available for DX as

  • evaluation eval() and API accordingly
  • well tested, time proven
  • have a tons of validators as in CLI as in IDE level
  • syntax colored

And all of this usefulness you want to dump in favor of custom solution?

@rictic
Copy link
Contributor

rictic commented Aug 2, 2023

@sashafirsov We're not committed to the {{}} syntax, at the face to face it was agreed that it's a strawman, a placeholder for the time being. The goal is to get implementor and library feedback as to whether the semantics are sufficient and whether we can integrate it into HTML in a backwards compatible way that's not too disruptive to browsers' parsers.

@sashafirsov
Copy link

@rictic , even if the syntax just a placeholder, the principle of attributes vs dom parts markup by same convention is important to separate on current discussion level. It can be done as in backward compatible as in breaking fashion. The parts within attribute already have a built-into-browser solution: XSLT convention. The dom parts as well: either as custom element or <xsl:value-of>

I see the proposal as attempt to swing away from ability to reuse the existing platform features along with complimentary advantages without matching compensation.

The <part> is the good approach which can be generated by whatever syntax the build toolchain support whether it is JSX or mustache syntax. But the reverse ( as in proposal) is not that easy.

There is a compromise which can/should come with separate proposal: the source DOM alias:
✂️=<part> using either html entities extension of another mechanism. This way the mustache {{ would be aliased to <part> or another string in parsing time. But if we would state the synthetic syntax upfront, that opportunity would not be an option.

@WICG WICG locked and limited conversation to collaborators Aug 2, 2023
@rniwa
Copy link
Collaborator

rniwa commented Aug 2, 2023

Please take the discussion of your proposal elsewhere. This issue is specifically about template / DOM part API.

@WICG WICG unlocked this conversation Aug 2, 2023
@tbondwilkinson tbondwilkinson force-pushed the pr-iteration branch 2 times, most recently from da053bb to 81ead89 Compare August 3, 2023 18:51
@tbondwilkinson
Copy link
Contributor Author

I'd like to get a bit of clarification on syntax around parts with content and metadata. So, imagine a part that represents email content. It wants to provide both the binding expression metadata and the SSR'd value of the email. Would that look like this?...

{{# email.expression.here}}[email protected]{{/}}

It might be good to clarify this in the document.

I added explicit mention of this in the declarative metadata section. How does that look?

@tbondwilkinson
Copy link
Contributor Author

To respond to Sasha briefly.

I think we are operating with the assumption that the majority of developers are working with frameworks, and I don't know of any popular frameworks that use XSLT syntax for templating. This has been true for many years. The choice of {{}} is not the standard for all templating languages, but it's close enough that we can use it as a placeholder for now before we survey more developers. XSLT does not seem to have developer interest, and adding new features that relied on it would seem to go against where the majority of developers are headed.

I think a number of us are also confident that we want a declarative syntax for template output, rather than just an imperative solution, which to me rules out template literal syntax in HTML. Template literals also have no concept of "start" "stop" ranges, and are merely insertion points for expressions, and we'd want our declarative syntax to be quite a bit more expressive.

I think {} syntax is still a candidate, but I don't think we are as interested in <xsl:attribute> or <part> syntax to mark specific attributes or nodes as parts. I feel strongly emojis are completely off the table for DOM part markers even as a "shorthand."

@justinfagnani
Copy link
Contributor

Another important point is that the syntax is also an instruction to the browser and may very well not be directly exposed to all users.

In my world, Lit-style templates would still use JS template literals with standard JS syntax, but be able to very easily use the browser's syntax to create an HTML template with parts:

const makeTemplate = (strings) => {
  const template = document.createElement('template');
  template.setAttribute('parseparts', '');
  template.innerHTML = strings.join('{{}}');
  return template;

The import part here is that the delimiter ('{{}}') is valid in attribute position, which rules out elements and comments.

@bahrus
Copy link

bahrus commented Aug 3, 2023

@tbondwilkinson:

The choice of {{}} is not the standard for all templating languages, but it's close enough that we can use it as a placeholder for now before we survey more developers.

I like the direction this is going. If I'm understanding correctly, the most important feedback should come from the server-side generating frameworks. If that is correct, I would encourage, if possible, surveying developers not just from the node-centric world, but include perhaps some representatives from beyond, a small slice of which may be using XSLT to generate the HTML. It seems that the key will be how easy it would be for early adopter users initially to extend the template syntax to support the desired output. Asp.net razor pages have support for custom tag helpers/ attribute helpers, which would allow for syntax that is defined in an elegant, semantic way (could even use xslt tags if so desired), which would emit the needed markers, similar to what it sounds like lit would support. XSLT supports user defined functions which could also generate the desired output, I believe.

So it seems to me looking for syntax that matches the existing authoring / templating languages on the server side may not be as important as their ability to flexibly emit what is needed, but I might be missing something. I suspect it may not matter all that much if {{}} is used versus ${}, for example, echoing what @justinfagnani said.

]);
// Syntax to be improved. Here, a new AttributePart is created between each string.
const part = AttributePart(document.getPartRoot(), element, "href");
part.value = ["mailto: ", email];
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should think about making this behavior consistent and intuitive across the case where there are statics vs when there aren't. We also need to work with trusted types, so that we can set part.value to a trusted type in a page where trusted types are enforced.

One proposal:

An attribute is said to be fully controlled by an AttributePart's value if the statics are ['', '']. In that case, commit behaves like:

  #commitFullyControlled() {
    const value = Array.isArray(this.value) ? this.value[0] : this.value;
    if (value == null || value === false) {
      this.element.removeAttributeNS(this.namespaceURI, this.localName);
      return;
    }
    this.element.setAttributeNS(this.namespaceURI, this.localName, value);
  }

Note: if value is an array, we use its first element. This is to be consistent with the behavior when there are nonempty statics. null, undefined, and false when set on a fully controlled attribute remove it from the element.

If the attribute is not fully controlled by the value, then commit behaves like:

  #commitWithStatics() {
    const value = Array.isArray(this.value) ? this.value : [this.value];
    let pieces = [this.statics[0]];
    for (let i = 1; i < statics.length; i++) {
      pieces.push(value[i - 1] ?? '', this.statics[i]);
    }
    return pieces.join('');
  }

Note: if the values array is too short, then the ?? '' will write it as empty string, and excess elements are ignored.

And for completeness:

  commit() {
    if (this.statics.length === 2 && this.statics[0] === '' && this.statics[1] === '') {
      this.#commitFullyControlled();
    } else {
      this.#commitWithStatics();
    }
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I updated the wording a bit in this section to more cleanly spell out how statics and values fit together.

I'm not sure on statics = ['', ''] being the boundary condition. Why not just statics = undefined in the case where the AttributePart was created without statics? We can throw an Error during construction if someone tries to create an AttributePart with a static array that is less than 2 in length.

Copy link
Contributor

Choose a reason for hiding this comment

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

['', ''] may be more elegant, but undefined may be more performant, and I'd prefer the more performant option. No objection from me for undefined

proposals/DOM-Parts-Imperative.md Show resolved Hide resolved
## Choice of marker

The `{{}}` and `{{#}}{{/}}` are reasonable DOM part markers, but this is open to
proposals. It's possible to even allow the page to decide ewhat their markers

Choose a reason for hiding this comment

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

nit: s/ewhat/what

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.

9 participants