Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,116 @@
# Created by https://www.toptal.com/developers/gitignore/api/yarn,intellij+all
# Edit at https://www.toptal.com/developers/gitignore?templates=yarn,intellij+all

### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# AWS User-specific
.idea/**/aws.xml

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

### Intellij+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360

.idea/

# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023

*.iml
modules.xml
.idea/misc.xml
*.ipr

# Sonarlint plugin
.idea/sonarlint

### yarn ###
# https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored

.yarn/*
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions

# if you are NOT using Zero-installs, then:
# comment the following lines
!.yarn/cache

# and uncomment the following lines
# .pnp.*

# End of https://www.toptal.com/developers/gitignore/api/yarn,intellij+all

/node_modules
/build/.rpt2_cache
/dist
144 changes: 111 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,33 +43,66 @@ import VueStore from 'vue-class-store'

@VueStore
export class Store {
// properties are rebuilt as reactive data values
public value: number
// any properties present after constructing your object are made reactive
private value = 10
name: string

// construct your object like normal
constructor(name: string) {
this.name = 'reactive ' + name
// You can use `this` in the constructor, because
// VueStore adds reactivity to the object in-place.
setInterval(() => this.value++, 1000);
}

// getters are converted to (cached) computed properties
public get double (): number {
return this.value * 2
// You can't call $emit/$watch/etc. in your constructor, since your
// object isn't a fully-fledged Vue object yet, so the 'created'
// lifecycle hook is exposed for that purpose. Properties added here
// will not be reactive.
created() {
this.$watch('name', () => { console.log('the name changed') })
}

// constructor parameters serve as props
constructor (value: number = 1) {
// constructor function serves as the created hook
this.value = value
// getters are converted to (cached) computed properties
public get double(): number {
return this.value * 2
}

// prefix properties with `on:` to convert to watches
'on:value' () {
// prefix properties/methods with `on:` to convert to watches.
'on:value'() {
console.log('value changed to:', this.value)
}

// you can add `.immediate` and/or `.deep` to set those watch flags
'on.immediate:name'() {
console.log('name is now:', this.name)
}

// you can even drill into sub properties!
// you can drill into sub properties
'on:some.other.value' = 'log'

// class methods are added as methods
log () {
// methods work as normal
log() {
console.log('value is:', this.value)
}

// static properties and methods work
static stuff = 100
static doStuff() {
console.log('doing things #' + stuff)
stuff += 10
}
}

// instanceof works
new Store() instanceof Store;

// your store behaves just like a `Vue` instance, including methods like $emit.
// however, you have to tell typescript that by creating an identically-named
// interface which extends `Vue`. You can't use any of them in your constructor
// though, since your object isn't a fully-initialized Vue instance yet.
import Vue from 'vue'
interface Store extends Vue {}
```

### Instantiation
Expand All @@ -79,7 +112,7 @@ To use a store, simply instantiate the class.
You can do this outside of a component, and it will be completely reactive:

```typescript
const store = new Store({ ... })
const store = new Store(...)
```

Or you can instantiate within a component:
Expand All @@ -89,38 +122,48 @@ export default {
...
computed: {
model () {
return new Store({ ... })
return new Store(...)
}
}
}
```

Alternatively, you can make any non-decorated class reactive on the fly using the static `.create()` method:
Alternatively, you can make any non-decorated object reactive on the fly using the static `.create()` method:

```typescript
import VueStore from 'vue-class-store'
import Store from './Store'

const store: Store = VueStore.create(new Store({ ... }))
const store: Store = VueStore.create({someData: 10, otherData: 20, 'on:someData'() {...}})
```

## How it works

This is probably a good point to stop and explain what is happening under the hood.
This is probably a good point to stop and explain what is happening under the hood.

Immediately after the class is instantiated, the decorator function extracts the class' properties and methods and rebuilds either a new Vue instance (Vue 2) or a Proxy object (Vue 3).
First we do some prep work. When and how exactly this happens depends on whether you use `@VueStore` or
`VueStore.create`, but whatever the method, the entire contents of `Vue.prototype` is merged into your prototype. This
makes your instances "look" just like `Vue`.

This functionally-identical object is then returned, and thanks to TypeScript generics your IDE and the TypeScript compiler will think it's an instance of the *original* class, so code completion will just work.
Your constructor is then called, returning your new object. `VueStore` intercepts the object, rips out all the data,
then turns around and tells Vue to initialize this (now empty) object as a Vue instance, passing it your data. Vue then
happily puts that data right back where it came from, but with added reactivity.

Additionally, because all methods have their scope rebound to the original class, breakpoints will stop in the right place, and you can even call the class keyword `super` and it will resolve correctly up the prototype chain.
![howitworks](docs/howitworks.png)

![devtools](docs/devtools.png)
### `@VueStore`
`@VueStore` is able to frontload both the injection of `Vue.prototype` and collecting your prototype's methods to form
the basis of the options object sent to vue. It also does a couple `class`-specific things. It copies the static methods
and properties from the base class into itself, ensuring that statics continue to work, and sets its `prototype` to the
wrapped class's `prototype`, meaning `obj instanceof Store` still works.

Note that the object will of course be a `Vue` or `Proxy` instance, so running code like `store instanceof Store` will return `false` .
### `VueStore.create`
`VueStore.create` differs somewhat in the way it injects `Vue.prototype`. Because we don't want to modify the entire
class of the object, we create a new "anonymous" prototype which extends the original one. We then inject vue into that
prototype, leaving the original intact.

## Inheritance

The decorator supports class inheritance meaning you can do things like this:
The decorator supports superclasses, meaning you can do things like this:

```typescript
class Rectangle {
Expand All @@ -146,35 +189,70 @@ class Square extends Rectangle {
}
```

Make sure you **don't inherit from another decorated class** because the original link to the prototype chain will have been broken by the substituted object returned by the previous decorator:
However, the decorator does *not* support subclassing. Because reactivity would be injected before the subclass
constructor, and due to prototype shenanigans, reactivity would start behaving in bizarre ways.

```typescript
// don't do this!

@VueStore
class Rectangle { ... }
class Rectangle {
width = 0
height = 0

get area() {
return width * height
}

'on:area'() {
console.log('area changed')
}
}

@VueStore
class Square extends Rectangle { ... }
class Square extends Rectangle {
// this won't be reactive
size = 0

// this getter override won't be called
get area() {
return size * size
}

// this won't be reactive
get perimeter() {
return size * 4
}

// this method override won't be called
'on:area'() {
console.log('square changed')
}

// this watch won't work
'on:width'() {
console.log('width changed')
}
}
```

If you need to keep the original Rectangle and Square intact, decorate a final empty class that leaves the original classes untouched:
If you need to keep the original hierarchy, decorate a final empty class that leaves the original classes untouched:

```typescript
// do this instead...

class Rectangle { ... }
class Square extends Rectangle { ... }

@VueStore
class RectangleStore extends Rectangle { }
@VueStore
class SquareStore extends Square { }
```

Alternatively, use inline creation:

```typescript
import Square from './Square'

const model: Rectangle = VueStore.create(new Rectangle(10))
const model: Square = VueStore.create(new Square(10))
```

Expand Down
Binary file added docs/howitworks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading