haori 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.ja.md +3 -1
  2. package/README.md +3 -1
  3. package/dist/haori.cjs.js +11 -11
  4. package/dist/haori.cjs.js.map +1 -1
  5. package/dist/haori.es.js +1134 -898
  6. package/dist/haori.es.js.map +1 -1
  7. package/dist/haori.iife.js +11 -11
  8. package/dist/haori.iife.js.map +1 -1
  9. package/dist/index.d.ts +38 -6
  10. package/dist/package.json +1 -1
  11. package/dist/src/core.d.ts.map +1 -1
  12. package/dist/src/core.js +1 -1
  13. package/dist/src/core.js.map +1 -1
  14. package/dist/src/env.d.ts +18 -0
  15. package/dist/src/env.d.ts.map +1 -1
  16. package/dist/src/env.js +44 -0
  17. package/dist/src/env.js.map +1 -1
  18. package/dist/src/event.d.ts +16 -1
  19. package/dist/src/event.d.ts.map +1 -1
  20. package/dist/src/event.js +4 -4
  21. package/dist/src/event.js.map +1 -1
  22. package/dist/src/haori.d.ts +13 -0
  23. package/dist/src/haori.d.ts.map +1 -1
  24. package/dist/src/haori.js +18 -0
  25. package/dist/src/haori.js.map +1 -1
  26. package/dist/src/index.d.ts +2 -1
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/index.js +1 -1
  29. package/dist/src/index.js.map +1 -1
  30. package/dist/src/intersect.d.ts +21 -0
  31. package/dist/src/intersect.d.ts.map +1 -0
  32. package/dist/src/intersect.js +188 -0
  33. package/dist/src/intersect.js.map +1 -0
  34. package/dist/src/observer.d.ts.map +1 -1
  35. package/dist/src/observer.js +5 -0
  36. package/dist/src/observer.js.map +1 -1
  37. package/dist/src/procedure.d.ts +18 -0
  38. package/dist/src/procedure.d.ts.map +1 -1
  39. package/dist/src/procedure.js +272 -172
  40. package/dist/src/procedure.js.map +1 -1
  41. package/dist/tests/env.test.js +28 -0
  42. package/dist/tests/env.test.js.map +1 -1
  43. package/dist/tests/event.test.js +13 -1
  44. package/dist/tests/event.test.js.map +1 -1
  45. package/dist/tests/intersect.test.d.ts +2 -0
  46. package/dist/tests/intersect.test.d.ts.map +1 -0
  47. package/dist/tests/intersect.test.js +173 -0
  48. package/dist/tests/intersect.test.js.map +1 -0
  49. package/dist/tests/procedure-bind-append.test.d.ts +2 -0
  50. package/dist/tests/procedure-bind-append.test.d.ts.map +1 -0
  51. package/dist/tests/procedure-bind-append.test.js +80 -0
  52. package/dist/tests/procedure-bind-append.test.js.map +1 -0
  53. package/dist/tests/procedure-fetch-options.test.js +58 -0
  54. package/dist/tests/procedure-fetch-options.test.js.map +1 -1
  55. package/package.json +1 -1
@@ -31,6 +31,58 @@ function resolveProcedureHaoriApi() {
31
31
  const hasRequiredMethods = PROCEDURE_HAORI_METHOD_NAMES.every(methodName => typeof candidate?.[methodName] === 'function');
32
32
  return hasRequiredMethods ? candidate : Haori;
33
33
  }
