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 +170 -9
- package/dist/viewlogic-router.js +109 -29
- package/dist/viewlogic-router.js.map +2 -2
- package/dist/viewlogic-router.min.js +3 -3
- package/dist/viewlogic-router.min.js.map +3 -3
- package/package.json +1 -1
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()`, `$
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
687
|
-
//
|
|
688
|
-
|
|
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
|
-
###
|
|
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
|
|
package/dist/viewlogic-router.js
CHANGED
|
@@ -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
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1592
|
+
requestAnimationFrame(() => {
|
|
1593
|
+
setTimeout(() => {
|
|
1594
|
+
component.navigateTo(redirectTo);
|
|
1595
|
+
}, 1e3);
|
|
1596
|
+
});
|
|
1547
1597
|
}
|
|
1548
1598
|
} catch (error) {
|
|
1549
|
-
|
|
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
|