viewlogic 1.2.0 → 1.2.2

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.
package/README.md CHANGED
@@ -338,10 +338,15 @@ const current = router.getCurrentRoute();
338
338
  // In route components - global methods automatically available:
339
339
  export default {
340
340
  dataURL: '/api/products', // Auto-fetch data
341
- mounted() {
341
+ async mounted() {
342
342
  const id = this.getParam('id'); // Get parameter
343
343
  this.navigateTo('detail', { id }); // Navigate
344
344
  console.log('Data loaded:', this.products); // From dataURL
345
+
346
+ // New $api pattern for RESTful API calls
347
+ const user = await this.$api.get('/api/users/{userId}');
348
+ await this.$api.post('/api/analytics', { pageView: 'products' });
349
+
345
350
  if (this.$isAuthenticated()) { /* auth check */ }
346
351
  const text = this.$t('welcome.message'); // i18n
347
352
  }
@@ -351,9 +356,9 @@ export default {
351
356
  ### Key Global Methods (Auto-available in all route components)
352
357
  - **Navigation**: `navigateTo()`, `getCurrentRoute()`
353
358
  - **Parameters**: `getParams()`, `getParam(key, defaultValue)`
354
- - **Data Fetching**: `$fetchData()`, `$fetchAllData()` (with dataURL)
359
+ - **Data Fetching**: `$fetchData()` (with dataURL), `$api.get()`, `$api.post()`, `$api.put()`, `$api.patch()`, `$api.delete()`
355
360
  - **Authentication**: `$isAuthenticated()`, `$getToken()`, `$logout()`
356
- - **Forms**: Auto-binding with `action` attribute and `{param}` templates
361
+ - **Forms**: Auto-binding with `action` attribute, duplicate prevention, validation
357
362
  - **i18n**: `$t(key, params)` for translations
358
363
 
359
364
  ### Auto-Injected Properties
@@ -659,9 +664,98 @@ export default {
659
664
  - ✅ **Built-in Security** - HTML sanitization included
660
665
  - ✅ **Zero Setup** - Works immediately without configuration
661
666
 
662
- ## 📝 Automatic Form Handling with Variable Parameters
667
+ ## 🔥 RESTful API Calls with $api Pattern
668
+
669
+ ViewLogic introduces a clean, RESTful API calling pattern with automatic parameter substitution and authentication handling.
670
+
671
+ ### Basic API Usage
672
+
673
+ ```javascript
674
+ // src/logic/user-profile.js
675
+ export default {
676
+ name: 'UserProfile',
677
+
678
+ async mounted() {
679
+ try {
680
+ // GET request with automatic parameter substitution
681
+ const user = await this.$api.get('/api/users/{userId}');
682
+
683
+ // POST request with data
684
+ const response = await this.$api.post('/api/users/{userId}/posts', {
685
+ title: 'New Post',
686
+ content: 'Post content here'
687
+ });
688
+
689
+ // PUT request for updates
690
+ await this.$api.put('/api/users/{userId}', {
691
+ name: user.name,
692
+ email: user.email
693
+ });
694
+
695
+ // DELETE request
696
+ await this.$api.delete('/api/posts/{postId}');
697
+
698
+ } catch (error) {
699
+ console.error('API call failed:', error);
700
+ this.handleError(error);
701
+ }
702
+ }
703
+ };
704
+ ```
705
+
706
+ ### Advanced API Features
707
+
708
+ ```javascript
709
+ export default {
710
+ methods: {
711
+ async handleUserActions() {
712
+ // With custom headers
713
+ const data = await this.$api.get('/api/protected-data', {
714
+ headers: { 'X-Custom-Header': 'value' }
715
+ });
716
+
717
+ // File upload with FormData
718
+ const formData = new FormData();
719
+ formData.append('file', this.selectedFile);
720
+ await this.$api.post('/api/upload', formData);
721
+
722
+ // With query parameters (automatically added from current route)
723
+ // URL: /users?id=123 → API call includes ?id=123
724
+ const result = await this.$api.get('/api/user-data');
725
+ },
726
+
727
+ // Error handling patterns
728
+ async safeApiCall() {
729
+ try {
730
+ const user = await this.$api.get('/api/users/{userId}');
731
+ this.user = user;
732
+
733
+ } catch (error) {
734
+ if (error.message.includes('404')) {
735
+ this.showError('User not found');
736
+ } else if (error.message.includes('401')) {
737
+ this.navigateTo('login');
738
+ } else {
739
+ this.showError('Something went wrong');
740
+ }
741
+ }
742
+ }
743
+ }
744
+ };
745
+ ```
746
+
747
+ ### Key $api Features
663
748
 
664
- ViewLogic Router includes revolutionary automatic form handling that eliminates the need for manual form submission logic. Just define your forms with `action` attributes and the router handles the rest!
749
+ - **🎯 Parameter Substitution**: `{userId}` automatically replaced with component data or route params
750
+ - **🔐 Auto Authentication**: Authorization headers automatically added when token is available
751
+ - **📄 Smart Data Handling**: JSON and FormData automatically detected and processed
752
+ - **🔗 Query Integration**: Current route query parameters automatically included
753
+ - **⚡ Error Standardization**: Consistent error format across all API calls
754
+ - **🚀 RESTful Pattern**: Clean `get()`, `post()`, `put()`, `patch()`, `delete()` methods
755
+
756
+ ## 📝 Advanced Form Handling with Smart Features
757
+
758
+ ViewLogic Router includes revolutionary automatic form handling with duplicate prevention, validation, and error handling. Just define your forms with `action` attributes and the router handles everything!
665
759
 
666
760
  ### Basic Form Handling
667
761
 
@@ -683,14 +777,81 @@ ViewLogic Router includes revolutionary automatic form handling that eliminates
683
777
  export default {
684
778
  name: 'ContactPage',
685
779
  mounted() {
686
- // Forms are automatically bound - no additional code needed!
687
- // Form submission will automatically POST to /api/contact
688
- console.log('Form handling is automatic!');
780
+ // Forms are automatically bound with smart features:
781
+ // Duplicate submission prevention
782
+ // Automatic validation
783
+ // ✅ Loading state management
784
+ // ✅ Error handling
785
+ console.log('Smart form handling is automatic!');
689
786
  }
690
787
  };
691
788
  ```
692
789
 
693
- ### Variable Parameter Forms - 🆕 Revolutionary!
790
+ ### Smart Form Features - 🆕 Enhanced!
791
+
792
+ ViewLogic FormHandler now includes advanced features for production-ready applications:
793
+
794
+ ```html
795
+ <!-- Smart form with all features -->
796
+ <form action="/api/users/{userId}/update" method="PUT"
797
+ class="auto-form"
798
+ data-success-handler="handleSuccess"
799
+ data-error-handler="handleError"
800
+ data-loading-handler="handleLoading"
801
+ data-redirect="/profile">
802
+
803
+ <input type="text" name="name" required
804
+ data-validation="validateName">
805
+ <input type="email" name="email" required>
806
+ <button type="submit">Update Profile</button>
807
+ </form>
808
+ ```
809
+
810
+ ```javascript
811
+ export default {
812
+ methods: {
813
+ // Custom validation
814
+ validateName(value) {
815
+ return value.length >= 2 && value.length <= 50;
816
+ },
817
+
818
+ // Success handler
819
+ handleSuccess(response, form) {
820
+ this.showToast('Profile updated successfully!', 'success');
821
+ // Automatic redirect to /profile happens after this
822
+ },
823
+
824
+ // Error handler with smart error detection
825
+ handleError(error, form) {
826
+ if (error.message.includes('validation')) {
827
+ this.showToast('Please check your input', 'warning');
828
+ } else {
829
+ this.showToast('Update failed. Please try again.', 'error');
830
+ }
831
+ },
832
+
833
+ // Loading state handler
834
+ handleLoading(isLoading, form) {
835
+ const button = form.querySelector('button[type="submit"]');
836
+ button.disabled = isLoading;
837
+ button.textContent = isLoading ? 'Updating...' : 'Update Profile';
838
+ }
839
+ }
840
+ };
841
+ ```
842
+
843
+ ### Key Form Features
844
+
845
+ - **🚫 Duplicate Prevention**: Automatic duplicate submission blocking
846
+ - **⏱️ Timeout Management**: 30-second default timeout with abort capability
847
+ - **✅ Built-in Validation**: HTML5 + custom validation functions
848
+ - **🔄 Loading States**: Automatic loading state management
849
+ - **🎯 Smart Error Handling**: Network vs validation error distinction
850
+ - **📄 File Upload Support**: Automatic FormData vs JSON detection
851
+ - **🔀 Auto Redirect**: Post-success navigation
852
+ - **🏷️ Parameter Substitution**: Dynamic URL parameter replacement
853
+
854
+ ### Variable Parameter Forms - Revolutionary!
694
855
 
695
856
  The most powerful feature is **variable parameter support** in action URLs. You can use simple template syntax to inject dynamic values:
696
857
 
@@ -1488,6 +1488,7 @@ var FormHandler = class {
1488
1488
  this.router = router;
1489
1489
  this.config = {
1490
1490
  debug: options.debug || false,
1491
+ requestTimeout: options.requestTimeout || 3e4,
1491
1492
  ...options
1492
1493
  };
1493
1494
  this.log("debug", "FormHandler initialized");
@@ -1500,6 +1501,48 @@ var FormHandler = class {
1500
1501
  this.router.errorHandler.log(level, "FormHandler", ...args);
1501
1502
  }
1502
1503
  }
1504
+ /**
1505
+ * 중복 요청 체크
1506
+ */
1507
+ isDuplicateRequest(form) {
1508
+ if (form._isSubmitting) {
1509
+ this.log("debug", "Duplicate request blocked");
1510
+ return true;
1511
+ }
1512
+ return false;
1513
+ }
1514
+ /**
1515
+ * 폼 제출 시작
1516
+ */
1517
+ startFormSubmission(form) {
1518
+ form._isSubmitting = true;
1519
+ form._abortController = new AbortController();
1520
+ form._timeoutId = setTimeout(() => {
1521
+ if (form._isSubmitting) {
1522
+ this.abortFormSubmission(form);
1523
+ }
1524
+ }, this.config.requestTimeout);
1525
+ }
1526
+ /**
1527
+ * 폼 제출 완료
1528
+ */
1529
+ finishFormSubmission(form) {
1530
+ form._isSubmitting = false;
1531
+ if (form._timeoutId) {
1532
+ clearTimeout(form._timeoutId);
1533
+ delete form._timeoutId;
1534
+ }
1535
+ delete form._abortController;
1536
+ }
1537
+ /**
1538
+ * 폼 제출 중단
1539
+ */
1540
+ abortFormSubmission(form) {
1541
+ if (form._abortController) {
1542
+ form._abortController.abort();
1543
+ }
1544
+ this.finishFormSubmission(form);
1545
+ }
1503
1546
  /**
1504
1547
  * 자동 폼 바인딩
1505
1548
  */
@@ -1525,28 +1568,40 @@ var FormHandler = class {
1525
1568
  const errorHandler = form.getAttribute("data-error-handler");
1526
1569
  const loadingHandler = form.getAttribute("data-loading-handler");
1527
1570
  const redirectTo = form.getAttribute("data-redirect");
1571
+ action = this.processActionParams(action, component);
1572
+ if (!this.validateForm(form, component)) {
1573
+ return;
1574
+ }
1575
+ if (this.isDuplicateRequest(form)) {
1576
+ return;
1577
+ }
1578
+ this.startFormSubmission(form);
1579
+ const formData = new FormData(form);
1580
+ const data = Object.fromEntries(formData.entries());
1528
1581
  try {
1529
1582
  if (loadingHandler && component[loadingHandler]) {
1530
1583
  component[loadingHandler](true, form);
1531
1584
  }
1532
- action = this.processActionParams(action, component);
1533
- if (!this.validateForm(form, component)) {
1534
- return;
1535
- }
1536
- const formData = new FormData(form);
1537
- const data = Object.fromEntries(formData.entries());
1538
1585
  this.log("debug", `Form submitting to: ${action}`, data);
1539
- const response = await this.submitFormData(action, method, data, form, component);
1586
+ const response = await this.submitFormData(action, method, data, form, component, form._abortController.signal);
1540
1587
  if (successHandler && component[successHandler]) {
1541
1588
  component[successHandler](response, form);
1542
1589
  }
1590
+ this.finishFormSubmission(form);
1543
1591
  if (redirectTo) {
1544
- setTimeout(() => {
1545
- component.navigateTo(redirectTo);
1546
- }, 1e3);
1592
+ requestAnimationFrame(() => {
1593
+ setTimeout(() => {
1594
+ component.navigateTo(redirectTo);
1595
+ }, 1e3);
1596
+ });
1547
1597
  }
1548
1598
  } catch (error) {
1549
- this.log("warn", `Form submission error:`, error);
1599
+ if (error.name === "AbortError") {
1600
+ this.log("debug", "Form submission aborted");
1601
+ return;
1602
+ }
1603
+ this.log("warn", "Form submission error:", error);
1604
+ this.finishFormSubmission(form);
1550
1605
  if (errorHandler && component[errorHandler]) {
1551
1606
  component[errorHandler](error, form);
1552
1607
  } else {
@@ -1567,11 +1622,13 @@ var FormHandler = class {
1567
1622
  /**
1568
1623
  * 폼 데이터 서브밋 (ApiHandler 활용)
1569
1624
  */
1570
- async submitFormData(action, method, data, form, component) {
1625
+ async submitFormData(action, method, data, form, component, signal = null) {
1571
1626
  const hasFile = Array.from(form.elements).some((el) => el.type === "file" && el.files.length > 0);
1572
1627
  const options = {
1573
1628
  method: method.toUpperCase(),
1574
- headers: {}
1629
+ headers: {},
1630
+ signal
1631
+ // AbortController 신호 추가
1575
1632
  };
1576
1633
  if (hasFile) {
1577
1634
  options.data = new FormData(form);
@@ -1624,20 +1681,44 @@ var FormHandler = class {
1624
1681
  this.log("warn", `Validation function '${validationFunction}' not found`);
1625
1682
  return true;
1626
1683
  }
1684
+ /**
1685
+ * 모든 폼 요청 취소
1686
+ */
1687
+ cancelAllRequests() {
1688
+ const forms = document.querySelectorAll("form");
1689
+ forms.forEach((form) => {
1690
+ if (form._isSubmitting) {
1691
+ this.abortFormSubmission(form);
1692
+ }
1693
+ });
1694
+ }
1627
1695
  /**
1628
1696
  * 정리 (메모리 누수 방지)
1629
1697
  */
1630
1698
  destroy() {
1699
+ this.cancelAllRequests();
1631
1700
  const forms = document.querySelectorAll("form.auto-form, form[action]");
1632
1701
  forms.forEach((form) => {
1633
1702
  if (form._boundSubmitHandler) {
1634
1703
  form.removeEventListener("submit", form._boundSubmitHandler);
1635
1704
  delete form._boundSubmitHandler;
1636
1705
  }
1706
+ this.cleanupFormState(form);
1637
1707
  });
1638
1708
  this.log("debug", "FormHandler destroyed");
1639
1709
  this.router = null;
1640
1710
  }
1711
+ /**
1712
+ * 폼 상태 정리
1713
+ */
1714
+ cleanupFormState(form) {
1715
+ delete form._isSubmitting;
1716
+ delete form._abortController;
1717
+ if (form._timeoutId) {
1718
+ clearTimeout(form._timeoutId);
1719
+ delete form._timeoutId;
1720
+ }
1721
+ }
1641
1722
  };
1642
1723
 
1643
1724
  // src/core/ApiHandler.js
@@ -1804,6 +1885,20 @@ var ApiHandler = class {
1804
1885
  async delete(url, component = null, options = {}) {
1805
1886
  return this.fetchData(url, component, { ...options, method: "DELETE" });
1806
1887
  }
1888
+ /**
1889
+ * 컴포넌트에 바인딩된 API 객체 생성
1890
+ */
1891
+ bindToComponent(component) {
1892
+ return {
1893
+ get: (url, options = {}) => this.get(url, component, options),
1894
+ post: (url, data, options = {}) => this.post(url, data, component, options),
1895
+ put: (url, data, options = {}) => this.put(url, data, component, options),
1896
+ patch: (url, data, options = {}) => this.patch(url, data, component, options),
1897
+ delete: (url, options = {}) => this.delete(url, component, options),
1898
+ fetchData: (url, options = {}) => this.fetchData(url, component, options),
1899
+ fetchMultipleData: (dataConfig) => this.fetchMultipleData(dataConfig, component)
1900
+ };
1901
+ }
1807
1902
  /**
1808
1903
  * 정리 (메모리 누수 방지)
1809
1904
  */
@@ -2278,6 +2373,7 @@ ${template}`;
2278
2373
  }
2279
2374
  },
2280
2375
  async mounted() {
2376
+ this.$api = router.routeLoader.apiHandler.bindToComponent(this);
2281
2377
  if (script.mounted) {
2282
2378
  await script.mounted.call(this);
2283
2379
  }
@@ -2344,22 +2440,6 @@ ${template}`;
2344
2440
  } finally {
2345
2441
  this.$dataLoading = false;
2346
2442
  }
2347
- },
2348
- // HTTP 메서드 래퍼들 (ApiHandler 직접 접근)
2349
- async $get(url, options = {}) {
2350
- return await router.routeLoader.apiHandler.get(url, this, options);
2351
- },
2352
- async $post(url, data, options = {}) {
2353
- return await router.routeLoader.apiHandler.post(url, data, this, options);
2354
- },
2355
- async $put(url, data, options = {}) {
2356
- return await router.routeLoader.apiHandler.put(url, data, this, options);
2357
- },
2358
- async $patch(url, data, options = {}) {
2359
- return await router.routeLoader.apiHandler.patch(url, data, this, options);
2360
- },
2361
- async $delete(url, options = {}) {
2362
- return await router.routeLoader.apiHandler.delete(url, this, options);
2363
2443
  }
2364
2444
  },
2365
2445
  _routeName: routeName