form.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. /*!
  2. * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
  3. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
  4. * v1.0.0-beta.82
  5. */
  6. import { closestElementCrossShadowBoundary } from "./dom";
  7. import { h } from "@stencil/core";
  8. /**
  9. * Exported for testing purposes.
  10. */
  11. export const hiddenFormInputSlotName = "hidden-form-input";
  12. function isCheckable(component) {
  13. return "checked" in component;
  14. }
  15. const onFormResetMap = new WeakMap();
  16. const formComponentSet = new WeakSet();
  17. function hasRegisteredFormComponentParent(form, formComponentEl) {
  18. // we use events as a way to test for nested form-associated components across shadow bounds
  19. const formComponentRegisterEventName = "calciteInternalFormComponentRegister";
  20. let hasRegisteredFormComponentParent = false;
  21. form.addEventListener(formComponentRegisterEventName, (event) => {
  22. hasRegisteredFormComponentParent = event
  23. .composedPath()
  24. .some((element) => formComponentSet.has(element));
  25. event.stopPropagation();
  26. }, { once: true });
  27. formComponentEl.dispatchEvent(new CustomEvent(formComponentRegisterEventName, {
  28. bubbles: true,
  29. composed: true
  30. }));
  31. return hasRegisteredFormComponentParent;
  32. }
  33. /**
  34. * Helper to submit a form.
  35. */
  36. export function submitForm(component) {
  37. var _a;
  38. (_a = component.formEl) === null || _a === void 0 ? void 0 : _a.requestSubmit();
  39. }
  40. /**
  41. * Helper to reset a form.
  42. */
  43. export function resetForm(component) {
  44. var _a;
  45. (_a = component.formEl) === null || _a === void 0 ? void 0 : _a.reset();
  46. }
  47. /**
  48. * Helper to set up form interactions on connectedCallback.
  49. */
  50. export function connectForm(component) {
  51. const { el, value } = component;
  52. const form = closestElementCrossShadowBoundary(el, "form");
  53. if (!form || hasRegisteredFormComponentParent(form, el)) {
  54. return;
  55. }
  56. component.formEl = form;
  57. component.defaultValue = value;
  58. if (isCheckable(component)) {
  59. component.defaultChecked = component.checked;
  60. }
  61. const boundOnFormReset = (component.onFormReset || onFormReset).bind(component);
  62. form.addEventListener("reset", boundOnFormReset);
  63. onFormResetMap.set(component.el, boundOnFormReset);
  64. formComponentSet.add(el);
  65. }
  66. function onFormReset() {
  67. if (isCheckable(this)) {
  68. this.checked = this.defaultChecked;
  69. return;
  70. }
  71. this.value = this.defaultValue;
  72. }
  73. /**
  74. * Helper to tear down form interactions on disconnectedCallback.
  75. */
  76. export function disconnectForm(component) {
  77. const { el, formEl } = component;
  78. if (!formEl) {
  79. return;
  80. }
  81. const boundOnFormReset = onFormResetMap.get(el);
  82. formEl.removeEventListener("reset", boundOnFormReset);
  83. onFormResetMap.delete(el);
  84. component.formEl = null;
  85. formComponentSet.delete(el);
  86. }
  87. /**
  88. * Helper for setting the default value on initialization after connectedCallback.
  89. *
  90. * Note that this is only needed if the default value cannot be determined on connectedCallback.
  91. */
  92. export function afterConnectDefaultValueSet(component, value) {
  93. component.defaultValue = value;
  94. }
  95. /**
  96. * Helper for maintaining a form-associated's hidden input in sync with the component.
  97. *
  98. * Based on Ionic's approach: https://github.com/ionic-team/ionic-framework/blob/e4bf052794af9aac07f887013b9250d2a045eba3/core/src/utils/helpers.ts#L198
  99. */
  100. function syncHiddenFormInput(component) {
  101. const { el, formEl, name, value } = component;
  102. const { ownerDocument } = el;
  103. const inputs = el.querySelectorAll(`input[slot="${hiddenFormInputSlotName}"]`);
  104. if (!formEl || !name) {
  105. inputs.forEach((input) => input.remove());
  106. return;
  107. }
  108. const values = Array.isArray(value) ? value : [value];
  109. const extra = [];
  110. const seen = new Set();
  111. inputs.forEach((input) => {
  112. const valueMatch = values.find((val) =>
  113. /* intentional non-strict equality check */
  114. val == input.value);
  115. if (valueMatch != null) {
  116. seen.add(valueMatch);
  117. defaultSyncHiddenFormInput(component, input, valueMatch);
  118. }
  119. else {
  120. extra.push(input);
  121. }
  122. });
  123. let docFrag;
  124. values.forEach((value) => {
  125. if (seen.has(value)) {
  126. return;
  127. }
  128. let input = extra.pop();
  129. if (!input) {
  130. input = ownerDocument.createElement("input");
  131. input.slot = hiddenFormInputSlotName;
  132. }
  133. if (!docFrag) {
  134. docFrag = ownerDocument.createDocumentFragment();
  135. }
  136. docFrag.append(input);
  137. defaultSyncHiddenFormInput(component, input, value);
  138. });
  139. if (docFrag) {
  140. el.append(docFrag);
  141. }
  142. extra.forEach((input) => input.remove());
  143. }
  144. function defaultSyncHiddenFormInput(component, input, value) {
  145. var _a;
  146. const { defaultValue, disabled, name, required } = component;
  147. // keep in sync to prevent losing reset value
  148. input.defaultValue = defaultValue;
  149. input.disabled = disabled;
  150. input.name = name;
  151. input.required = required;
  152. input.tabIndex = -1;
  153. if (isCheckable(component)) {
  154. // keep in sync to prevent losing reset value
  155. input.defaultChecked = component.defaultChecked;
  156. // heuristic to support default/on mode from https://html.spec.whatwg.org/multipage/input.html#dom-input-value-default-on
  157. input.value = component.checked ? value || "on" : "";
  158. // we disable the component when not checked to avoid having its value submitted
  159. if (!disabled && !component.checked) {
  160. input.disabled = true;
  161. }
  162. }
  163. else {
  164. input.value = value || "";
  165. }
  166. (_a = component.syncHiddenFormInput) === null || _a === void 0 ? void 0 : _a.call(component, input);
  167. }
  168. /**
  169. * Helper to render the slot for form-associated component's hidden input.
  170. *
  171. * If the component has a default slot, this must be placed at the bottom of the component's root container to ensure it is the last child.
  172. *
  173. * render(): VNode {
  174. * <Host>
  175. * <div class={CSS.container}>
  176. * // ...
  177. * <HiddenFormInputSlot component={this} />
  178. * </div>
  179. * </Host>
  180. * }
  181. *
  182. * Note that the hidden-form-input Sass mixin must be added to the component's style to apply specific styles.
  183. */
  184. export const HiddenFormInputSlot = ({ component }) => {
  185. syncHiddenFormInput(component);
  186. return h("slot", { name: hiddenFormInputSlotName });
  187. };