New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Feathers-Vuex 2.0 rewritten with TypeScript #216
Conversation
If you watched a single param in the watcher, it would pass the single param to the `find` action instead of passing all props. This change makes sure no params are passed, which allows the `find` mixin to perform the proper params-picking logic, internally.
New actions expose functionality that was previously enclosed inside the `find` action.
The Feathers client directly modifies params, so this clones them so that the Feathers client doesn’t modify the query that gets passed to the `find` getter (when the same query params are used for both the action and the getter, as is done with the `makeFindMixin`.
@marshallswain v2 looks like it will be great! Looking through your commits, that's a hell of a lot of code. Could you give me some pointers on how you code so productively please? |
@james2mid haha. Thank you! The secret to my success is ample amounts of strategic procrastination. That might sound like a joke, but I'm totally serious! I find that when my brain is constantly engaged in processing and writing code in front of the computer, I don't spend enough time in a creative space. When I get away from the computer for a period of time, it allows me to get back into more of a creative ideal. Usually, the downtime is when I work out the simplest solutions. The "creative brain" tends to unravel the tendency to over-engineer things. :) Secret number two is take advice from the community. Lots of great feedback has guided Feathers-Vuex into something that I absolutely love to work with. ;) Thanks again for your kind words. |
@marshallswain you're very welcome and thank you for your advice! |
This adds the FeathersVuexFormWrapper so that it automatically gets installed by the Vue Plugin.
Differences in Feathers-Vuex 2.0
The biggest change in Feathers-Vuex 2.0 is that it has been refactored with TypeScript! (It's mostly ES6, still)
Your project does NOT require to be written in TypeScript. The
dist
is compiled to ES6.Trying it out
I accidentally published
feathers-vuex@2.0.0
with apre
tag on npm, you have to install the specific version number to get the latest code. To find out which is the most-recently published version, runnpm view feathers-vuex
and look at thepre
tag. You can ignore thenext
andpegasus
tags. Those were accidents that happened because I publish a lot of packages. :)To try the latest code.
npm view feathers-vuex
The end of the output looks something like this:
Now use the latest
pre
tag:My TypeScript experience
Initially, I wasn't a fan of TypeScript. I'm turned off by the steeper learning curve it introduces to writing code.
What I do like is the tooling. It's like having an assistant that alerts you to stupid moves before you have to accidentally discover them yourself. I'm only able to appreciate it for one reason: you can use directives to tell it to ignore lines or files, similar to ESLint. The tooling will then shut up and let me write JavaScript. TypeScript has been quite beneficial to allowing me to better see how to refactor and get rid of a huge monkey patch. It's down to a few lines of code, now.
I imagine I will like TypeScript more when there's first-class support for it built into Vue. I imagine how powerful it will be in an entire Vue project. It's one of the reasons that I'm REALLY excited about the new VueJS 3.0 API (The new function-based syntax is way more exciting, though. It's going to let me write some really pretty, well-organized code :)
But that's enough about TypeScript.
Where I need assistance: the build
The build system has been my only frustration with switching to TypeScript. Transpiling from TypeScript to JavaScript only to be transpiled by Babel (or whichever) into another form of JavaScript... has proven to be frustrating. I could really use some help in this area.
My original intent was to target the build to ES5. But there are some weird errors that show up when classes get transpiled into old Javascript code. Classes don't quite behave the same after transpiling. Things were so inconsistent that I couldn't write down any useful notes to tell you what I was experiencing. The only clarity I got out of the experience was that it was frustrating. ;)
I found that the simplest way around my frustration was to target ES6. I found next that Vue's default build assumes packages in the
node_modules
folder to be compiled to ES5. So you'll run into this error:This error was fixed by adding
feathers-vuex
to thetranspileDependencies
in thevue.config.js
file:It felt like everything was solved until I ran the production build. There were more errors. I finally copied the
feathers-vuex
folder fromnode_modules
into asrc/libs
folder in my project. Voila! It all works. So I'm currently running this code in production by using a shell script to copy it inside my project.rm -rf src/libs mkdir src/libs # feathers-vuex cp -r node_modules/feathers-vuex/dist src/libs/feathers-vuex
Then in my
package.json
scripts:I don't consider the above solution to be a pretty one. I likely will not publish 2.0 until a solution is discovered which doesn't required copying from
node_modules
. I know it's got to be simple. It's has something to do with transpile settings fornode_modules
. I just haven't found it yet.Quasar Build
For Quasar apps,
transpileDependencies
can be updated inquasar.conf.js
under build asNuxt Build
By default with Nuxt, the build server doesn't understand the word import, which is ES6 module syntax. You have to tell Nuxt in the config which node_modules to transpile from ES6 to ES5. (I tried avoiding this situation, but double transpilation while implementing classes can cause major issues at deployment time.) . Thankfully, the fix is easy in nuxt.config.js:
Here's what's new in
feathers-vuex
Check out the tests for the best documentation. They've been reorganized. This is still a Work in Progress.
Changes to Initialization
serverAlias
option is now required. This requires a simple addition to the initial options.BaseModel
is available. This is the baseFeathersVuexModel
which contains the model methods. Feel free to extend it and make it fit your awesome services!service
method has been renamed tomakeServicePlugin
.auth
method is now calledmakeAuthPlugin
models
object is now exported, so you can access them from anywhere. They are keyed byserverAlias
.clients
object is available. The intention is to allow working with multiple FeathersJS API servers.servicePath
to create a service-plugin. Instead, pass the actual Feathers service.makeServicePlugin
method.Below is an all-in-one example of a the basic configuration steps. See the next section for how to setup a project.
Setting up a project
There are four steps to setting up the entirety of
feathers-vuex
:Setup the FeathersJS Client
It's now recommended that the FeathersJS and client live together in the same file. This cleans up imports when setting up services. So let's start with the
feathers-client.js
file. I usually put this insrc/feathers-client.js
, but you can put it in the store folder if you want.Now that we have setup the client, we can use the configured exports in each of our services.
Setup the Services Plugins
Now let's setup a Vuex plugin for each service. I use Webpack's
require.context
to automatically import all of the services instead of explicitly typing them all. So, I'll put the services in thesrc/store/services
folder.Once the service plugin is exported, we can register it with Vuex, but first let's setup the auth plugin.
Setup the Auth Plugin
We'll use the
makeAuthPlugin
method to tell the auth plugin where to find our/users
service:Once you've added the export, we're finally ready to setup the store.
Register all plugins with Vuex
The final step is to add all of the plugins to the Vuex store.
With the above four steps accomplished, the base of most any application using
feathers-vuex
is ready to build something awesome!FeathersVuex Vue plugin changes
The Vue plugin is registered in exactly the same way. The difference comes when you try to find the Model classes in the
$FeathersVuex
object. Instead of finding models directly on the$FeathersVuex
object, they are namespaced by theserverAlias
you provided. This allows cleaner support for multiple APIs. Supposing you had this code in a component, previously...Modify it to include the new
serverAlias
. Suppose you set aserverAlias
ofmyApi
, you'd put this in the new version:Better default
idField
supportSince records are keyed by id,
feathers-vuex
needs to know what theidField
is for each service. In the last version, the default wasid
, and you had to specify something different. This version supportsid
and_id
with zero configuration. You only need to setidField
when you're using something other thanid
or_id
.There's still a warning message when records don't have a property matching the
idField
. Just like in the last version, it only appears when you turn ondebug: true
in the options.Support for Temporary Records
Feathers-Vuex 2.0 supports tracking temporary items and automatically assigns a temporary id to new records. It also adds the records to
state.tempsById
. This is customizable using thetempIdField
option.Because of the new ability to handle temporary records, a message is only logged when assigning a temporary id to a record. The
checkId
utility function has been removed, since this was its main purpose.Getters Work with Temporary Records
The
find
getter has been updated to include records fromstate.tempsById
, by default. You can passtemps: false
in the params to only searchstate.keyedById
:find({ query: {}, temps: false })
The
get
getter has also been updated to work with temp ids. Pass the tempId the way you normally would pass the id:get(tempId)
The "currentItem" workflow is no longer supported
The
setCurrent
mutation andcurrentId
state encouraged use of a very limiting API. It's much more common for apps to require more than one current record. ThecreateCopy
,resetCopy
(formerly calledrejectCopy
),commitCopy
, andclearCopy
mutations (since v1.x) provide a more flexible solution for implementing the same functionality. As a result of this, following have been removed from the modules:currentID
current
setCurrent
,clearList
,copy
The
diffOnPatch
option has been removed(See the next section for its replacement.)
I have not been able to find a diffing algorithm that works equally well acroos all schemas. It's especially difficult for nested schemas. Because of this,
diffOnPatch
is no longer a global option. It is being replaced by thediffOnPatch
static Model method. See the next section.Model Classes: BYOD (Bring Your Own Diffing)
First, why do any diffing? On the API server, an
update
request replaces an entire object, but apatch
request only overwrites the attributes that are provided in the data. For services with simple schemas, it doesn't really matter. But if your schema grows really large, it can be supportive to only send the updates instead of the entire object.A new
diffOnPatch
method is available to override in your extended models.diffOnPatch
gets called just before sending the data to the API server. It gets called with the data and must return the diffed data. By default, it is set todiffOnPatch: data => data
.Below is an example of how you might implement
diffOnPatch
. You would only ever use this with a cloned instance, otherwise there's nothing to diff.The
modelName
option has movedWhile the original intent was to completely remove the
modelName
option, it's still required after transpiling to ES5. This is because during transpilation, the class name gets stripped and can't be put back into place. Since ES5 is the default target for most build environments, themodelName
is still required to be specified, but it has been moved. Instead of being an option, it's required as a static property of each class.Note: Once ES6 is the default target for most build systems, modelName will become optional. For future upgradability, it's recommended that you give your
modelName
the exact same name as your model class.Options are no longer kept on the Model
The Model class no longer has an
options
property. You can access the same information through theModel.store.state[Model.namespace]
.The 'apiPrefix' option has been removed
Feathers-Vuex now includes full support for communicating with multiple FeathersJS APIs. The
apiPrefix
option was a poorly-implemented, hacky, first attempt at this same feature. Since it didn't work as intended, it has been removed. See this example test for working with multiple APIs:Services are no longer set up, internally
You no longer just pass a servicePath. Instead, create the service, then pass the returned service object.
Simplified Pending Mutations
Previously, there was a mutation for every single variety of method and set/unset pending. (
setFindPending
,unsetFindPending
, etc.). There were a total of twelve methods for this simple operation. They are now combined into two methods:setPending(method)
andunsetPending(method)
. Here's the difference.Simplified Error Mutations
The "error" mutations have been simplified similar to the "pending" mutations:
instanceDefaults
must be a functionIn the previous version, you could specify instanceDefaults as an object. It was buggy and limiting. In this new version,
instanceDefaults
must always be a function. See the next section for an example.Getter and Setter props go on the Model classes
One of the great features about using Model classes is data-level computed properties. You get to specify computed properties directly on your data structures instead of inside components, which keeps a better separation of concerns. In
feathers-vuex@2.x
, since we have direct access to the Model classes, it's the perfect place to define the computed properties:Relationships have been separated from
instanceDefaults
Feathers-Vuex 2.0 has a new API for establishing relationships between data. Before we cover how it works, let's review the old API.
Feathers-Vuex 1.x allowed using the
instanceDefaults
API to both setup default values for Vue reactivity AND establishing relationships between services. It supported passing a string name that matched a model name to setup a relationship, as shown in this next example. This was a simple, but very limited API:Any instance data with a matching key would overwrite the same property in the instanceDefaults, which resulted in an inconsistent API.
In Feathers-Vuex 2.0, the
instanceDefaults
work the same for setting defaults with only one exception: They no longer setup the relationships. The newsetupInstance
function provides an API that is much more powerful.As mentioned earlier, it MUST be provided as a function:
Notice in the above example that we did not return
user
. Relationships are now handled in thesetupInstance
method.Where
instanceDefaults
props get overwritten with instance data, the props returned fromsetupInstance
overwrite the instance data. If it were usingObject.assign
, internally (it's not, but IF it were), it would look like the below example, wheredata
is the original instance data passed to the constructor.Define Relationships and Modify Data with
setupInstance
The new
setupInstance
method allows a lot of flexibility in creating new instances. It has the exact same API as theinstanceDefaults
method. The only difference is the order in which they are applied to the instance data.Although it looks similar to
instanceDefaults
, it can't be used for default values. This is because it overwrites instance data. Having separate methods allows a clean separation between setting up defaults and establishing relationships with other constructors.Or below is an example that does the exact same thing with one line per attribute:
Where
instanceDefaults
props get replaced by instance data, the props returned fromsetupInstance
overwrite the instance data. If it were usingObject.assign
, internally (it's not, but IF it were), it would look like the below example, wheredata
is the original instance data passed to the constructor.Preventing duplicate merge when extending BaseModel with a custom constructor
The BaseModel constructor calls
mergeWithAccessors(this, newData)
. This utility function correctly copies data between both regular objects and Vue.observable instances. If you create a class where you need to do your own merging, you probably don't wantmergeWithAccessors
to run twice. In this case, you can use themerge: false
BaseModel instance option to prevent the internal merge. You can then access themergeWithAccessors
method by callingMyModel.merge(this, newData)
. Here's an example:It's important to note that setting
merge: false
in the options will disable thesetupinstance
function. You need to manually call it, like this:Customizing the BaseModel
Because we have access to the BaseModel, we can extend it to do whatever custom stuff we need in our application. The
feathers-client.js
file is a great, centralized location for accomplishing this:Auth plugin changes
With FeathersJS version 4, the user is returned in the authentication response, by default. This means that it's no longer required to provide a
userService
option to populate the user. 👍If you would like to enable backwards compatibility with the previous version of Feathers, pass the below code in to the
The above code will override the
responseHandler
auth action to work with the Passport-based version of Feathers Authentication.Gotchas
Don't Perform Queries (Side Effects) in Getters
Don't try to perform a query from within a getter like the example, below. It will result in an infinite loop:
Using custom query parameters
There are two places where the query operators have to be allowed.
whitelist
ing operators.paramsForServer
andwhitelist
options forfeathers-vuex
. Both accept an array of strings representing prop names, but now I can't remember why I determined that I needed both. :)For the Feathers Client, follow the FeathersJS docs for your database adapter.
Access
$FeathersVuex
models in NuxtasyncData
In
feathers-vuex@2.x
, you can get access to the$FeathersVuex
object by importing themodels
object from the main export:models
and$FeathersVuex
are the same object.