34
+ const QUERY_TRANSPORT_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
35
+ /**
36
+ * URL クエリ化の対象メソッドかどうかを判定します。
37
+ *
38
+ * @param method 判定対象の HTTP メソッド。
39
+ * @return クエリ送信対象なら true。
40
+ */
41
+ function isQueryTransportMethod(method) {
42
+ return QUERY_TRANSPORT_METHODS.has(method.toUpperCase());
43
+ }
44
+ /**
45
+ * 送信データを URLSearchParams に追加します。
46
+ *
47
+ * @param params 追加先の URLSearchParams。
48
+ * @param payload 追加対象の送信データ。
49
+ * @return 戻り値はありません。
50
+ */
51
+ function appendPayloadToSearchParams(params, payload) {
52
+ for (const [key, value] of Object.entries(payload)) {
53
+ if (value === undefined) {
54
+ continue;
55
+ }
56
+ if (value === null) {
57
+ params.append(key, '');
58
+ }
59
+ else if (Array.isArray(value)) {
60
+ value.forEach(item => {
61
+ params.append(key, String(item));
62
+ });
63
+ }
64
+ else if (typeof value === 'object' || typeof value === 'function') {
65
+ params.append(key, JSON.stringify(value));
66
+ }
67
+ else {
68
+ params.append(key, String(value));
69
+ }
70
+ }
71
+ }
72
+ /**
73
+ * 送信データをクエリ文字列へ付加した URL を返します。
74
+ *
75
+ * @param fetchUrl 元のフェッチ URL。
76
+ * @param payload 追加対象の送信データ。
77
+ * @return クエリ文字列を付加した URL。
78
+ */
79
+ function appendPayloadToUrl(fetchUrl, payload) {
80
+ const url = new URL(fetchUrl, window.location.href);
81
+ const params = new URLSearchParams(url.search);
82
+ appendPayloadToSearchParams(params, payload);
83
+ url.search = params.toString();
84
+ return url.toString();
85
+ }
34
86
  /**
35
87
  * 手続き的処理管理クラスです。
36
88
  */
@@ -401,6 +453,16 @@ ${body}
401
453
  const paramsString = fragment.getRawAttribute(bindParamsAttr);
402
454
  options.bindParams = paramsString.split('&').map(p => p.trim());
403
455
  }
