Skip to content

Commit d7b8ba6

Browse files
committed
Finished implementing new API
1 parent b735fc2 commit d7b8ba6

File tree

15 files changed

+570
-595
lines changed

15 files changed

+570
-595
lines changed

README.md

Lines changed: 145 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,16 @@
11
# Vuex Class Component
22
A Type Safe Solution for Vuex Modules using ES6 Classes and ES7 Decorators that works out of the box for TypeScript and JavaScript.
33

4-
## Goals
5-
* Ensure your Codebase is type safe when writing Vuex Modules.
6-
* Provide proxies for getters, mutations and actions in a type safe way
7-
* Create a Vuex Manager for handling all vuex calls throughout your codebase.
8-
9-
## Changelog
10-
- `v.1.1.0` - Sub Modules support.
11-
- `v.1.4.0` - async/await now works with actions in mutatate mode.
12-
- `v.1.5.0` - JavaScript Support
13-
- `v.1.6.0` - NuxtJS Support.
14-
154
## Installation
165
```
176
$ npm install --save vuex-class-component
187
```
198

20-
## Examples
21-
- Vuex Class Component Simple: https://github.com/michaelolof/vuex-class-component-simple
22-
- Vuex Class Component Test: https://github.com/michaelolof/vuex-class-component-test
9+
## New API
10+
The goal of the new API is to reduce the decorator overhead and
11+
https://github.com/michaelolof/vuex-class-component/issues/27
2312

24-
## How to use
25-
Consider this example.
13+
How we normally define Vuex Stores.
2614
```js
2715
// user.vuex.ts
2816
const user = {
@@ -31,12 +19,11 @@ const user = {
3119
firstname: "Michael",
3220
lastname: "Olofinjana",
3321
specialty: "JavaScript",
34-
occupation: "Developer",
3522
},
3623
mutations: {
37-
changeName(state, payload) {
38-
state.firstname = payload.firstname;
39-
state.lastname = payload.lastname;
24+
clearName(state ) {
25+
state.firstname = "";
26+
state.lastname = "";
4027
}
4128
},
4229
actions: {
@@ -45,39 +32,29 @@ const user = {
4532
},
4633
getters: {
4734
fullname: (state) => state.firstname + " " + state.lastname,
48-
occupation: (state) => state.occupation,
49-
specialty: (state) => state.specialty,
35+
bio: (state) => `Name: ${state.fullname} Specialty: ${state.specialty}`,
5036
}
5137
}
5238
```
53-
This is how we previously wrote Vuex Modules. Some of the obvious difficulties with this approach are:
5439

