mvc-kit 2.12.5 → 2.13.1
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/BEST_PRACTICES.md +1390 -0
- package/agent-config/bin/postinstall.mjs +4 -3
- package/agent-config/bin/setup.mjs +5 -1
- package/agent-config/claude-code/agents/mvc-kit-architect.md +16 -8
- package/agent-config/claude-code/skills/guide/SKILL.md +29 -7
- package/agent-config/claude-code/skills/guide/patterns.md +12 -0
- package/agent-config/claude-code/skills/guide/recipes.md +510 -0
- package/agent-config/claude-code/skills/guide/testing.md +297 -0
- package/agent-config/claude-code/skills/review/SKILL.md +3 -13
- package/agent-config/claude-code/skills/review/checklist.md +30 -5
- package/agent-config/claude-code/skills/scaffold/SKILL.md +4 -13
- package/agent-config/lib/install-claude.mjs +90 -25
- package/dist/Channel.cjs +276 -300
- package/dist/Channel.cjs.map +1 -1
- package/dist/Channel.js +275 -299
- package/dist/Channel.js.map +1 -1
- package/dist/Collection.cjs +424 -504
- package/dist/Collection.cjs.map +1 -1
- package/dist/Collection.js +423 -503
- package/dist/Collection.js.map +1 -1
- package/dist/Controller.cjs +70 -67
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.js +69 -66
- package/dist/Controller.js.map +1 -1
- package/dist/EventBus.cjs +77 -88
- package/dist/EventBus.cjs.map +1 -1
- package/dist/EventBus.js +76 -87
- package/dist/EventBus.js.map +1 -1
- package/dist/Feed.cjs +81 -77
- package/dist/Feed.cjs.map +1 -1
- package/dist/Feed.js +80 -76
- package/dist/Feed.js.map +1 -1
- package/dist/Model.cjs +181 -207
- package/dist/Model.cjs.map +1 -1
- package/dist/Model.js +179 -205
- package/dist/Model.js.map +1 -1
- package/dist/Pagination.cjs +75 -73
- package/dist/Pagination.cjs.map +1 -1
- package/dist/Pagination.js +74 -72
- package/dist/Pagination.js.map +1 -1
- package/dist/Pending.cjs +255 -287
- package/dist/Pending.cjs.map +1 -1
- package/dist/Pending.js +253 -285
- package/dist/Pending.js.map +1 -1
- package/dist/PersistentCollection.cjs +242 -285
- package/dist/PersistentCollection.cjs.map +1 -1
- package/dist/PersistentCollection.js +241 -284
- package/dist/PersistentCollection.js.map +1 -1
- package/dist/Resource.cjs +166 -174
- package/dist/Resource.cjs.map +1 -1
- package/dist/Resource.js +164 -172
- package/dist/Resource.js.map +1 -1
- package/dist/Selection.cjs +84 -94
- package/dist/Selection.cjs.map +1 -1
- package/dist/Selection.js +83 -93
- package/dist/Selection.js.map +1 -1
- package/dist/Service.cjs +54 -55
- package/dist/Service.cjs.map +1 -1
- package/dist/Service.js +53 -54
- package/dist/Service.js.map +1 -1
- package/dist/Sorting.cjs +102 -101
- package/dist/Sorting.cjs.map +1 -1
- package/dist/Sorting.js +102 -101
- package/dist/Sorting.js.map +1 -1
- package/dist/Trackable.cjs +112 -80
- package/dist/Trackable.cjs.map +1 -1
- package/dist/Trackable.js +111 -79
- package/dist/Trackable.js.map +1 -1
- package/dist/ViewModel.cjs +528 -576
- package/dist/ViewModel.cjs.map +1 -1
- package/dist/ViewModel.js +525 -573
- package/dist/ViewModel.js.map +1 -1
- package/dist/bindPublicMethods.cjs +43 -24
- package/dist/bindPublicMethods.cjs.map +1 -1
- package/dist/bindPublicMethods.js +43 -24
- package/dist/bindPublicMethods.js.map +1 -1
- package/dist/errors.cjs +67 -68
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +68 -71
- package/dist/errors.js.map +1 -1
- package/dist/mvc-kit.cjs +44 -46
- package/dist/mvc-kit.js +5 -32
- package/dist/produceDraft.cjs +105 -95
- package/dist/produceDraft.cjs.map +1 -1
- package/dist/produceDraft.js +106 -97
- package/dist/produceDraft.js.map +1 -1
- package/dist/react/components/CardList.cjs +30 -40
- package/dist/react/components/CardList.cjs.map +1 -1
- package/dist/react/components/CardList.js +31 -41
- package/dist/react/components/CardList.js.map +1 -1
- package/dist/react/components/DataTable.cjs +146 -169
- package/dist/react/components/DataTable.cjs.map +1 -1
- package/dist/react/components/DataTable.js +147 -170
- package/dist/react/components/DataTable.js.map +1 -1
- package/dist/react/components/InfiniteScroll.cjs +51 -42
- package/dist/react/components/InfiniteScroll.cjs.map +1 -1
- package/dist/react/components/InfiniteScroll.js +52 -43
- package/dist/react/components/InfiniteScroll.js.map +1 -1
- package/dist/react/components/types.cjs +10 -6
- package/dist/react/components/types.cjs.map +1 -1
- package/dist/react/components/types.js +11 -9
- package/dist/react/components/types.js.map +1 -1
- package/dist/react/guards.cjs +10 -6
- package/dist/react/guards.cjs.map +1 -1
- package/dist/react/guards.js +11 -9
- package/dist/react/guards.js.map +1 -1
- package/dist/react/provider.cjs +23 -20
- package/dist/react/provider.cjs.map +1 -1
- package/dist/react/provider.js +23 -21
- package/dist/react/provider.js.map +1 -1
- package/dist/react/use-event-bus.cjs +24 -20
- package/dist/react/use-event-bus.cjs.map +1 -1
- package/dist/react/use-event-bus.js +24 -21
- package/dist/react/use-event-bus.js.map +1 -1
- package/dist/react/use-instance.cjs +43 -36
- package/dist/react/use-instance.cjs.map +1 -1
- package/dist/react/use-instance.js +43 -36
- package/dist/react/use-instance.js.map +1 -1
- package/dist/react/use-local.cjs +48 -64
- package/dist/react/use-local.cjs.map +1 -1
- package/dist/react/use-local.js +47 -63
- package/dist/react/use-local.js.map +1 -1
- package/dist/react/use-model.cjs +84 -98
- package/dist/react/use-model.cjs.map +1 -1
- package/dist/react/use-model.js +84 -100
- package/dist/react/use-model.js.map +1 -1
- package/dist/react/use-singleton.cjs +19 -23
- package/dist/react/use-singleton.cjs.map +1 -1
- package/dist/react/use-singleton.js +16 -20
- package/dist/react/use-singleton.js.map +1 -1
- package/dist/react/use-subscribe-only.cjs +28 -22
- package/dist/react/use-subscribe-only.cjs.map +1 -1
- package/dist/react/use-subscribe-only.js +28 -22
- package/dist/react/use-subscribe-only.js.map +1 -1
- package/dist/react/use-teardown.cjs +20 -19
- package/dist/react/use-teardown.cjs.map +1 -1
- package/dist/react/use-teardown.js +20 -19
- package/dist/react/use-teardown.js.map +1 -1
- package/dist/react-native/NativeCollection.cjs +98 -78
- package/dist/react-native/NativeCollection.cjs.map +1 -1
- package/dist/react-native/NativeCollection.js +97 -77
- package/dist/react-native/NativeCollection.js.map +1 -1
- package/dist/react-native.cjs +2 -4
- package/dist/react-native.js +1 -4
- package/dist/react.cjs +24 -26
- package/dist/react.js +1 -17
- package/dist/singleton.cjs +28 -22
- package/dist/singleton.cjs.map +1 -1
- package/dist/singleton.js +29 -26
- package/dist/singleton.js.map +1 -1
- package/dist/walkPrototypeChain.cjs +20 -12
- package/dist/walkPrototypeChain.cjs.map +1 -1
- package/dist/walkPrototypeChain.js +21 -13
- package/dist/walkPrototypeChain.js.map +1 -1
- package/dist/web/IndexedDBCollection.cjs +53 -36
- package/dist/web/IndexedDBCollection.cjs.map +1 -1
- package/dist/web/IndexedDBCollection.js +52 -35
- package/dist/web/IndexedDBCollection.js.map +1 -1
- package/dist/web/WebStorageCollection.cjs +82 -84
- package/dist/web/WebStorageCollection.cjs.map +1 -1
- package/dist/web/WebStorageCollection.js +81 -83
- package/dist/web/WebStorageCollection.js.map +1 -1
- package/dist/web/idb.cjs +107 -99
- package/dist/web/idb.cjs.map +1 -1
- package/dist/web/idb.js +108 -105
- package/dist/web/idb.js.map +1 -1
- package/dist/web.cjs +4 -6
- package/dist/web.js +1 -5
- package/dist/wrapAsyncMethods.cjs +141 -168
- package/dist/wrapAsyncMethods.cjs.map +1 -1
- package/dist/wrapAsyncMethods.js +141 -168
- package/dist/wrapAsyncMethods.js.map +1 -1
- package/examples/primitive/channel.ts +109 -0
- package/examples/primitive/collection.ts +118 -0
- package/examples/primitive/controller.ts +118 -0
- package/examples/primitive/counter.ts +108 -0
- package/examples/primitive/env.d.ts +1 -0
- package/examples/primitive/eventbus.ts +77 -0
- package/examples/primitive/feed.ts +162 -0
- package/examples/primitive/model.ts +82 -0
- package/examples/primitive/pagination.ts +91 -0
- package/examples/primitive/pending.ts +189 -0
- package/examples/primitive/persistent-collection.ts +116 -0
- package/examples/primitive/resource.ts +114 -0
- package/examples/primitive/selection.ts +96 -0
- package/examples/primitive/sorting.ts +112 -0
- package/examples/primitive/timer.ts +58 -0
- package/examples/primitive/trackable.ts +225 -0
- package/examples/primitive/tsconfig.json +20 -0
- package/examples/primitive/viewmodel-service.ts +161 -0
- package/examples/react/AuthExample/index.html +12 -0
- package/examples/react/AuthExample/src/App.tsx +29 -0
- package/examples/react/AuthExample/src/components/AdminPage.tsx +51 -0
- package/examples/react/AuthExample/src/components/AppHeader.tsx +32 -0
- package/examples/react/AuthExample/src/components/AuthGuard.tsx +50 -0
- package/examples/react/AuthExample/src/components/AuthScreen.tsx +181 -0
- package/examples/react/AuthExample/src/components/DashboardPage.tsx +41 -0
- package/examples/react/AuthExample/src/components/ProfilePage.tsx +44 -0
- package/examples/react/AuthExample/src/components/Toast.tsx +41 -0
- package/examples/react/AuthExample/src/env.d.ts +10 -0
- package/examples/react/AuthExample/src/events/AppEventBus.ts +7 -0
- package/examples/react/AuthExample/src/main.tsx +10 -0
- package/examples/react/AuthExample/src/mock/api.ts +78 -0
- package/examples/react/AuthExample/src/models/LoginFormModel.ts +19 -0
- package/examples/react/AuthExample/src/models/RegisterFormModel.ts +25 -0
- package/examples/react/AuthExample/src/services/AuthService.ts +21 -0
- package/examples/react/AuthExample/src/styles.css +445 -0
- package/examples/react/AuthExample/src/types/auth.ts +12 -0
- package/examples/react/AuthExample/src/viewmodels/AuthViewModel.ts +111 -0
- package/examples/react/AuthExample/tsconfig.json +22 -0
- package/examples/react/AuthExample/vite.config.ts +18 -0
- package/examples/react/ComplexApp/index.html +12 -0
- package/examples/react/ComplexApp/src/App.tsx +17 -0
- package/examples/react/ComplexApp/src/channels/ActivityChannel.ts +24 -0
- package/examples/react/ComplexApp/src/channels/DashboardChannel.ts +26 -0
- package/examples/react/ComplexApp/src/channels/ErrorsChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/LatencyChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/OrdersChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/RevenueChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/TrafficChannel.ts +5 -0
- package/examples/react/ComplexApp/src/channels/UsersMetricChannel.ts +5 -0
- package/examples/react/ComplexApp/src/collections/DashboardCollection.ts +6 -0
- package/examples/react/ComplexApp/src/collections/ErrorsCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/LatencyCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/OrdersCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/RevenueCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/TrafficCollection.ts +3 -0
- package/examples/react/ComplexApp/src/collections/UsersMetricCollection.ts +3 -0
- package/examples/react/ComplexApp/src/components/activity/ActivityFeed.tsx +31 -0
- package/examples/react/ComplexApp/src/components/activity/ActivityItemRow.tsx +35 -0
- package/examples/react/ComplexApp/src/components/dashboard/DashboardCard.tsx +37 -0
- package/examples/react/ComplexApp/src/components/dashboard/DashboardPage.tsx +34 -0
- package/examples/react/ComplexApp/src/components/layout/Navbar.tsx +32 -0
- package/examples/react/ComplexApp/src/components/layout/SocialFeedPanel.tsx +57 -0
- package/examples/react/ComplexApp/src/components/shared/Spinner.tsx +3 -0
- package/examples/react/ComplexApp/src/components/shared/StatusIndicator.tsx +13 -0
- package/examples/react/ComplexApp/src/components/shared/Toast.tsx +40 -0
- package/examples/react/ComplexApp/src/env.d.ts +10 -0
- package/examples/react/ComplexApp/src/events/AppEventBus.ts +7 -0
- package/examples/react/ComplexApp/src/main.tsx +10 -0
- package/examples/react/ComplexApp/src/mock-remote/MockWebSocket.ts +38 -0
- package/examples/react/ComplexApp/src/mock-remote/activity-api.ts +48 -0
- package/examples/react/ComplexApp/src/mock-remote/dashboard-generators.ts +45 -0
- package/examples/react/ComplexApp/src/mock-remote/delay.ts +18 -0
- package/examples/react/ComplexApp/src/mock-remote/social-api.ts +55 -0
- package/examples/react/ComplexApp/src/resources/ActivityResource.ts +12 -0
- package/examples/react/ComplexApp/src/resources/SocialFeedResource.ts +17 -0
- package/examples/react/ComplexApp/src/styles.css +463 -0
- package/examples/react/ComplexApp/src/types/activity.ts +8 -0
- package/examples/react/ComplexApp/src/types/dashboard.ts +5 -0
- package/examples/react/ComplexApp/src/types/social.ts +8 -0
- package/examples/react/ComplexApp/src/types/users.ts +6 -0
- package/examples/react/ComplexApp/src/viewmodels/ActivityFeedViewModel.ts +68 -0
- package/examples/react/ComplexApp/src/viewmodels/AppStateViewModel.ts +26 -0
- package/examples/react/ComplexApp/src/viewmodels/DashboardCardViewModel.ts +69 -0
- package/examples/react/ComplexApp/src/viewmodels/ErrorsCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/LatencyCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/OrdersCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/RevenueCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/SocialFeedViewModel.ts +39 -0
- package/examples/react/ComplexApp/src/viewmodels/TrafficCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/src/viewmodels/UsersMetricCardViewModel.ts +9 -0
- package/examples/react/ComplexApp/tsconfig.json +22 -0
- package/examples/react/ComplexApp/vite.config.ts +18 -0
- package/examples/react/FullApp/index.html +12 -0
- package/examples/react/FullApp/src/App.tsx +28 -0
- package/examples/react/FullApp/src/collections/ConversationsCollection.ts +4 -0
- package/examples/react/FullApp/src/collections/LocationsCollection.ts +4 -0
- package/examples/react/FullApp/src/components/auth/LoginPage.tsx +80 -0
- package/examples/react/FullApp/src/components/dashboard/DashboardPage.tsx +29 -0
- package/examples/react/FullApp/src/components/dashboard/RecentActivityCard.tsx +35 -0
- package/examples/react/FullApp/src/components/dashboard/StatsCard.tsx +19 -0
- package/examples/react/FullApp/src/components/layout/AppShell.tsx +31 -0
- package/examples/react/FullApp/src/components/layout/Header.tsx +25 -0
- package/examples/react/FullApp/src/components/layout/Sidebar.tsx +29 -0
- package/examples/react/FullApp/src/components/locations/LocationFilters.tsx +60 -0
- package/examples/react/FullApp/src/components/locations/LocationForm.tsx +112 -0
- package/examples/react/FullApp/src/components/locations/LocationProfilePage.tsx +81 -0
- package/examples/react/FullApp/src/components/locations/LocationsPage.tsx +127 -0
- package/examples/react/FullApp/src/components/messaging/ConversationList.tsx +59 -0
- package/examples/react/FullApp/src/components/messaging/MessageBubble.tsx +22 -0
- package/examples/react/FullApp/src/components/messaging/MessageThread.tsx +100 -0
- package/examples/react/FullApp/src/components/messaging/MessagingPage.tsx +52 -0
- package/examples/react/FullApp/src/components/shared/ErrorBanner.tsx +3 -0
- package/examples/react/FullApp/src/components/shared/Spinner.tsx +7 -0
- package/examples/react/FullApp/src/components/shared/Toast.tsx +41 -0
- package/examples/react/FullApp/src/components/users/UserFilters.tsx +59 -0
- package/examples/react/FullApp/src/components/users/UsersPage.tsx +80 -0
- package/examples/react/FullApp/src/components/users/UsersTable.tsx +52 -0
- package/examples/react/FullApp/src/env.d.ts +10 -0
- package/examples/react/FullApp/src/events/AppEventBus.ts +7 -0
- package/examples/react/FullApp/src/main.tsx +10 -0
- package/examples/react/FullApp/src/mock/delay.ts +21 -0
- package/examples/react/FullApp/src/mock/locations.ts +76 -0
- package/examples/react/FullApp/src/mock/messages.ts +237 -0
- package/examples/react/FullApp/src/mock/users.ts +84 -0
- package/examples/react/FullApp/src/models/LocationFormModel.ts +31 -0
- package/examples/react/FullApp/src/models/LoginFormModel.ts +19 -0
- package/examples/react/FullApp/src/resources/UsersResource.ts +12 -0
- package/examples/react/FullApp/src/services/AuthService.ts +18 -0
- package/examples/react/FullApp/src/services/LocationService.ts +23 -0
- package/examples/react/FullApp/src/services/MessageService.ts +65 -0
- package/examples/react/FullApp/src/services/UserService.ts +23 -0
- package/examples/react/FullApp/src/styles.css +767 -0
- package/examples/react/FullApp/src/types/conversation.ts +7 -0
- package/examples/react/FullApp/src/types/location.ts +12 -0
- package/examples/react/FullApp/src/types/message.ts +7 -0
- package/examples/react/FullApp/src/types/user.ts +10 -0
- package/examples/react/FullApp/src/viewmodels/AuthViewModel.ts +51 -0
- package/examples/react/FullApp/src/viewmodels/ConversationsViewModel.ts +89 -0
- package/examples/react/FullApp/src/viewmodels/DashboardViewModel.ts +56 -0
- package/examples/react/FullApp/src/viewmodels/LocationProfileViewModel.ts +81 -0
- package/examples/react/FullApp/src/viewmodels/LocationsViewModel.ts +113 -0
- package/examples/react/FullApp/src/viewmodels/MessageThreadViewModel.ts +83 -0
- package/examples/react/FullApp/src/viewmodels/UsersViewModel.ts +88 -0
- package/examples/react/FullApp/tsconfig.json +22 -0
- package/examples/react/FullApp/vite.config.ts +18 -0
- package/examples/react/WorkerApp/index.html +12 -0
- package/examples/react/WorkerApp/src/App.tsx +24 -0
- package/examples/react/WorkerApp/src/channels/MessagingChannel.ts +46 -0
- package/examples/react/WorkerApp/src/channels/WorkerStatusChannel.ts +35 -0
- package/examples/react/WorkerApp/src/components/auth/LoginPage.tsx +60 -0
- package/examples/react/WorkerApp/src/components/layout/AppShell.tsx +31 -0
- package/examples/react/WorkerApp/src/components/layout/Header.tsx +23 -0
- package/examples/react/WorkerApp/src/components/layout/Sidebar.tsx +28 -0
- package/examples/react/WorkerApp/src/components/messaging/ComposeBar.tsx +33 -0
- package/examples/react/WorkerApp/src/components/messaging/ConversationList.tsx +59 -0
- package/examples/react/WorkerApp/src/components/messaging/MessageBubble.tsx +45 -0
- package/examples/react/WorkerApp/src/components/messaging/MessageThread.tsx +93 -0
- package/examples/react/WorkerApp/src/components/messaging/MessagingPage.tsx +53 -0
- package/examples/react/WorkerApp/src/components/shared/ErrorBanner.tsx +3 -0
- package/examples/react/WorkerApp/src/components/shared/PendingBanner.tsx +37 -0
- package/examples/react/WorkerApp/src/components/shared/Spinner.tsx +7 -0
- package/examples/react/WorkerApp/src/components/shared/Toast.tsx +41 -0
- package/examples/react/WorkerApp/src/components/shift/ShiftPage.tsx +98 -0
- package/examples/react/WorkerApp/src/components/shift/ShiftTimer.tsx +24 -0
- package/examples/react/WorkerApp/src/components/shift/SiteSelector.tsx +27 -0
- package/examples/react/WorkerApp/src/components/sites/SiteFilters.tsx +61 -0
- package/examples/react/WorkerApp/src/components/sites/SitesPage.tsx +102 -0
- package/examples/react/WorkerApp/src/env.d.ts +10 -0
- package/examples/react/WorkerApp/src/events/AppEventBus.ts +7 -0
- package/examples/react/WorkerApp/src/main.tsx +10 -0
- package/examples/react/WorkerApp/src/mock/MockWebSocket.ts +38 -0
- package/examples/react/WorkerApp/src/mock/delay.ts +31 -0
- package/examples/react/WorkerApp/src/mock/messages.ts +120 -0
- package/examples/react/WorkerApp/src/mock/shifts.ts +57 -0
- package/examples/react/WorkerApp/src/mock/sites.ts +14 -0
- package/examples/react/WorkerApp/src/mock/workers.ts +12 -0
- package/examples/react/WorkerApp/src/models/ComposeMessageModel.ts +17 -0
- package/examples/react/WorkerApp/src/resources/ConversationsResource.ts +10 -0
- package/examples/react/WorkerApp/src/resources/MessagesResource.ts +32 -0
- package/examples/react/WorkerApp/src/resources/ShiftResource.ts +73 -0
- package/examples/react/WorkerApp/src/resources/SitesResource.ts +11 -0
- package/examples/react/WorkerApp/src/resources/WorkersResource.ts +11 -0
- package/examples/react/WorkerApp/src/styles.css +756 -0
- package/examples/react/WorkerApp/src/types/conversation.ts +7 -0
- package/examples/react/WorkerApp/src/types/message.ts +7 -0
- package/examples/react/WorkerApp/src/types/shift.ts +13 -0
- package/examples/react/WorkerApp/src/types/site.ts +8 -0
- package/examples/react/WorkerApp/src/types/worker.ts +8 -0
- package/examples/react/WorkerApp/src/viewmodels/AuthViewModel.ts +41 -0
- package/examples/react/WorkerApp/src/viewmodels/ConversationsViewModel.ts +83 -0
- package/examples/react/WorkerApp/src/viewmodels/MessageThreadViewModel.ts +113 -0
- package/examples/react/WorkerApp/src/viewmodels/ShiftViewModel.ts +147 -0
- package/examples/react/WorkerApp/src/viewmodels/SitesViewModel.ts +82 -0
- package/examples/react/WorkerApp/tsconfig.json +22 -0
- package/examples/react/WorkerApp/vite.config.ts +18 -0
- package/package.json +11 -9
- package/src/Pending.test.ts +1 -2
- package/src/Sorting.test.ts +1 -1
- package/src/produceDraft.test.ts +3 -3
- package/src/react/components/CardList.test.tsx +1 -1
- package/src/react/components/DataTable.test.tsx +1 -1
- package/src/react/components/InfiniteScroll.test.tsx +5 -5
- package/dist/mvc-kit.cjs.map +0 -1
- package/dist/mvc-kit.js.map +0 -1
- package/dist/react-native.cjs.map +0 -1
- package/dist/react-native.js.map +0 -1
- package/dist/react.cjs.map +0 -1
- package/dist/react.js.map +0 -1
- package/dist/web.cjs.map +0 -1
- package/dist/web.js.map +0 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Controller, ViewModel, Collection, singleton, teardownAll } from 'mvc-kit';
|
|
2
|
+
|
|
3
|
+
// Controller: Stateless multi-ViewModel orchestrator
|
|
4
|
+
//
|
|
5
|
+
// Controllers coordinate between ViewModels, Models, and Services when
|
|
6
|
+
// a single ViewModel can't own the workflow. This is rare — most
|
|
7
|
+
// orchestration fits in a single ViewModel.
|
|
8
|
+
//
|
|
9
|
+
// Use Controller only for pure cross-cutting coordination with no state
|
|
10
|
+
// of its own: multi-step checkout, drag-and-drop between lists, etc.
|
|
11
|
+
//
|
|
12
|
+
// What Controller provides:
|
|
13
|
+
// - init() / dispose() lifecycle
|
|
14
|
+
// - subscribeTo() / listenTo() with auto-cleanup
|
|
15
|
+
// - disposeSignal for cancelling async operations
|
|
16
|
+
// - addCleanup() for custom teardown
|
|
17
|
+
//
|
|
18
|
+
// What Controller does NOT provide:
|
|
19
|
+
// - State (no set())
|
|
20
|
+
// - Computed getters
|
|
21
|
+
// - Async tracking
|
|
22
|
+
// - Events (no emit())
|
|
23
|
+
|
|
24
|
+
// --- Supporting types ---
|
|
25
|
+
|
|
26
|
+
interface Task {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
status: 'todo' | 'done';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface TaskListState {
|
|
33
|
+
items: Task[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Two ViewModels that the Controller coordinates ---
|
|
37
|
+
|
|
38
|
+
class TodoListViewModel extends ViewModel<TaskListState> {
|
|
39
|
+
removeItem(id: string) {
|
|
40
|
+
this.set({ items: this.state.items.filter(t => t.id !== id) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
addItem(task: Task) {
|
|
44
|
+
this.set({ items: [...this.state.items, task] });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class DoneListViewModel extends ViewModel<TaskListState> {
|
|
49
|
+
addItem(task: Task) {
|
|
50
|
+
this.set({ items: [...this.state.items, { ...task, status: 'done' as const }] });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Controller definition ---
|
|
55
|
+
|
|
56
|
+
class TaskTransferController extends Controller {
|
|
57
|
+
constructor(
|
|
58
|
+
private todoVM: TodoListViewModel,
|
|
59
|
+
private doneVM: DoneListViewModel,
|
|
60
|
+
) {
|
|
61
|
+
super();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected onInit() {
|
|
65
|
+
// subscribeTo auto-cleans up on dispose
|
|
66
|
+
this.subscribeTo(this.todoVM, (state) => {
|
|
67
|
+
console.log(`Todo list: ${state.items.length} items`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.subscribeTo(this.doneVM, (state) => {
|
|
71
|
+
console.log(`Done list: ${state.items.length} items`);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Pure coordination — moves an item from todo to done
|
|
76
|
+
completeTask(taskId: string) {
|
|
77
|
+
const task = this.todoVM.state.items.find(t => t.id === taskId);
|
|
78
|
+
if (!task) return;
|
|
79
|
+
this.todoVM.removeItem(taskId);
|
|
80
|
+
this.doneVM.addItem(task);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected onDispose() {
|
|
84
|
+
console.log('TaskTransferController disposed');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Usage ---
|
|
89
|
+
|
|
90
|
+
const todoVM = new TodoListViewModel({
|
|
91
|
+
items: [
|
|
92
|
+
{ id: '1', title: 'Learn mvc-kit', status: 'todo' },
|
|
93
|
+
{ id: '2', title: 'Build an app', status: 'todo' },
|
|
94
|
+
{ id: '3', title: 'Write tests', status: 'todo' },
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
todoVM.init();
|
|
98
|
+
|
|
99
|
+
const doneVM = new DoneListViewModel({ items: [] });
|
|
100
|
+
doneVM.init();
|
|
101
|
+
|
|
102
|
+
const controller = new TaskTransferController(todoVM, doneVM);
|
|
103
|
+
controller.init();
|
|
104
|
+
|
|
105
|
+
console.log('Todo:', todoVM.state.items.length); // 3
|
|
106
|
+
console.log('Done:', doneVM.state.items.length); // 0
|
|
107
|
+
|
|
108
|
+
// Controller coordinates the move
|
|
109
|
+
controller.completeTask('1');
|
|
110
|
+
|
|
111
|
+
console.log('Todo:', todoVM.state.items.length); // 2
|
|
112
|
+
console.log('Done:', doneVM.state.items.length); // 1
|
|
113
|
+
console.log('Done item:', doneVM.state.items[0]?.title); // 'Learn mvc-kit'
|
|
114
|
+
|
|
115
|
+
// Cleanup
|
|
116
|
+
controller.dispose();
|
|
117
|
+
todoVM.dispose();
|
|
118
|
+
doneVM.dispose();
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ViewModel, singleton, hasSingleton, teardown, teardownAll } from 'mvc-kit';
|
|
2
|
+
|
|
3
|
+
// ViewModel: Reactive state + computed getters + async tracking + typed events
|
|
4
|
+
//
|
|
5
|
+
// The core building block. Holds state, derives computed values via getters,
|
|
6
|
+
// provides actions to update state, and tracks async method loading/error
|
|
7
|
+
// automatically. After init(), getters are auto-memoized and only recompute
|
|
8
|
+
// when their dependencies change.
|
|
9
|
+
|
|
10
|
+
// Define state interface
|
|
11
|
+
interface CounterState {
|
|
12
|
+
count: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Create a ViewModel by extending the base class
|
|
16
|
+
class CounterViewModel extends ViewModel<CounterState> {
|
|
17
|
+
// --- Computed getters (auto-memoized after init) ---
|
|
18
|
+
get doubled(): number {
|
|
19
|
+
return this.state.count * 2;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get isPositive(): boolean {
|
|
23
|
+
return this.state.count > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get parity(): 'even' | 'odd' {
|
|
27
|
+
return this.state.count % 2 === 0 ? 'even' : 'odd';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- Actions ---
|
|
31
|
+
increment() {
|
|
32
|
+
this.set({ count: this.state.count + 1 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
decrement() {
|
|
36
|
+
this.set({ count: this.state.count - 1 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
reset() {
|
|
40
|
+
this.set({ count: 0 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Updater function pattern for derived updates
|
|
44
|
+
multiplyBy(factor: number) {
|
|
45
|
+
this.set(prev => ({ count: prev.count * factor }));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Lifecycle hook: called after each state update
|
|
49
|
+
protected onSet(prev: Readonly<CounterState>, next: Readonly<CounterState>) {
|
|
50
|
+
console.log(`Count changed: ${prev.count} → ${next.count}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Lifecycle hook: called when disposed
|
|
54
|
+
protected onDispose() {
|
|
55
|
+
console.log('CounterViewModel disposed');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Basic usage ---
|
|
60
|
+
|
|
61
|
+
const counter = new CounterViewModel({ count: 0 });
|
|
62
|
+
counter.init(); // activates getter memoization and async tracking
|
|
63
|
+
|
|
64
|
+
// Subscribe to state changes
|
|
65
|
+
const unsubscribe = counter.subscribe((state, prev) => {
|
|
66
|
+
console.log(`Subscriber notified: ${prev.count} → ${state.count}`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
counter.increment(); // Count changed: 0 → 1
|
|
70
|
+
counter.increment(); // Count changed: 1 → 2
|
|
71
|
+
counter.multiplyBy(5); // Count changed: 2 → 10
|
|
72
|
+
|
|
73
|
+
// Access current state
|
|
74
|
+
console.log('Current count:', counter.state.count); // 10
|
|
75
|
+
|
|
76
|
+
// --- Computed getters ---
|
|
77
|
+
|
|
78
|
+
console.log('Doubled:', counter.doubled); // 20
|
|
79
|
+
console.log('Is positive:', counter.isPositive); // true
|
|
80
|
+
console.log('Parity:', counter.parity); // even
|
|
81
|
+
|
|
82
|
+
counter.decrement(); // Count changed: 10 → 9
|
|
83
|
+
console.log('Doubled:', counter.doubled); // 18
|
|
84
|
+
console.log('Parity:', counter.parity); // odd
|
|
85
|
+
|
|
86
|
+
// Cleanup
|
|
87
|
+
unsubscribe();
|
|
88
|
+
counter.dispose();
|
|
89
|
+
|
|
90
|
+
// --- Singleton pattern ---
|
|
91
|
+
|
|
92
|
+
// Get or create a singleton instance
|
|
93
|
+
const shared1 = singleton(CounterViewModel, { count: 100 });
|
|
94
|
+
shared1.init(); // activate getter memoization on the singleton too
|
|
95
|
+
const shared2 = singleton(CounterViewModel, { count: 999 }); // args ignored, same instance returned
|
|
96
|
+
|
|
97
|
+
console.log('Same instance:', shared1 === shared2); // true
|
|
98
|
+
console.log('Singleton count:', shared1.state.count); // 100
|
|
99
|
+
|
|
100
|
+
// Check if singleton exists
|
|
101
|
+
console.log('Has singleton:', hasSingleton(CounterViewModel)); // true
|
|
102
|
+
|
|
103
|
+
// Cleanup singleton
|
|
104
|
+
teardown(CounterViewModel);
|
|
105
|
+
console.log('Has singleton after teardown:', hasSingleton(CounterViewModel)); // false
|
|
106
|
+
|
|
107
|
+
// Cleanup all singletons (useful in tests)
|
|
108
|
+
teardownAll();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare const __MVC_KIT_DEV__: boolean;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { EventBus } from 'mvc-kit';
|
|
2
|
+
|
|
3
|
+
// EventBus: Typed pub/sub for decoupled communication
|
|
4
|
+
|
|
5
|
+
// Define event types
|
|
6
|
+
interface AppEvents {
|
|
7
|
+
'user:login': { userId: string; timestamp: number };
|
|
8
|
+
'user:logout': { userId: string };
|
|
9
|
+
'notification': { message: string; type: 'info' | 'warning' | 'error' };
|
|
10
|
+
'cart:updated': { itemCount: number };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// --- Basic usage ---
|
|
14
|
+
|
|
15
|
+
const bus = new EventBus<AppEvents>();
|
|
16
|
+
|
|
17
|
+
// Subscribe to events
|
|
18
|
+
const unsubLogin = bus.on('user:login', ({ userId, timestamp }) => {
|
|
19
|
+
console.log(`User ${userId} logged in at ${new Date(timestamp).toISOString()}`);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const unsubNotification = bus.on('notification', ({ message, type }) => {
|
|
23
|
+
console.log(`[${type.toUpperCase()}] ${message}`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Emit events
|
|
27
|
+
bus.emit('user:login', { userId: '123', timestamp: Date.now() });
|
|
28
|
+
bus.emit('notification', { message: 'Welcome back!', type: 'info' });
|
|
29
|
+
|
|
30
|
+
// One-time subscription - auto-unsubscribes after first event
|
|
31
|
+
bus.once('user:logout', ({ userId }) => {
|
|
32
|
+
console.log(`User ${userId} logged out (one-time handler)`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
bus.emit('user:logout', { userId: '123' }); // Handler called
|
|
36
|
+
bus.emit('user:logout', { userId: '123' }); // Handler NOT called (already unsubscribed)
|
|
37
|
+
|
|
38
|
+
// Unsubscribe manually
|
|
39
|
+
unsubLogin();
|
|
40
|
+
unsubNotification();
|
|
41
|
+
|
|
42
|
+
// After unsubscribe, handlers are not called
|
|
43
|
+
bus.emit('user:login', { userId: '456', timestamp: Date.now() }); // No output
|
|
44
|
+
|
|
45
|
+
// --- Practical pattern: Cross-component communication ---
|
|
46
|
+
|
|
47
|
+
class AuthService {
|
|
48
|
+
constructor(private bus: EventBus<AppEvents>) {}
|
|
49
|
+
|
|
50
|
+
login(userId: string) {
|
|
51
|
+
// ... authentication logic ...
|
|
52
|
+
this.bus.emit('user:login', { userId, timestamp: Date.now() });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
logout(userId: string) {
|
|
56
|
+
// ... logout logic ...
|
|
57
|
+
this.bus.emit('user:logout', { userId });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class NotificationManager {
|
|
62
|
+
constructor(private bus: EventBus<AppEvents>) {
|
|
63
|
+
// React to login events
|
|
64
|
+
this.bus.on('user:login', () => {
|
|
65
|
+
this.bus.emit('notification', { message: 'Welcome!', type: 'info' });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Components can communicate without direct references
|
|
71
|
+
const auth = new AuthService(bus);
|
|
72
|
+
new NotificationManager(bus);
|
|
73
|
+
|
|
74
|
+
auth.login('789'); // Triggers login event, which triggers notification
|
|
75
|
+
|
|
76
|
+
// Cleanup
|
|
77
|
+
bus.dispose();
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Feed } from 'mvc-kit';
|
|
2
|
+
|
|
3
|
+
// Feed: Cursor-based pagination for infinite scroll / load-more
|
|
4
|
+
//
|
|
5
|
+
// A subscribable helper that accumulates items across pages using
|
|
6
|
+
// cursor-based server pagination. Tracks cursor position, hasMore
|
|
7
|
+
// status, and the growing item list.
|
|
8
|
+
//
|
|
9
|
+
// Designed to be a property on a ViewModel — auto-tracked so
|
|
10
|
+
// ViewModel getters that read feed state recompute automatically.
|
|
11
|
+
//
|
|
12
|
+
// Typical flow:
|
|
13
|
+
// 1. ViewModel calls API with feed.cursor
|
|
14
|
+
// 2. API returns { items, hasMore, cursor }
|
|
15
|
+
// 3. ViewModel calls feed.appendPage(result)
|
|
16
|
+
// 4. Repeat until hasMore is false
|
|
17
|
+
|
|
18
|
+
// --- Entity type ---
|
|
19
|
+
|
|
20
|
+
interface Post {
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Simulated API ---
|
|
27
|
+
|
|
28
|
+
function fakeFetchPosts(cursor: string | null): {
|
|
29
|
+
items: Post[];
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
cursor: string;
|
|
32
|
+
} {
|
|
33
|
+
const pages: Record<string, { items: Post[]; hasMore: boolean; cursor: string }> = {
|
|
34
|
+
initial: {
|
|
35
|
+
items: [
|
|
36
|
+
{ id: '1', title: 'First post', createdAt: '2024-01-01' },
|
|
37
|
+
{ id: '2', title: 'Second post', createdAt: '2024-01-02' },
|
|
38
|
+
{ id: '3', title: 'Third post', createdAt: '2024-01-03' },
|
|
39
|
+
],
|
|
40
|
+
hasMore: true,
|
|
41
|
+
cursor: 'page2',
|
|
42
|
+
},
|
|
43
|
+
page2: {
|
|
44
|
+
items: [
|
|
45
|
+
{ id: '4', title: 'Fourth post', createdAt: '2024-01-04' },
|
|
46
|
+
{ id: '5', title: 'Fifth post', createdAt: '2024-01-05' },
|
|
47
|
+
],
|
|
48
|
+
hasMore: true,
|
|
49
|
+
cursor: 'page3',
|
|
50
|
+
},
|
|
51
|
+
page3: {
|
|
52
|
+
items: [
|
|
53
|
+
{ id: '6', title: 'Sixth post', createdAt: '2024-01-06' },
|
|
54
|
+
],
|
|
55
|
+
hasMore: false,
|
|
56
|
+
cursor: 'end',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const key = cursor ?? 'initial';
|
|
61
|
+
return pages[key]!;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- Basic usage ---
|
|
65
|
+
|
|
66
|
+
const feed = new Feed<Post>();
|
|
67
|
+
|
|
68
|
+
// Subscribe to feed state changes
|
|
69
|
+
feed.subscribe(() => {
|
|
70
|
+
console.log(`Feed: ${feed.count} items, hasMore=${feed.hasMore}, cursor=${feed.cursor}`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Initial state
|
|
74
|
+
console.log('Cursor:', feed.cursor); // null
|
|
75
|
+
console.log('Has more:', feed.hasMore); // true
|
|
76
|
+
console.log('Items:', feed.count); // 0
|
|
77
|
+
|
|
78
|
+
// --- Load first page ---
|
|
79
|
+
|
|
80
|
+
const page1 = fakeFetchPosts(feed.cursor);
|
|
81
|
+
feed.appendPage(page1);
|
|
82
|
+
|
|
83
|
+
console.log('After page 1:', feed.count); // 3
|
|
84
|
+
console.log('Cursor:', feed.cursor); // 'page2'
|
|
85
|
+
console.log('Has more:', feed.hasMore); // true
|
|
86
|
+
|
|
87
|
+
// --- Load next page (items accumulate) ---
|
|
88
|
+
|
|
89
|
+
const page2 = fakeFetchPosts(feed.cursor);
|
|
90
|
+
feed.appendPage(page2);
|
|
91
|
+
|
|
92
|
+
console.log('After page 2:', feed.count); // 5
|
|
93
|
+
console.log('Cursor:', feed.cursor); // 'page3'
|
|
94
|
+
|
|
95
|
+
// --- Load final page ---
|
|
96
|
+
|
|
97
|
+
const page3 = fakeFetchPosts(feed.cursor);
|
|
98
|
+
feed.appendPage(page3);
|
|
99
|
+
|
|
100
|
+
console.log('After page 3:', feed.count); // 6
|
|
101
|
+
console.log('Has more:', feed.hasMore); // false (no more pages)
|
|
102
|
+
|
|
103
|
+
// Access all accumulated items
|
|
104
|
+
console.log('All items:', feed.items.map(p => p.title));
|
|
105
|
+
|
|
106
|
+
// --- Prepend (for chat UIs / newest-first feeds) ---
|
|
107
|
+
|
|
108
|
+
const chatFeed = new Feed<Post>();
|
|
109
|
+
|
|
110
|
+
chatFeed.appendPage({
|
|
111
|
+
items: [
|
|
112
|
+
{ id: 'c1', title: 'Latest message', createdAt: '2024-01-10' },
|
|
113
|
+
{ id: 'c2', title: 'Previous message', createdAt: '2024-01-09' },
|
|
114
|
+
],
|
|
115
|
+
hasMore: true,
|
|
116
|
+
cursor: 'older',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Prepend older messages at the top (e.g., scrolling up in a chat)
|
|
120
|
+
chatFeed.prependPage({
|
|
121
|
+
items: [
|
|
122
|
+
{ id: 'c3', title: 'Oldest message', createdAt: '2024-01-08' },
|
|
123
|
+
],
|
|
124
|
+
hasMore: false,
|
|
125
|
+
cursor: 'start',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
console.log('Chat order:', chatFeed.items.map(p => p.title));
|
|
129
|
+
// ['Oldest message', 'Latest message', 'Previous message']
|
|
130
|
+
|
|
131
|
+
// --- Push items without affecting cursor ---
|
|
132
|
+
|
|
133
|
+
feed.push({ id: '7', title: 'Optimistic post', createdAt: '2024-01-07' });
|
|
134
|
+
console.log('After push:', feed.count); // 7
|
|
135
|
+
|
|
136
|
+
// --- Filter items ---
|
|
137
|
+
|
|
138
|
+
feed.filter(p => p.id !== '7'); // remove the optimistic post
|
|
139
|
+
console.log('After filter:', feed.count); // 6
|
|
140
|
+
|
|
141
|
+
// --- Pull-to-refresh (replacePage) ---
|
|
142
|
+
|
|
143
|
+
feed.replacePage({
|
|
144
|
+
items: [
|
|
145
|
+
{ id: 'r1', title: 'Refreshed post 1', createdAt: '2024-02-01' },
|
|
146
|
+
{ id: 'r2', title: 'Refreshed post 2', createdAt: '2024-02-02' },
|
|
147
|
+
],
|
|
148
|
+
hasMore: true,
|
|
149
|
+
cursor: 'refreshed-page2',
|
|
150
|
+
});
|
|
151
|
+
console.log('After replacePage:', feed.count); // 2 (replaced all)
|
|
152
|
+
|
|
153
|
+
// --- setResult (update cursor/hasMore only) ---
|
|
154
|
+
|
|
155
|
+
feed.setResult({ hasMore: false, cursor: 'final' });
|
|
156
|
+
console.log('After setResult hasMore:', feed.hasMore); // false
|
|
157
|
+
console.log('Items unchanged:', feed.count); // 2
|
|
158
|
+
|
|
159
|
+
// --- Reset ---
|
|
160
|
+
|
|
161
|
+
feed.reset();
|
|
162
|
+
console.log('After reset:', feed.count, feed.cursor, feed.hasMore); // 0, null, true
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Model } from 'mvc-kit';
|
|
2
|
+
|
|
3
|
+
// Model: Reactive entity with validation and dirty tracking
|
|
4
|
+
|
|
5
|
+
interface UserFormState {
|
|
6
|
+
name: string;
|
|
7
|
+
email: string;
|
|
8
|
+
age: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class UserFormModel extends Model<UserFormState> {
|
|
12
|
+
setName(name: string) {
|
|
13
|
+
this.set({ name });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setEmail(email: string) {
|
|
17
|
+
this.set({ email });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setAge(age: number) {
|
|
21
|
+
this.set({ age });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Override to provide validation logic
|
|
25
|
+
protected validate(state: UserFormState) {
|
|
26
|
+
const errors: Partial<Record<keyof UserFormState, string>> = {};
|
|
27
|
+
|
|
28
|
+
if (!state.name.trim()) {
|
|
29
|
+
errors.name = 'Name is required';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!state.email.includes('@')) {
|
|
33
|
+
errors.email = 'Invalid email address';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (state.age < 0 || state.age > 150) {
|
|
37
|
+
errors.age = 'Age must be between 0 and 150';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return errors;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Usage ---
|
|
45
|
+
|
|
46
|
+
const form = new UserFormModel({ name: '', email: '', age: 0 });
|
|
47
|
+
|
|
48
|
+
// Subscribe to state changes
|
|
49
|
+
form.subscribe((state) => {
|
|
50
|
+
console.log('Form state:', state);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Check validation
|
|
54
|
+
console.log('Initial valid:', form.valid); // false
|
|
55
|
+
console.log('Initial errors:', form.errors); // { name: 'Name is required', email: 'Invalid email...' }
|
|
56
|
+
|
|
57
|
+
// Update fields
|
|
58
|
+
form.setName('John Doe');
|
|
59
|
+
form.setEmail('john@example.com');
|
|
60
|
+
form.setAge(30);
|
|
61
|
+
|
|
62
|
+
console.log('After updates valid:', form.valid); // true
|
|
63
|
+
console.log('After updates errors:', form.errors); // {}
|
|
64
|
+
|
|
65
|
+
// Dirty tracking - check if state differs from committed baseline
|
|
66
|
+
console.log('Is dirty:', form.dirty); // true (differs from initial state)
|
|
67
|
+
|
|
68
|
+
// Commit - mark current state as the new baseline
|
|
69
|
+
form.commit();
|
|
70
|
+
console.log('After commit dirty:', form.dirty); // false
|
|
71
|
+
|
|
72
|
+
// Make more changes
|
|
73
|
+
form.setName('Jane Doe');
|
|
74
|
+
console.log('After change dirty:', form.dirty); // true
|
|
75
|
+
|
|
76
|
+
// Rollback - revert to committed state
|
|
77
|
+
form.rollback();
|
|
78
|
+
console.log('After rollback name:', form.state.name); // 'John Doe'
|
|
79
|
+
console.log('After rollback dirty:', form.dirty); // false
|
|
80
|
+
|
|
81
|
+
// Cleanup
|
|
82
|
+
form.dispose();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Pagination } from 'mvc-kit';
|
|
2
|
+
|
|
3
|
+
// Pagination: Page-based state with array slicing pipeline
|
|
4
|
+
//
|
|
5
|
+
// A subscribable helper that manages page/pageSize state and slices
|
|
6
|
+
// arrays accordingly. Designed to be a property on a ViewModel —
|
|
7
|
+
// auto-tracked so ViewModel getters that read pagination state
|
|
8
|
+
// recompute automatically.
|
|
9
|
+
|
|
10
|
+
// --- Sample data ---
|
|
11
|
+
|
|
12
|
+
const allItems = Array.from({ length: 47 }, (_, i) => ({
|
|
13
|
+
id: String(i + 1),
|
|
14
|
+
title: `Item ${i + 1}`,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// --- Basic usage ---
|
|
18
|
+
|
|
19
|
+
const pagination = new Pagination(); // default pageSize: 10
|
|
20
|
+
|
|
21
|
+
// Subscribe to page changes
|
|
22
|
+
pagination.subscribe(() => {
|
|
23
|
+
console.log(`Page ${pagination.page} of ${pagination.pageCount(allItems.length)}`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Read current state
|
|
27
|
+
console.log('Page:', pagination.page); // 1
|
|
28
|
+
console.log('Page size:', pagination.pageSize); // 10
|
|
29
|
+
|
|
30
|
+
// --- Apply slice pipeline ---
|
|
31
|
+
|
|
32
|
+
const page1 = pagination.apply(allItems);
|
|
33
|
+
console.log('Page 1 items:', page1.length); // 10
|
|
34
|
+
console.log('First:', page1[0].title); // 'Item 1'
|
|
35
|
+
console.log('Last:', page1[9].title); // 'Item 10'
|
|
36
|
+
|
|
37
|
+
// --- Navigation ---
|
|
38
|
+
|
|
39
|
+
pagination.nextPage();
|
|
40
|
+
const page2 = pagination.apply(allItems);
|
|
41
|
+
console.log('Page 2 first:', page2[0].title); // 'Item 11'
|
|
42
|
+
|
|
43
|
+
pagination.nextPage();
|
|
44
|
+
pagination.nextPage();
|
|
45
|
+
pagination.nextPage(); // page 5 (last page)
|
|
46
|
+
|
|
47
|
+
const lastPage = pagination.apply(allItems);
|
|
48
|
+
console.log('Last page items:', lastPage.length); // 7 (47 - 40)
|
|
49
|
+
console.log('Last page first:', lastPage[0].title); // 'Item 41'
|
|
50
|
+
|
|
51
|
+
// --- Boundary checks ---
|
|
52
|
+
|
|
53
|
+
console.log('Page count:', pagination.pageCount(allItems.length)); // 5
|
|
54
|
+
console.log('Has next:', pagination.hasNext(allItems.length)); // false (on last page)
|
|
55
|
+
console.log('Has prev:', pagination.hasPrev()); // true
|
|
56
|
+
|
|
57
|
+
pagination.prevPage();
|
|
58
|
+
console.log('After prevPage:', pagination.page); // 4
|
|
59
|
+
|
|
60
|
+
// prevPage on page 1 is a no-op
|
|
61
|
+
pagination.setPage(1);
|
|
62
|
+
pagination.prevPage();
|
|
63
|
+
console.log('After prevPage on page 1:', pagination.page); // 1
|
|
64
|
+
|
|
65
|
+
// --- Change page size ---
|
|
66
|
+
|
|
67
|
+
pagination.setPageSize(20); // resets to page 1 automatically
|
|
68
|
+
console.log('New page size:', pagination.pageSize); // 20
|
|
69
|
+
console.log('Page after resize:', pagination.page); // 1
|
|
70
|
+
console.log('New page count:', pagination.pageCount(allItems.length)); // 3
|
|
71
|
+
|
|
72
|
+
const bigPage = pagination.apply(allItems);
|
|
73
|
+
console.log('Items on resized page:', bigPage.length); // 20
|
|
74
|
+
|
|
75
|
+
// --- Navigate to specific page ---
|
|
76
|
+
|
|
77
|
+
pagination.setPage(3);
|
|
78
|
+
const thirdPage = pagination.apply(allItems);
|
|
79
|
+
console.log('Page 3 items:', thirdPage.length); // 7 (47 - 40)
|
|
80
|
+
|
|
81
|
+
// --- Custom initial page size ---
|
|
82
|
+
|
|
83
|
+
const smallPages = new Pagination({ pageSize: 5 });
|
|
84
|
+
console.log('Custom page size:', smallPages.pageSize); // 5
|
|
85
|
+
console.log('Custom page count:', smallPages.pageCount(allItems.length)); // 10
|
|
86
|
+
|
|
87
|
+
// --- Reset ---
|
|
88
|
+
|
|
89
|
+
pagination.setPage(3);
|
|
90
|
+
pagination.reset(); // resets to page 1
|
|
91
|
+
console.log('After reset page:', pagination.page); // 1
|