#Custom elements and the DocumentSheet update flow

1 messages · Page 1 of 1 (latest)

little lintel
#

Okay, so, I have a sheet, whose template looks like this (with extra bits omitted):

<form>
  <ability-score-field
    name="system.scores.str.value"
    value="{{actor.system.scores.str.value}}"
    modifier-value="{{actor.system.scores.str.mod}}"
    data-dtype="Number"
  >
    Field A
  </ability-score-field>
  <ability-score-field
    name="system.scores.int.value"
    value="{{actor.system.scores.int.value}}"
    modifier-value="{{actor.system.scores.int.mod}}"
    data-dtype="Number"
  >
    Field B
  </ability-score-field>
</form>

The sheet class looks like this:

export default class SomeSheetClass extends ActorSheet {
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: `path/to/some/template.hbs`,
    });
  }

  _onChangeInput(e: any) {
    // ... whatever transformations might need to happen
    super._onChangeInput(e);
  }

  activateListeners(html: JQuery<HTMLElement>) {
    super.activateListeners(html);

    html.on("change", "ability-score-field", (e) => this._onChangeInput(e));
  }
}
#

My custom element looks like this:

export default class AbilityScoreField extends HTMLElement {
  static formAssociated = true;

  value: string = "";

  name: string = "";

  #internals: ElementInternals;

  #shadowRoot: ShadowRoot;

  constructor() {
    super();
    this.#internals = this.attachInternals();
    this.#shadowRoot = this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    this.#render();
    this.#shadowRoot
      .querySelector("input")
      ?.addEventListener("change", (e) => {
        this.onInput(e);
      });
  }

  get #label() {
    const label: HTMLLabelElement = document.createElement("label");
    label.setAttribute("for", this.id);

    const slot: HTMLSlotElement = document.createElement("slot");

    label.append(slot);

    return label;
  }

  get #input() {
    const input: HTMLInputElement = document.createElement("input");
    input.setAttribute("type", "text");
    input.setAttribute("name", this.getAttribute("name") || "");
    input.setAttribute("id", this.getAttribute("id") || "");
    input.setAttribute(
      "value",
      this.getAttribute("value")?.toString() || ""
    );
    input.toggleAttribute("readonly", this.hasAttribute("readonly"));
    input.toggleAttribute("disabled", this.hasAttribute("disabled"));
    return input;
  }

  #render() {
    this.#shadowRoot.append(this.#label as Node, this.#input);
  }

  onInput(e: Event) {
    const oldValue = parseInt(this.getAttribute("value") || "", 10);
    let newValue = parseInt((e.target as HTMLInputElement).value || "", 10);

    if (newValue < 0) newValue = 0;
    if (Number.isNaN(newValue)) newValue = oldValue;

    this.setValue(newValue.toString());
  }

  setValue(newValue: string = "") {
    this.value = newValue;
    this.setAttribute("value", newValue);
    this.#internals?.setFormValue(newValue);
    this.dispatchEvent(new Event("change", { bubbles: true }));
  }
}

customElements.define("ability-score-field", AbilityScoreField);
#

Everything here works fine -- editing the field updates the document as expected. But it removes focus from the sheet.

pastel apex
#

Right, because it completely destroys and recreates the DOM elements

little lintel
#

Which I figured, given how the next field in tab order gets focus when you tab out of a changed field; your cursor ends up at the start of the field, rather than selecting the content of the field.

#

That's the thing I'd like to mimic with my setup -- when I change focus, at least get the cursor in the next/previous field in tab order.

pastel apex
#

you would have to add some code to record what to focus after rendering again

little lintel
#

Yep! I was just wondering how Foundry's doing that, so I can try to stay hooked into how core's doing things.