55-
* Lack of Type Safety: Type Safety and Intellisense when defining the store.
56-
* Also notice how we are redefining **occupation** and **specialty** fields both as states and getters. This repitition and can easily lead to bugs if we update one and forget to update the other
57-
* We also don't have any type secure way to use this module in our Vue components. We have to use strings to handle getters, commits and dispatch calls. This can easily lead to bugs.
58-
59-
A better approach to solving this problem would be to use a VuexModule class as follows.
6040
```ts
61-
// user.vuex.ts
62-
import { VuexModule, mutation, action, getter, Module } from "vuex-class-component";
41+
import { createModule, mutation, action, extractVuexModule } from "vuex-class-component";
6342

64-
// TypeScript Only. (JavaScript devs don't need this)
65-
interface Name {
66-
firstname:string;
67-
lastname:string;
68-
}
43+
const VuexModule = createModule({
44+
namespaced: true,
45+
strict: false,
46+
target: "nuxt",
47+
})
6948

70-
@Module({ namespacedPath: "user/" })
7149
export class UserStore extends VuexModule {
72-
50+
7351
private firstname = "Michael";
7452
private lastname = "Olofinjana";
75-
@getter specialty = "JavaScript"; // The @getter decorator automatically exposes a defined state as a getter.
76-
@getter occupation = "Developer";
77-
78-
@mutation changeName({firstname, lastname}:Name) {
79-
this.firstname = firstname;
80-
this.lastname = lastname;
53+
specialty = "JavaScript";
54+
55+
@mutation clearName() {
56+
this.firstname = "";
57+
this.lastname = "";
8158
}
8259

8360
@action async doSomethingAsync() { return 20 }
@@ -89,89 +66,152 @@ export class UserStore extends VuexModule {
8966
}
9067

9168
// Explicitly define a vuex getter using class getters.
92-
get fullName() {
69+
get fullname() {
9370
return this.firstname + " " + this.lastname;
71+
}
72+
73+
// Define a mutation for the vuex getter.
74+
// NOTE this only works for getters.
75+
set fullname( name :string ) {
76+
const names = name.split( " " );
77+
this.firstname = names[ 0 ];
78+
this.lastname = names[ 1 ];
79+
}
80+
81+
get bio() {
82+
return `Name: ${this.fullname} Specialty: ${this.specialty}`;
9483
}
9584
}
96-
```
97-
This is a significant improvement over our initial method. First we get type safety out of the box since we're using classes without doing much work. Also note that **occupation** and **specialty** don't need to be redefined as getters any longer. Just import the getter decorator, use it on an already defined state and it automatically becomes a getter. This is useful when you have a long list of initialized state you also want to make available as getters. Which is also why state that are not getters must be **private**\
98-
\
99-
This module can now be used in our Vuex store by extracting it like so.
100-
```ts
85+
10186
// store.vuex.ts
10287
export const store = new Vuex.Store({
10388
modules: {
104-
/**
105-
* PLEASE NOTE
106-
* ---------------------
107-
* If you're using namespaces in your modules, then the modules object field
108-
* must be the same value as your "namespacedPath" value.
109-
* In this case, the "user" field must be the same as the namespacedPath "user/"
110-
*
111-
* If you're not using namespaces, this doesn't matter.
112-
*/
113-
user: UserStore.ExtractVuexModule( UserStore ),
89+
...extractVuexModule( UserStore )
11490
}
11591
})
92+
93+
// Creating proxies.
94+
const vxm = {
95+
user: createProxy( UserStore ),
96+
}
11697
```
11798

118-
## Ok. So What About Vue Components?
119-
Ensuring type safety in Vuex Modules is just one half of the problem solved. We still need to use them in our Vue Components.\
120-
\
121-
To do this is we just create a proxy in our Vue Component.
99+
On the surface, it looks like not much has changed. But some rethinking has gone into how the libary works to make for a much better developer experience.
100+
101+
## More Powerful Proxies
102+
With the `strict` option set to `false` we can enable greater functionality for our proxies with automatic getters and setters for our state.\
103+
For Example:
122104
```ts
123-
@Component
124-
export class MyComponent extends Vue {
125-
126-
user = UserStore.CreateProxy( this.$store, UserStore );
127-
128-
mounted() {
129-
/** Now we can easily call */
130-
this.user.fullName; /** Michael Olofinjana */
131-
this.user.occupation; /** Developer */
132-
this.user.changeName({ firstname:"John", lastname:"Doe" });
133-
this.user.doAnotherAsyncStuff(payload)
134-
.then( result => console.log( result ) );
105+
vxm.user.firstname // Michael
106+
vxm.user.firstname = "John";
107+
vxm.user.firstname // John
108+
109+
vxm.user.fullname // John Olofinjana
110+
vxm.user.fullname = "Mad Max";
111+
vxm.user.fullname // Mad Max
112+
vxm.user.firstname // Mad
113+
vxm.user.lastname // Max
114+
```
115+
116+
Notice that we didn't have to define a mutation to change the `firstname` we just set the state and it updates reactively. This means no more boilerplate mutations for our state, we just mutate them directly.
117+
118+
This also opens up new possibilities in how we consume stores in Vue components.
119+
Example
120+
```html
121+
<!-- App.vue -->
122+
<template>
123+
<div class>
124+
<input type="text" v-model="user.firstname" />
125+
<div>Firstname: {{ user.firstname }}</div>
126+
127+
<button @click="user.clearName()">Clear Name</button>
128+
<div>Bio: {{ user.bio }}</div>
129+
</div>
130+
</template>
131+
132+
<script>
133+
import { vxm } from "./store";
134+
135+
export default {
136+
data() {
137+
return {
138+
user: vxm.user,
139+
}
135140
}
136141
}
142+
</script>
137143
```
138144

