Inheritable/extensible reusable code, advanced composables or traits #801
haustvindr
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Motivation
Aiming for a much greater code reusability between components, complete with functions override to enjoy some kind of limited inheritance, as well as common props, emits, and exposed functions.
Summary
The idea is to have some composable-alike code files, that can be referred within a component and loaded in order. They then are compiled into Vue code and ultimately merged in a single object containing all the end functionality and optionally returned to the component code. For simplicity and differentiation against normal composables I'll call them "traits", but the name is not really important.
Merging and overriding
The merge process will cycle through the traits in the order provided and merge them in a temporal object, which is then passed as a parameter to the next trait. This allows any trait to call the previous merge in a similar way to class inheritance "super.", therefore enabling function overriding without losing any inherited trait functionality.
The final merge is returned by the merge function, allowing the component to use the resulting merge. This enables the ability to use common code between components with similar funcionatilites. E.g. both a list view and an icon view may have the concept of "sort by", and to keep track of and perform the sorting, it may be reasonable to build a "has-order" trait to provide methods to perform the sorting.
Pros: All traits functionality will be consolidated in a single object, there is no need to track which one has which function. Very simple to use, and very simple to offload the code elsewhere and keep components tidy. Able to call earlier merger functions, enabling overriding while still keeping full functionality.
Cons: Merge possibly may need to be done at runtime. There may be unwanted/unexpected overrides if a component uses many traits at the same time.
Unique instance
The traits must provide the component with an unique instance of itself, to allow each component to hold their own trait with their own data. Only the code should be shared.
Pros: No collateral effects, unless the developer explicitly want it.
Cons: Probably more complicated setup when coding traits.
Sharing data between the component and the trait
Traits must be able to receive parameters when defined in the component. This will allow the component to share with the trait variables, refs/reactives, functions, and so on. The goal is to try to allow the developer to keep the most code possible in the trait, without resorting to hacks or partial implementations.
E.g. a trait for data validation based on a schema, will need both the data and the schema as parameters.
The trait can include any kind of structure in the merged object, be it functions, refs/reactives, constant/variables, or anything else.
Pros: Offload of data management to the traits where it makes sense, minimizing partial/support code in the component.
Cons: There may be unwanted/unexpected overrides if a component uses many traits at the same time.
Props sharing
The trait can define any number of props, which will be included automatically in the component defineProps, thus allowing some kind of props inheritance. Props will be overridden if they have the same name.
The component props will be received as a trait parameter in runtime, in case the trait needs to check their values.
Pros: Less props duplication in the components. Traits can define their own props needed for their own functionality, independently of the component.
Cons: IDEs may have a harder time figuring props autocomplete. Harder to track which props are available to components.
Exposed functions sharing
As with props, a trait can define exposed functions, which will end in the defineExpose macro. These functions must be bound to the particular trait instance to prevent dereference of "this". Exposed functions will be overriden if they have the same name.
Pros: Less component functions duplication for exposed functionality. Traits can define and manage exposed functionality, independently of the component.
Cons: IDEs may have a harder time figuring exposed functions. There may be unwanted/unexpected overrides if a component uses many traits at the same time.
Emit sharing
Similarly to props and exposed functions, a trait can define emits. Since the emitting logic depends on the component logic, the trait don't need to do the emitting themselves, but an emit function will be received as a trait parameter in case they need to.
Pros: Less emit definitions duplication. Traits can define emits independently of the component.
Cons: IDEs may have a harder time figuring component events. Logic should still be needed in the component, even for very common cases.
Interface
In component
Can be anything sugary enough, really.
Parameters to traits may need to be named for the trait code to find them, so a parameter object may be desirable instead of direct function parameters, therefore I came up with this interface and macro:
I would really prefer to be able to skip the "null" (or empty object, or wathever) when no parameters are needed, but couldn't find a way to do it with my limited knowledge.
Due to the need to return a merged object, I think this kind of interface may be desirable over individual calls for each trait.
Then, in your component you will be able to use "self" just directly, of course that includes the template:
Trait definition and code
Currently, the best approach I found is implementing a class and returning an instance in a specific structure. That alone match with the unique instance requirement, among other things.
basic-form trait:
Overriding and previous merge
Overriding a function or variable is made simply by using the same name. A "Clear" function in trait A will be overridden with a "Clear" function in trait B, as long as trait B is loaded after trait A.
As for accesing the previous merge, right now it's just a quick and dirty patch: if the instance has a "previous" property, it will be set with the previous merge reference.
Obivously this should be made in a better way, but at the time I needed it "NOW".
schema-validated-form trait:
Technicalities on current implementation
The current implementation works with a Vite plugin, which is run before the Vue compiler.
In reality, what it does is transform the defineTraits macro, generate some unique names for each loaded trait, and setup the main merger function along the "Apply" functions for each trait. The plugin will also merge the props, emits and exposed with their corresponding defineProps, defineEmits and defineExpose macros, or create them if they do not exist.
After that, since the new code injected is valid Vue code, the Vue compiler does the rest.
Currently the main limitation I felt is that the merging of traits is done in runtime. I feel that there's probably room to make it work at compile time, but my expertise and time available is not enough to delve that deep. Of course, the syntax sugar is another point to improve.
End, TL;DR
This is a proposal for the Vue team to consider how to build much more reusable pieces of code. Particularly code that allows also reuse props definitions, emits, exposed functions, and even allowing overriding the reused code based on the load order. Ideally while still allowing a single point of access to the functionality.
It absolutely doesn't mean that needs to be remotely similar to the interfaces and code provided, of course.
[edited] some typos and better descriptions here and there
Beta Was this translation helpful? Give feedback.
All reactions