form.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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.97
  5. */
  6. import { o as closestElementCrossShadowBoundary } from './dom.js';
  7. import { h } from '@stencil/core/internal/client/index.js';
  8. /**
  9. * Exported for testing purposes.
  10. */
  11. 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. * @param component
  37. * @returns true if its associated form was submitted, false otherwise.
  38. */
  39. function submitForm(component) {
  40. const { formEl } = component;
  41. if (!formEl) {
  42. return false;
  43. }
  44. "requestSubmit" in formEl ? formEl.requestSubmit() : formEl.submit();
  45. return true;
  46. }
  47. /**
  48. * Helper to reset a form.
  49. *
  50. * @param component
  51. */
  52. function resetForm(component) {
  53. var _a;
  54. (_a = component.formEl) === null || _a === void 0 ? void 0 : _a.reset();
  55. }
  56. /**
  57. * Helper to set up form interactions on connectedCallback.
  58. *
  59. * @param component
  60. */
  61. function connectForm(component) {
  62. const { el, value } = component;
  63. const form = closestElementCrossShadowBoundary(el, "form");
  64. if (!form || hasRegisteredFormComponentParent(form, el)) {
  65. return;
  66. }
  67. component.formEl = form;
  68. component.defaultValue = value;
  69. if (isCheckable(component)) {
  70. component.defaultChecked = component.checked;
  71. }
  72. const boundOnFormReset = (component.onFormReset || onFormReset).bind(component);
  73. form.addEventListener("reset", boundOnFormReset);
  74. onFormResetMap.set(component.el, boundOnFormReset);
  75. formComponentSet.add(el);
  76. }
  77. function onFormReset() {
  78. if (isCheckable(this)) {
  79. this.checked = this.defaultChecked;
  80. return;
  81. }
  82. this.value = this.defaultValue;
  83. }
  84. /**
  85. * Helper to tear down form interactions on disconnectedCallback.
  86. *
  87. * @param component
  88. */
  89. function disconnectForm(component) {
  90. const { el, formEl } = component;
  91. if (!formEl) {
  92. return;
  93. }
  94. const boundOnFormReset = onFormResetMap.get(el);
  95. formEl.removeEventListener("reset", boundOnFormReset);
  96. onFormResetMap.delete(el);
  97. component.formEl = null;
  98. formComponentSet.delete(el);
  99. }
  100. /**
  101. * Helper for setting the default value on initialization after connectedCallback.
  102. *
  103. * Note that this is only needed if the default value cannot be determined on connectedCallback.
  104. *
  105. * @param component
  106. * @param value
  107. */
  108. function afterConnectDefaultValueSet(component, value) {
  109. component.defaultValue = value;
  110. }
  111. const hiddenInputChangeHandler = (event) => {
  112. event.target.dispatchEvent(new CustomEvent("calciteInternalHiddenInputChange", { bubbles: true }));
  113. };
  114. const removeHiddenInputChangeEventListener = (input) => input.removeEventListener("change", hiddenInputChangeHandler);
  115. /**
  116. * Helper for maintaining a form-associated's hidden input in sync with the component.
  117. *
  118. * Based on Ionic's approach: https://github.com/ionic-team/ionic-framework/blob/e4bf052794af9aac07f887013b9250d2a045eba3/core/src/utils/helpers.ts#L198
  119. *
  120. * @param component
  121. */
  122. function syncHiddenFormInput(component) {
  123. const { el, formEl, name, value } = component;
  124. const { ownerDocument } = el;
  125. const inputs = el.querySelectorAll(`input[slot="${hiddenFormInputSlotName}"]`);
  126. if (!formEl || !name) {
  127. inputs.forEach((input) => {
  128. removeHiddenInputChangeEventListener(input);
  129. input.remove();
  130. });
  131. return;
  132. }
  133. const values = Array.isArray(value) ? value : [value];
  134. const extra = [];
  135. const seen = new Set();
  136. inputs.forEach((input) => {
  137. const valueMatch = values.find((val) =>
  138. /* intentional non-strict equality check */
  139. val == input.value);
  140. if (valueMatch != null) {
  141. seen.add(valueMatch);
  142. defaultSyncHiddenFormInput(component, input, valueMatch);
  143. }
  144. else {
  145. extra.push(input);
  146. }
  147. });
  148. let docFrag;
  149. values.forEach((value) => {
  150. if (seen.has(value)) {
  151. return;
  152. }
  153. let input = extra.pop();
  154. if (!input) {
  155. input = ownerDocument.createElement("input");
  156. input.slot = hiddenFormInputSlotName;
  157. }
  158. if (!docFrag) {
  159. docFrag = ownerDocument.createDocumentFragment();
  160. }
  161. docFrag.append(input);
  162. // emits when hidden input is autofilled
  163. input.addEventListener("change", hiddenInputChangeHandler);
  164. defaultSyncHiddenFormInput(component, input, value);
  165. });
  166. if (docFrag) {
  167. el.append(docFrag);
  168. }
  169. extra.forEach((input) => {
  170. removeHiddenInputChangeEventListener(input);
  171. input.remove();
  172. });
  173. }
  174. function defaultSyncHiddenFormInput(component, input, value) {
  175. var _a;
  176. const { defaultValue, disabled, name, required } = component;
  177. // keep in sync to prevent losing reset value
  178. input.defaultValue = defaultValue;
  179. input.disabled = disabled;
  180. input.name = name;
  181. input.required = required;
  182. input.tabIndex = -1;
  183. if (isCheckable(component)) {
  184. // keep in sync to prevent losing reset value
  185. input.defaultChecked = component.defaultChecked;
  186. // heuristic to support default/on mode from https://html.spec.whatwg.org/multipage/input.html#dom-input-value-default-on
  187. input.value = component.checked ? value || "on" : "";
  188. // we disable the component when not checked to avoid having its value submitted
  189. if (!disabled && !component.checked) {
  190. input.disabled = true;
  191. }
  192. }
  193. else {
  194. input.value = value || "";
  195. }
  196. (_a = component.syncHiddenFormInput) === null || _a === void 0 ? void 0 : _a.call(component, input);
  197. }
  198. /**
  199. * Helper to render the slot for form-associated component's hidden input.
  200. *
  201. * 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.
  202. *
  203. * render(): VNode {
  204. * <Host>
  205. * <div class={CSS.container}>
  206. * // ...
  207. * <HiddenFormInputSlot component={this} />
  208. * </div>
  209. * </Host>
  210. * }
  211. *
  212. * Note that the hidden-form-input Sass mixin must be added to the component's style to apply specific styles.
  213. *
  214. * @param root0
  215. * @param root0.component
  216. */
  217. const HiddenFormInputSlot = ({ component }) => {
  218. syncHiddenFormInput(component);
  219. return h("slot", { name: hiddenFormInputSlotName });
  220. };
  221. export { HiddenFormInputSlot as H, afterConnectDefaultValueSet as a, connectForm as c, disconnectForm as d, resetForm as r, submitForm as s };