456
+ const bindAppendAttr = event
457
+ ? Procedure.attrName(event, 'bind-append')
458
+ : Procedure.attrName(null, 'bind-append', true);
459
+ if (fragment.hasAttribute(bindAppendAttr)) {
460
+ const paramsString = fragment.getRawAttribute(bindAppendAttr);
461
+ options.bindAppendParams = paramsString
462
+ .split('&')
463
+ .map(p => p.trim())
464
+ .filter(Boolean);
465
+ }
404
466
  if (event) {
405
467
  if (fragment.hasAttribute(Procedure.attrName(event, 'adjust'))) {
406
468
  const adjustSelector = fragment.getRawAttribute(Procedure.attrName(event, 'adjust'));
@@ -600,180 +662,194 @@ ${body}
600
662
  * @returns 実行結果のPromise
601
663
  */
602
664
  run() {
665
+ return this.runWithResult().then(() => undefined);
666
+ }
667
+ /**
668
+ * 一連の処理を実行し、成功したかどうかを返します。
669
+ *
670
+ * @returns 成功した場合は true、途中停止や失敗時は false
671
+ */
672
+ runWithResult() {
673
+ return this.execute();
674
+ }
675
+ /**
676
+ * 一連の処理を実行します。成功結果を内部で扱うための実体です。
677
+ *
678
+ * @returns 実行成功時は true、停止や失敗時は false
679
+ */
680
+ async execute() {
603
681
  if (Object.keys(this.options).length === 0) {
604
- return Promise.resolve();
682
+ return false;
605
683
  }
606
684
  if (this.options.formFragment &&
607
685
  this.validate(this.options.formFragment) === false) {
608
- return Promise.resolve();
686
+ return false;
609
687
  }
610
- return this.confirm().then(confirmed => {
611
- if (!confirmed) {
612
- return Promise.resolve();
613
- }
614
- let fetchUrl = this.options.fetchUrl;
615
- let fetchOptions = this.options.fetchOptions;
616
- if (this.options.beforeCallback) {
617
- const result = this.options.beforeCallback(fetchUrl || null, fetchOptions || null);
618
- if (result !== undefined && result !== null) {
619
- if (result === false || (typeof result === 'object' && result.stop)) {
620
- return Promise.resolve();
621
- }
622
- if (typeof result === 'object') {
623
- fetchUrl = ('fetchUrl' in result ? result.fetchUrl : fetchUrl);
624
- fetchOptions = ('fetchOptions' in result ? result.fetchOptions : fetchOptions);
625
- }
688
+ const confirmed = await this.confirm();
689
+ if (!confirmed) {
690
+ return false;
691
+ }
692
+ let fetchUrl = this.options.fetchUrl;
693
+ let fetchOptions = this.options.fetchOptions;
694
+ if (this.options.beforeCallback) {
695
+ const result = this.options.beforeCallback(fetchUrl || null, fetchOptions || null);
696
+ if (result !== undefined && result !== null) {
697
+ if (result === false || (typeof result === 'object' && result.stop)) {
698
+ return false;
699
+ }
700
+ if (typeof result === 'object') {
701
+ fetchUrl = ('fetchUrl' in result ? result.fetchUrl : fetchUrl);
702
+ fetchOptions = ('fetchOptions' in result ? result.fetchOptions : fetchOptions);
626
703
  }
627
704
  }
628
- // フォーム値と data を統合してペイロードを作成
629
- const payload = {};
630
- if (this.options.formFragment) {
631
- const formValues = Form.getValues(this.options.formFragment);
632
- Object.assign(payload, formValues);
633
- }
634
- if (this.options.data && typeof this.options.data === 'object') {
635
- Object.assign(payload, this.options.data);
705
+ }
706
+ // フォーム値と data を統合してペイロードを作成
707
+ const payload = {};
708
+ if (this.options.formFragment) {
709
+ const formValues = Form.getValues(this.options.formFragment);
710
+ Object.assign(payload, formValues);
711
+ }
712
+ if (this.options.data && typeof this.options.data === 'object') {
713
+ Object.assign(payload, this.options.data);
714
+ }
715
+ const hasPayload = Object.keys(payload).length > 0;
716
+ if (fetchUrl) {
717
+ const finalOptions = { ...(fetchOptions || {}) };
718
+ const headers = new Headers(finalOptions.headers || undefined);
719
+ const requestedMethod = (finalOptions.method || 'GET').toUpperCase();
720
+ const isDemoQueryNormalization = Env.runtime === 'demo' && !isQueryTransportMethod(requestedMethod);
721
+ const method = isDemoQueryNormalization ? 'GET' : requestedMethod;
722
+ finalOptions.method = method;
723
+ if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') {
724
+ if (hasPayload) {
725
+ fetchUrl = appendPayloadToUrl(fetchUrl, payload);
726
+ }
636
727
  }
637
- const hasPayload = Object.keys(payload).length > 0;
638
- if (fetchUrl) {
639
- const finalOptions = { ...(fetchOptions || {}) };
640
- const headers = new Headers(finalOptions.headers || undefined);
641
- const method = (finalOptions.method || 'GET').toUpperCase();
642
- if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') {
643
- if (hasPayload) {
644
- const url = new URL(fetchUrl, window.location.href);
645
- const params = new URLSearchParams(url.search);
646
- for (const [k, v] of Object.entries(payload)) {
647
- if (v === undefined) {
648
- continue;
649
- }
650
- if (v === null) {
651
- params.append(k, '');
652
- }
653
- else if (Array.isArray(v)) {
654
- v.forEach(item => {
655
- params.append(k, String(item));
656
- });
657
- }
658
- else if (typeof v === 'object' || typeof v === 'function') {
659
- params.append(k, JSON.stringify(v));
660
- }
661
- else {
662
- params.append(k, String(v));
663
- }
728
+ else if (hasPayload) {
729
+ const contentType = headers.get('Content-Type') || '';
730
+ if (/multipart\/form-data/i.test(contentType)) {
731
+ headers.delete('Content-Type');
732
+ const formData = new FormData();
733
+ for (const [k, v] of Object.entries(payload)) {
734
+ if (v === undefined || v === null) {
735
+ formData.append(k, '');
664
736
  }
665
- url.search = params.toString();
666
- fetchUrl = url.toString();
667
- }
668
- }
669
- else if (hasPayload) {
670
- const contentType = headers.get('Content-Type') || '';
671
- if (/multipart\/form-data/i.test(contentType)) {
672
- headers.delete('Content-Type');
673
- const formData = new FormData();
674
- for (const [k, v] of Object.entries(payload)) {
675
- if (v === undefined || v === null) {
676
- formData.append(k, '');
677
- }
678
- else if (v instanceof Blob) {
679
- formData.append(k, v);
680
- }
681
- else if (Array.isArray(v)) {
682
- v.forEach(item => formData.append(k, String(item)));
683
- }
684
- else if (typeof v === 'object') {
685
- formData.append(k, JSON.stringify(v));
686
- }
687
- else {
688
- formData.append(k, String(v));
689
- }
737
+ else if (v instanceof Blob) {
738
+ formData.append(k, v);
690
739
  }
691
- finalOptions.body = formData;
692
- }
693
- else if (/application\/x-www-form-urlencoded/i.test(contentType)) {
694
- const params = new URLSearchParams();
695
- for (const [k, v] of Object.entries(payload)) {
696
- if (v === undefined) {
697
- continue;
698
- }
699
- if (v === null) {
700
- params.append(k, '');
701
- }
702
- else if (Array.isArray(v)) {
703
- v.forEach(item => params.append(k, String(item)));
704
- }
705
- else if (typeof v === 'object') {
706
- params.append(k, JSON.stringify(v));
707
- }
708
- else {
709
- params.append(k, String(v));
710
- }
740
+ else if (Array.isArray(v)) {
741
+ v.forEach(item => formData.append(k, String(item)));
742
+ }
743
+ else if (typeof v === 'object') {
744
+ formData.append(k, JSON.stringify(v));
745
+ }
746
+ else {
747
+ formData.append(k, String(v));
711
748
  }
712
- finalOptions.body = params;
713
- }
714
- else {
715
- headers.set('Content-Type', 'application/json');
716
- finalOptions.body = JSON.stringify(payload);
717
749
  }
750
+ finalOptions.body = formData;
718
751
  }
719
- finalOptions.headers = headers;
720
- // fetchstartイベントを発火
721
- if (this.options.targetFragment && fetchUrl) {
722
- const startedAt = performance.now();
723
- HaoriEvent.fetchStart(this.options.targetFragment.getTarget(), fetchUrl, finalOptions, hasPayload ? payload : undefined);
724
- return fetch(fetchUrl, finalOptions)
725
- .then(response => {
726
- return this.handleFetchResult(response, fetchUrl || undefined, startedAt);
727
- })
728
- .catch(error => {
729
- if (fetchUrl) {
730
- HaoriEvent.fetchError(this.options.targetFragment.getTarget(), fetchUrl, error);
752
+ else if (/application\/x-www-form-urlencoded/i.test(contentType)) {
753
+ const params = new URLSearchParams();
754
+ for (const [k, v] of Object.entries(payload)) {
755
+ if (v === undefined) {
756
+ continue;
731
757
  }
732
- throw error;
733
- });
734
- }
735
- else if (fetchUrl) {
736
- return fetch(fetchUrl, finalOptions).then(response => {
737
- return this.handleFetchResult(response, fetchUrl || undefined);
738
- });
758
+ if (v === null) {
759
+ params.append(k, '');
760
+ }
761
+ else if (Array.isArray(v)) {
762
+ v.forEach(item => params.append(k, String(item)));
763
+ }
764
+ else if (typeof v === 'object') {
765
+ params.append(k, JSON.stringify(v));
766
+ }
767
+ else {
768
+ params.append(k, String(v));
769
+ }
770
+ }
771
+ finalOptions.body = params;
739
772
  }
740
773
  else {
741
- return Promise.resolve();
774
+ headers.set('Content-Type', 'application/json');
775
+ finalOptions.body = JSON.stringify(payload);
742
776
  }
743
777
  }
744
- else {
745
- // fetchUrlが無い場合(changeイベント等)、bindFragmentsが無ければformFragmentにバインド
746
- if ((!this.options.bindFragments ||
747
- this.options.bindFragments.length === 0) &&
748
- this.options.formFragment &&
749
- hasPayload) {
750
- // 双方向バインディング: フォーム値を自動的にバインディングデータに反映
751
- const formFragment = this.options.formFragment;
752
- const formElement = formFragment.getTarget();
753
- formElement.setAttribute(`${Env.prefix}bind`, JSON.stringify(payload));
754
- const bindingData = formFragment.getBindingData();
755
- Object.assign(bindingData, payload);
756
- return Core.setBindingData(formElement, bindingData);
757
- }
758
- const merged = hasPayload ? payload : {};
759
- const response = new Response(JSON.stringify(merged), {
760
- headers: { 'Content-Type': 'application/json' },
778
+ finalOptions.headers = headers;
779
+ let queryString;
780
+ if (isDemoQueryNormalization) {
781
+ queryString = fetchUrl
782
+ ? new URL(fetchUrl, window.location.href).search || undefined
783
+ : undefined;
784
+ headers.delete('Content-Type');
785
+ Log.info('Haori demo fetch normalization', {
786
+ runtime: Env.runtime,
787
+ requestedMethod,
788
+ effectiveMethod: method,
789
+ transportMode: 'query-get',
790
+ url: fetchUrl,
791
+ payload: hasPayload ? payload : undefined,
792
+ queryString,
793
+ });
794
+ }
795
+ // fetchstartイベントを発火
796
+ if (this.options.targetFragment && fetchUrl) {
797
+ const startedAt = performance.now();
798
+ const fetchStartMetadata = {
799
+ runtime: Env.runtime,
800
+ requestedMethod,
801
+ effectiveMethod: method,
802
+ transportMode: isDemoQueryNormalization ? 'query-get' : 'http',
803
+ ...(isDemoQueryNormalization ? { queryString } : {}),
804
+ };
805
+ HaoriEvent.fetchStart(this.options.targetFragment.getTarget(), fetchUrl, finalOptions, hasPayload ? payload : undefined, fetchStartMetadata);
806
+ return fetch(fetchUrl, finalOptions)
807
+ .then(response => {
808
+ return this.handleFetchResult(response, fetchUrl || undefined, startedAt);
809
+ })
810
+ .catch(error => {
811
+ if (fetchUrl) {
812
+ HaoriEvent.fetchError(this.options.targetFragment.getTarget(), fetchUrl, error);
813
+ }
814
+ throw error;
761
815
  });
762
- return this.handleFetchResult(response);
763
816
  }
817
+ return fetch(fetchUrl, finalOptions).then(response => {
818
+ return this.handleFetchResult(response, fetchUrl || undefined);
819
+ });
820
+ }
821
+ // fetchUrlが無い場合(changeイベント等)、bindFragmentsが無ければformFragmentにバインド
822
+ if ((!this.options.bindFragments ||
823
+ this.options.bindFragments.length === 0) &&
824
+ this.options.formFragment &&
825
+ hasPayload) {
826
+ // 双方向バインディング: フォーム値を自動的にバインディングデータに反映
827
+ const formFragment = this.options.formFragment;
828
+ const formElement = formFragment.getTarget();
829
+ formElement.setAttribute(`${Env.prefix}bind`, JSON.stringify(payload));
830
+ const bindingData = formFragment.getBindingData();
831
+ Object.assign(bindingData, payload);
832
+ await Core.setBindingData(formElement, bindingData);
833
+ return true;
834
+ }
835
+ const merged = hasPayload ? payload : {};
836
+ const response = new Response(JSON.stringify(merged), {
837
+ headers: { 'Content-Type': 'application/json' },
764
838
  });
839
+ return this.handleFetchResult(response);
765
840
  }
766
841
  /**
767
842
  * フェッチ後の処理を実行します。
768
843
  */
769
- handleFetchResult(response, url, startedAt) {
844
+ async handleFetchResult(response, url, startedAt) {
770
845
  const activeHaori = resolveProcedureHaoriApi();
771
846
  // エラー応答時は以後の処理を停止し、メッセージを伝播
772
847
  if (!response.ok) {
773
848
  if (this.options.targetFragment && url) {
774
849
  HaoriEvent.fetchError(this.options.targetFragment.getTarget(), url, new Error(`${response.status} ${response.statusText}`), response.status, startedAt);
775
850
  }
776
- return this.handleFetchError(response);
851
+ await this.handleFetchError(response);
852
+ return false;
777
853
  }
778
854
  // fetchendイベントを発火
779
855
  if (this.options.targetFragment && url && startedAt) {
@@ -783,7 +859,7 @@ ${body}
783
859
  const result = this.options.afterCallback(response);
784
860
  if (result !== undefined && result !== null) {
785
861
  if (result === false || (typeof result === 'object' && result.stop)) {
786
- return Promise.resolve();
862
+ return false;
787
863
  }
788
864
  if (typeof result === 'object' && 'response' in result) {
789
865
  response = ('response' in result ? result.response : response);
@@ -842,30 +918,19 @@ ${body}
842
918
  });
843
919
  }
844
920
  // 仕様順序: 先に各種操作(bind/adjust/row/reset/refetch/click/open/close)を完了
845
- return Promise.all(promises)
846
- .then(() => {
847
- // その後にダイアログ/トーストを表示
848
- if (this.options.dialogMessage) {
849
- return activeHaori.dialog(this.options.dialogMessage);
850
- }
851
- return Promise.resolve();
852
- })
853
- .then(() => {
854
- if (this.options.toastMessage) {
855
- return activeHaori.toast(this.options.toastMessage, 'info');
856
- }
857
- return Promise.resolve();
858
- })
859
- .then(() => {
860
- this.pushHistory();
861
- return Promise.resolve();
862
- })
863
- .then(() => {
864
- if (this.options.redirectUrl) {
865
- window.location.href = this.options.redirectUrl;
866
- }
867
- return Promise.resolve();
868
- });
921
+ await Promise.all(promises);
922
+ // その後にダイアログ/トーストを表示
923
+ if (this.options.dialogMessage) {
924
+ await activeHaori.dialog(this.options.dialogMessage);
925
+ }
926
+ if (this.options.toastMessage) {
927
+ await activeHaori.toast(this.options.toastMessage, 'info');
928
+ }
929
+ this.pushHistory();
930
+ if (this.options.redirectUrl) {
931
+ window.location.href = this.options.redirectUrl;
932
+ }
933
+ return true;
869
934
  }
870
935
  /**
871
936
  * history.pushState を実行します。
@@ -989,7 +1054,7 @@ ${body}
989
1054
  if (entries.length === 0) {
990
1055
  // 汎用メッセージ
991
1056
  await addGeneralMessage(`${response.status} ${response.statusText}`);
992
- return;
1057
+ return false;
993
1058
  }
994
1059
  // メッセージを反映
995
1060
  for (const e of entries) {
@@ -1000,7 +1065,7 @@ ${body}
1000
1065
  await addGeneralMessage(e.message);
1001
1066
  }
1002
1067
  }
1003
- return;
1068
+ return false;
1004
1069
  }
1005
1070
  catch {
1006
1071
  // JSON 解析失敗時はテキストにフォールバック
@@ -1019,6 +1084,7 @@ ${body}
1019
1084
  catch {
1020
1085
  await addGeneralMessage(`${response.status} ${response.statusText}`);
1021
1086
  }
1087
+ return false;
1022
1088
  }
1023
1089
  /**
1024
1090
  * 対象のフラグメント以下の入力要素に対してバリデーションを実行します。
@@ -1106,7 +1172,21 @@ ${body}
1106
1172
  if (this.options.bindArg) {
1107
1173
  this.options.bindFragments.forEach(fragment => {
1108
1174
  const bindingData = fragment.getBindingData();
1109
- bindingData[this.options.bindArg] = data;
1175
+ const bindArg = this.options.bindArg;
1176
+ if (data &&
1177
+ typeof data === 'object' &&
1178
+ !Array.isArray(data)) {
1179
+ const currentValue = bindingData[bindArg];
1180
+ const currentObject = currentValue &&
1181
+ typeof currentValue === 'object' &&
1182
+ !Array.isArray(currentValue)
1183
+ ? currentValue
1184
+ : {};
1185
+ bindingData[bindArg] = this.mergeAppendBindingData(fragment, data, currentObject);
1186
+ }
1187
+ else {
1188
+ bindingData[bindArg] = data;
1189
+ }
1110
1190
  promises.push(Core.setBindingData(fragment.getTarget(), bindingData));
1111
1191
  });
1112
1192
  }
@@ -1116,12 +1196,32 @@ ${body}
1116
1196
  }
1117
1197
  else {
1118
1198
  this.options.bindFragments.forEach(fragment => {
1119
- promises.push(Core.setBindingData(fragment.getTarget(), data));
1199
+ const resolvedData = this.mergeAppendBindingData(fragment, data);
1200
+ promises.push(Core.setBindingData(fragment.getTarget(), resolvedData));
1120
1201
  });
1121
1202
  }
1122
1203
  return Promise.all(promises).then(() => undefined);
1123
1204
  });
1124
1205
  }
1206
+ /**
1207
+ * bind-append 指定があるキーについて、既存配列と結合したデータを返します。
1208
+ */
1209
+ mergeAppendBindingData(fragment, data, currentData = fragment.getBindingData()) {
1210
+ if (!this.options.bindAppendParams ||
1211
+ this.options.bindAppendParams.length === 0) {
1212
+ return data;
1213
+ }
1214
+ const merged = { ...data };
1215
+ const current = currentData;
1216
+ for (const key of this.options.bindAppendParams) {
1217
+ const incoming = merged[key];
1218
+ const existing = current[key];
1219
+ if (Array.isArray(existing) && Array.isArray(incoming)) {
1220
+ merged[key] = existing.concat(incoming);
1221
+ }
1222
+ }
1223
+ return merged;
1224
+ }
1125
1225
  /**
1126
1226
  * 値の増減を行います。
1127
1227
  */