139-
No more getters, commits or dispatch calls using strings. We just call them like we defined them in our class.
145+
Notice how much boilerplate has been reduced both in defining our vuex stores and also in using them in our components.
146+
Also notice we no longer need functions like `mapState` or `mapGetters`.
140147

141-
### We can do better
142-
All is looking good. we've ensured type safety in our Vue Components by creating proxies that rely on the store object and then just calling the methods like normal.\
143-
\
144-
The only problem here is, in reality we might have one component making request to multiple vuex modules. Imagine a vue component talking to 5 vuex modules. Importing and creating proxies for each module in all our components might become repetitive and frankly unecessary.
148+
## Implementing More Vuex Functionality
149+
Vuex today has additional functionalities like `$watch` `$subscribe` and `$subScribeAction` respectfully.
145150

146-
### Vuex Manager
147-
A vuex manager is simply an exportable object that houses all your proxies. We can easily create a vuex manager in our original vuex store file. (See Example)
151+
This also possible with `vuex-class-component`
148152
```ts
149-
// store.vuex.ts
150-
export const store = new Vuex.Store({
151-
modules: {
152-
user: UserStore.ExtractVuexModule( UserStore ),
153-
pages: PagesStore.ExtractVuexModule( PagesStore ),
154-
story: StoryStore.ExtractVuexModule( StoryStore ),
153+
// Watch getters in vuex components
154+
vxm.user.$watch( "fullname", newVal => {
155+
console.log( `Fullname has changed: ${newVal}` )
156+
});
157+
158+
// Subscribe to mutations in vuex components
159+
vxm.user.$subscribe( "clearName", payload => {
160+
console.log( `clearName was called. payload: ${payload}` )
161+
});
162+
163+
// Subscribe to an action in vuex components
164+
vxm.user.$subscribeAction( "doSomethingAsync", {
165+
before: (payload :any) => console.log( payload ),
166+
after: (payload :any) => console.log( payload ),
167+
})
168+
```
169+
170+
We can even do better with Local watchers and subscribers.
171+
172+
```ts
173+
const VuexModule = createModule({
174+
namespaced: true,
175+
strict: false,
176+
target: "nuxt",
177+
enableLocalWatchers: true,
178+
})
179+
180+
export class UserStore extends VuexModule {
181+
182+
firstname = "John";
183+
lastname = "Doe";
184+
@mutation changeName( name :string ) { ... }
185+
@action fetchDetails() { ... }
186+
get fullname() {
187+
return this.firstname + " " + this.lastname;
188+
}
189+
190+
$watch = {
191+
fullname( newValue ) { console.log( `Fullname has changed ${newValue}` },
192+
}
193+
194+
$subscribe = {
195+
changeName( payload ) {
196+
console.log( `changeName was called with payload: ${payload}`)
155197
}
156-
})
198+
}
157199

158-
export const vxm = {
159-
user: UserStore.CreateProxy( store, UserStore ),
160-
pages: PagesStore.CreateProxy( store, PagesStore ),
161-
story: StoryStore.CreateProxy( store, StoryStore ),
200+
$subscribeAction = {
201+
fetchDetails( payload ) {
202+
console.log( `fetchDetails action was called with payload: ${payload}` )
203+
}
162204
}
163205

164-
/** vxm (stands for vuex manager) can then be imported by any Vue Component and used */
165-
vxm.user.fullname /** Michael Olofinjana */
206+
}
166207
```
167-
With this any component that imports **vxm** can easily use any vuex module without any hassle with full type support and intellisense.
208+
209+
168210
169211
## SubModules Support
170-
From version 1.1 We now get the ability to include sub modules in our Vuex Classes.
171-
Let say we had a sub module called `CarStore`
212+
To use submodules
172213
```ts
173-
@Module({ namespacedPath: "car/" })
174-
class CarStore extends VuexModule {
214+
class CarStore extends createModule({}) {
175215
@getter noOfWheels = 4;
176216

177217
@action drive() {
@@ -181,9 +221,8 @@ Let say we had a sub module called `CarStore`
181221
```
182222
We could use this sub module in a class
183223
```ts
184-
@Module({ namespacedPath: "vehicle/" })
185-
class Vehicle extends VuexModule {
186-
car = CarStore.CreateSubModule( CarStore );
224+
class Vehicle extends createModule({}) {
225+
car = createSubModule( CarStore );
187226
}
188227
```
189228
Now you can easily use in your Vue Components like:
@@ -212,60 +251,10 @@ import { Module, VuexModule, getter, action } from "vuex-class-component/js";
212251
From verison `1.6.0` Nuxt is also supported.
213252
To use `vuex-class-component` with Nuxt, You add a `target` property to the @Module decorator and set it to `"nuxt"`.
214253
```js
215-
@Module({ namespacedPath: "user/", target: "nuxt" })
216-
217-
export class UserStore {
254+
export class UserStore extends createSubModule({ target: "nuxt" }) {
218255
...
219256
}
220257
```
221258
222-
## A note on Vuex Actions?
223-
Vuex Actions comes in two modes. A `mutate` mode and a `raw` mode. Both can be very useful.\
224-
For most of your use cases the `mutate` mode is all you'll need. The `raw` mode can be especially useful when you need access to `rootState` and `rootGetters`.\
225-
This is how we use them
226-
```ts
227-
import { getRawActionContext } from "vuex-class-component";
228-
229-
@Module()
230-
class MyModule extends VuexModule {
231-
232-
private name = "John Doe";
233-
@getter occupation = "Engineer"
234-
235-
@mutation
236-
changeOccupation(occupation) {
237-
this.occupation = occupation;
238-
}
239-
240-
// This action is in mutate mode by default hence
241-
// the brackets and options are not necessary.
242-
@action dummyAction() { ... }
243-
244-
@action({ mode:"mutate"})
245-
async mutatedAction(payload) {
246-
this.name = payload.firstname + " " + payload.lastname;
247-
this.changeOccupation("Developer");
248-
this.occupation /** Developer */
249-
const dummyVal = await this.dummyAction();
250-
return dummyVal;
251-
}
252-
253-
@action({ mode: "raw" })
254-
async rawAction(payload) {
255-
const context = getRawActionContext( this );
256-
context.state.name = payload.firstname + " " + payload.lastname;
257-
context.commit("changeOccupation", "Developer");
258-
context.getters.occupation /** Developer */
259-
const dummyVal = await context.dispatch("dummyAction");
260-
return dummyVal;
261-
}
262-
}
263-
```
264-
The above code snippet highlights the difference between the two action modes.
265-
266-
Mutated Actions can access state, getters, mutations and other actions with the `this` keyword just like any normal function.
267-
268-
Raw Actions on the other hand gives you access to `rootState` and `rootGetters`. The only limitation to this appoach however is that **you can't and shouldn't use the `this` keyword.** Instead you should get back the context object with the `getRawActionContext` function and then treat the function body like a regular vuex action.
269-
270-
All actions MUST return a promise.\
271-
All actions proxies are totally type safe and can still be used normally in Vue components whether mutatated or raw.
259+
## See Old API
260+
[Old API >](old-api-readme.md)

0 commit comments

Comments
 (0)