-
Notifications
You must be signed in to change notification settings - Fork 1
Design doc :: Vuex Macros for HaxeVX
Status: Implemented since haxelib version 0.6
HaxeVx for Vuex :: Interfaces that generate types and validates classes by macro:
In pseudo code examples, for functions with arguments enclosed with {}, type signatures should be manually left out in function arguments, so that macro will inject in specific types manually (typing it out manually is possible but will be checked against expected types, anyway)
For class/interface type-parameters (those in capitalized letters) that are non-applicable in certain cases, you can mark them with a NoneT
empty marker class, in order to prevent any form of direct access on those types.
Note: Haxe build/autoBuild macros are executed live while coding in your editor When such Haxe macros run, any generated fields/methods by the macros are reflected in Intellisense immediately, giving you full-type hinting support right off the bat, not just compile-time checking.
An IModules class includes a macro that consolidates any relavant static class methods as getter functions for Vuex, including it's state
reference and any other annotated fields for @:mutator
, @:action
@:module
, etc.
It also auto-generates the public read-only getter fields from the static getter methods, with matching types.
Note that VModule
is simply a bare-bones boilerplate class that implements IModule
.
Required type parameters:
-
S
: The local state type -
RS
: The root state type
Within the IModule
/VModule
class, your context-specific getters are declared as static functions to retrieve given value from state parameter.
var state:S;
// min 1 parameter required for all Getter functions.
public static function Get_someValue(state:{macro-checked against S}) {
}
public static function Get_something(state:{macro-checked against S}, getters:???) {
}
public static function Get_somethingsWhileReferingToRoot(state:{macro-checked against S}, getters:???, rootState:{macro-checked against RS}) {
}
// @:namespaced class style declarations below.
// If VModule/IModule class for module is explcitly marked as @:namespaced,
// due to enforced vuex module @:namespaced setting for this class, getters can automatically refer to local module context.
public static function Get_somethingNamespaced(state:{macro-checked against S}, getters:{macro-generated anon typedef of localised getter fields}, rootState:{macro-checked against RS}, rootGetters:???)) {
}
Such static functions can be public as well in order to be used in other contexts. Furthermore, you can also declare them as inline
.
If you use the @:namespaced
metadata on the given module class, Haxe will ALWAYS have such a module set with the vuex setting namespaced:true
, always. This will cause all mutations, actions, etc. as such to be deemed already registered as namespaced on native Vuex's end.
Alternatively, instead of using a global @:namespaced
annotation on the module class which would tie all your mutations/actions to be automatically prefixed by the module namespace path, you can instead manually further annotate individual @:mutator
or @:action
fields with @:useNamespacing
instead. This HaxeVx only feature allows you to apply a mixture of mutations/actions, some of which may/may not be namespaced, allowing a module to respond to both module-specific dispatches/commits and root dispatches/commits. By default, all @:mutation
and @:action
fields under the module class are not namespaced, so that they can globally respond to root dispatches/commits. Thus, @:useNamespacing
annotations allow you to manually control which mutations/actions are meant to respond from global scope versus module scope.
If you use @:useNamespacing
annotations for individual fields, you CANNOT use @:namespaced
annotation for your module class (ie. a compiler warning will warn you with: Annotation @:useNamespacing is ignored. Class is already @:namespaced
.
Type limitation caveats within Get_
static function handlers:
- The type for
rootGetters
parameter cannot be explicitly determined as it exists outside of the scope of the current module class. - The type for
getters
parameter for module classes that aren't annotated to@:namespaced
cannot be explicitly determined from within the module itself (since in a non-namespaced situation, it would refer to the root store getters, which is outside the scope of the current module class).
An IGetter class includes a macro that consolidates all it's static class methods as getter functions for Vuex/HaxeVx, and auto-generates public read-only getter fields from them with matching types.
Required type parameters:
-
S
: The local state type -
RS
: The root state type
The static Getter functions are defined in similar format to IModule/VModule. Only such functions can be declared and nothing else. (ie. no state:S. @:mutator
, @:action
, @:module
fields, etc.)
It actually extends from IGetters
via:
interface IStoreGetters extends IGetters<S, NoneT>
An IStoreGetters class includes a macro that consolidates all it's static class methods as getter functions for Vuex stores only (at root-level), and auto-generates plain public read-only property fields from them with matching types. If you are sure that your IGetters
will only be used on the root store itself, use IStoreGetters
instead for better optimization and brevity.
Required type parameters:
-
S
: The local state type
IStoreGetters
can only be declared on VxStore
classes under the store.getters
field, and no where else.
Within store getters, the getters
parameter under Get_
static function handlers have no type limitation caveat, ie. the type signature for getters
parameter is wired immediately to the local class context type itself.
An IMutatorclass includes a macro that consolidates all it's local class prototype private methods as mutator function handlers for Vuex, and auto-generates the matching helper public inline functions to commit the given mutations.
Required type parameters:
-
S
: The local state type
Mutators are as simple as it gets.
function setSomething(state:{S}, payload??) {
}
And auto-generates the following matching helper method that is underscore _
prefixed:
public inline function _setSomething(context:IVxContext, payload??, useNamespacing:Bool=false, namespace:String="")
{
if (useNamespacing) {
context.commit(namespace+"macroGeneratedStringFromClass", payload);
}
else {
context.commit("macroGeneratedStringFromClass", payload);
}
}
Since the method is inlined, it doesn't actually gets called at runtime. useNamespacing
is a Boolean constant flag to determine which inlined output branch to use, and so long as you supplied an explicit constant true
/false
in your function call) , the Haxe compiler can skip the if (useNamespacing)
check and the compiled code results in a single line. If you assigned a boolean variable (instead of constant) for useNamespacing
, though, it'll intelligently make the inlined if
consideration based off the variable. Thus, in either case, there's no additional runtime overhead.
The only external dependency required to be supplied to the inlined method, is the current IVxContext
, (which is available within component classes as store
reference, or action handler functions as context
parameter ). For more information on IVxContext
, see the next section on IAction.
An IAction class includes a macro that consolidates all it's local class prototype private methods as action handlers for Vuex, and auto-generates the matching helper public inline functions to dispatch the given actions.
-
IVxContext
is the base interface with the basic storecommit
anddispatch
functions. -
IVxContext1 extends IVxContext
supports additionalS
type parameter for current context state. This interfaces is also implemented byVxStore
by default. -
IVxContext2 extends IVxContext1
supports additionalG
type parameter for current context getters, which might refer to the current module's getters if the module registering this action instance is usingnamespaced:true
option, or root getters if the module isn't namespaced (by default). -
IVxContext3 extends IVxContext2
supports additionalRS
type parameter for root state. -
IVxContext4 extends IVxContext3
supports additionalRG
type parameter for root getters.
examples:
function doSomethingVerySimple(context:IVxContext, ?>payload:???) {}
function doSomethingReferCurState(context:IVxContext1{<S>}, ?>payload:???) { }
function doSomethingComplex(context:IVxContext3{<S,G?,RS>}, ?>payload:???) {}
function doSomethingNamespaced(context:IVxContext4{<S,G,RS,RG?>>}payload:???) { }
The auto-generated public inline _
helper methods are spawned in similar fashion to IMutator
implementation, but uses context.dispatch(..)
instead.
Type limitation caveats:
-
The types for getters
G
parameter and rootGettersRG
parameters in all circumstances cannot be explicitly determined. -
Be aware that there is no compile-time macro to detect whether you accidentally set something on
context.state
orcontext.rootState
within the action handler (this is hard to determine/write). This can only be detected effeciently at runtime if running Vuex in development (strict?) mode, which will check that you only commit mutations while processing actions. It's possible within Haxe to scan for AST expressions to determine if a=
assignment is done oncontext.state
orcontext.rootState
, but doing so indirectly through another variable reference (or another "magic" function within context.state) can be used to circumvent such checks.
For situations within G
/RG
getters which can't safely+accurately resolve their given types , an alternative approach is to simply call ANY class's static Getter functions and pass in the relavant state parameter. In fact, doing so might yield better performance (particular if the static functions are inlined) instead of relying on the dynamic ES5 getter approach from Vuex getters. In fact, why do we really need dynamically registered Vuex-style getters anyway in a Haxe environment, when you already have static inlinable class functions?
Also, this same practice can be adopted in regular Javascript by importing/refering to whatever handy getter functions you have in your code library to retrieve values from a given state.
In short, it might be a better practice to enforce either Dynamic
or NoneT
types for unknown types of getter/rootGetter, rather than assume with a given type, which might lead to runtime errors if you assumed the wrong type in your Haxe codebase. The current Typescript definitions for Vuex has "getters"-related fields typed to Dynamic any
by default. For HaxeVx, might consider an optional compile flag like D vue_no_untyped_getters
to use NoneT
instead on such unknown getters, forcing coders to rely on actual Getter(state)
functions instead.
Will generate internal :
// to inject into obj definition of mutators/actions:{} respectively:
public function _SetInto(obj:Dynamic, namespace:String){}
All getters are are defined as private static function format as below:
static function Get_getterName(state:T):? { return state.blabhlabh }
Which will generate the following fields by macro:
function get_getterName():? {
return untyped this._stg[_+"getterName"]; // "_stg is injected as store.getters, once nativeStore is created for "store".
}
public var getterName(get, never):?
together with:
public var _(default,never):String; // macro-generated field for namespace prefix reference of module.
The Haxe getter fields with get_ function calls will perform namespaced getter request using _
namespace reference and locally injected native Vuex $store.getters
.
When you call store.someModule.path.getterName
, this will call method this.$store.someModule.path.get_getterName()
at runtime. All namespacing is done behind the scenes already.
All store-only getters are are defined as private static function format as below:
static function Get_getterName(state:T):? { return state.blabhlabh }
Which will generate the following field by macro:
public var getterName(default,never):?; // direct access
Internal initialization functions (tailored to each class) will be macro-generated for each Stores and Modules classes.
For VxStore
, a macro-generated call to Init will automatically be added to the end of it's constructor as _Init("")
, so initialization occurs immediately (including all it's sub-modules) upon instantiation of a VxStore
implementation:
public function _Init(ns:String ) {
_ = ns;
#if (hasLocalClassMetadata(":namespaced")) {
untyped this.namespaced = true;
// will be namespaced by Vuex, so do NOT apply HaxeVx namespacing
var useNS:String = "";
}
#else {
var useNS:String = ns; // apply namespacing manually
}
#end
var d;
var cls:Dynamic = ThisClass;
var clsP:Dynamic = cls.prototype;
var key:String;
// if got getters under local class module/store,
d = {}; untyped this.getters = d;
// For each "getterField" in local class getters or in store's "getter" field class.
// set up parameters over def with given namespace to static getter functions
key = useNS+"getterField";
untyped d[key] = cls.Get_getterField;
// register field globally to GettersToInjectOf(this, ns, key)
// For any @:getter field add-ons ...
// if got getter add-ons, instantiate set up parameters over def with given namespace
if (getterBuildFieldName == null) {
getterBuildFieldName = new GetterModuleInstance(); // assumes no parameter in constructor.
}
cls= ClassOfGetter;
// For each "getterField" in `cls` getters
key = useNS+"getterField"+getSuffix(ClassOfGetter);
untyped d[key] = cls.Get_getterField;
// register field globally to GettersToInjectOf(getterBuildFieldName , ns + "|"+getterBuildFieldName, key)
// ...
// For any @:mutator field add-ons...
// if got mutators, set up parameters over def with given namespace if required
d = {}; untyped this.mutators = d;
if (mutatorBuildFieldName == null) {
mutatorBuildFieldName = getSingletonOf(MutatorClass); // assumes no parameter in constructor.
}
mutatorBuildFieldName._SetInto(d, #if(useNamespacing(buildField)) useNS #else "" #end ) );
// ...
// For any @:action field add-ons...
// if got actions, set up parameters over def with given namespace if required
d = {}; untyped this.actions = d;
if (actionBuildFieldName == null) {
actionBuildFieldName = getSingletonOf(ActionClass); // assumes no parameter in constructor.
}
actionBuildFieldName ._SetInto(d, #if(useNamespacing(buildField)) useNS #else "" #end ) );
// ...
// whether to use `useNS` setting or not is dependent on it's build field having @:useNamespacing annotations or not.
// For any @:module field add-ons
// if got modules, set up parameters over def with given namespace
d = {}; untyped this.modules = d;
// will auto-instnatite module by default, unless marked with @:manual metadata on module field (which is a good way to prevent infinite recursion and mark module fields that might conditionally be null).
// #may not occur if field has @:manual metadata included in
if (moduleBuildFieldName == null) {
moduleBuildFieldName = new ModuleInstance(); // assumes no parameter in constructor.
}
// #end
if (moduleBuildFieldName != null) {
d.moduleBuildFieldName = moduleBuildFieldName;
moduleBuildFieldName._Init(ns+moduleBuildFieldName+"/"); // warning, this is a recursive function call approach
}
// ...
}
public inline function _InjNative(nativeStoreGetter:Dynamic) {
untyped this._stg = nativeStoreGetter;
untyped this.state._ = _;
}
This feature is only available in HaxeVx, and not in regular Vuex ie. the ability to access modules' namespaces and getters directly from the given store reference while coding within components, with full strict-typed Intellisense.
A module getter can be quickly accessed from store via something like store.someModule.aModule.bModule.getterName
.
Likewise, it's recorded namespace of module can be easily accessed with underscore property of module, like store.someModule.aModule.bModule._
.
If a particular state class implements a IPrefixedState
interface or includes an assosiated build macro to include in a readonly underscore _:String
property, the property can be accessed from within the store's state tree, so long as the modules' associated states are somehow listed within the state tree as well. eg. store.state.someModule.aModule.bModule._
.
Namespace can be used as last parameter in macro-generated inline (Action/Mutator reference)._commitStr(store, {?payload:?}, useNamespace:Bool=false, namespace:String=""})
function. This allows you to commit mutations or dispatch actions with module namespaces in a strict typed manner elegantly. (eg. (Action/Mutator reference)._commitStr(store, true, store.someModule.aModule.bModule._)
), and is good for specifically targeting mutations/actions that will definitely only affect a certain module. Also note that due to Haxe inlining, the (Action/Mutator reference)
can also be null, it won't matter anyway as no instance function is actually called anyway.
By default, the intention is to keep HaxeVx+Vuex runtime initialization to the minimum in production/deployment mode. But with the HaxeVx-only features mentioned above, these are the only 2 additional post-construct processes required :
- Once native Vuex.Store is created from
VxStore
definition instance, run through global linear stack of collected initialized modules, and call their InjNative(nativeStore.getters) functions. This will inject the native store.getters reference as athis._stg
property. Clear the stack to an empty array so it can be re-used later for any dynamically registered modules. - Also, inject modules from
VxStore
definition instance into native Vuex store instance, so module getter helper methods and helper_
namespace variables can be accessed from the native Vuex store itself.
Note that if you dynamically register a module, something like Step 1-2 would happen as well to link those newly added modules up to Vuex.
Of course, more runtime checks will be done in development mode:
- All mutators/action classes will be regsitered as singletons when initializing VxStore as well during _Init of VxStore and it's modules recursively.
- Component requesting mutators/action classes via @:mutator/@:action metadata will attempt to request for singleton, and will show a warning if singleton not found in registry.