@jodolrui/glue
Version:
Total feature separation in Vue 3 Composition API components
427 lines (313 loc) • 11.1 kB
Markdown
# Glue (total feature separation in Vue 3 Composition API)
Glue provides total feature separation in [Vue 3 Composition API](https://v3.vuejs.org/guide/composition-api-introduction.html) components.
Glue is intended for better code organization and less scrolling.

## Installation
```
npm install @jodolrui/glue
```
## Version
* __1.0.6__ Bug fixed.
* __1.0.5__ `defineState` function.
* __1.0.4__ Typescript support in `exposed()` function.
* __1.0.3__ Add license.
* __1.0.1__ First version.
## Example of use
### The issue
This is a typical [Vue 3 Composition API](https://v3.vuejs.org/guide/composition-api-introduction.html) component with two features (`foo` and `bar`):
```js
// Foobar.vue
<template>{{ foo }} {{ bar }}</template>
<script>
import { ref } from "vue";
export default {
props: { foo: { type: String, default: "Foo" },
bar: { type: String, default: "Bar" } },
emits: ["foo", "bar"],
setup(props, context) {
let foo = ref(props.foo);
context.emit("foo");
let bar = ref(props.bar);
context.emit("bar");
return { foo, bar };
},
};
</script>
```
Notice that both features (`foo` and `bar`) appear mixed at some parts of the component:
* At `props` declaration.
* At `emits` declaration.
* Throughout `setup` function.
* At `return` statement.
### Glue solution
With Glue you can totally separate features into parts/files:
`foo.js` part/file:
```js
// foo.js
import { ref } from "vue";
export default {
props: { foo: { type: String, default: "Foo" } },
emits: ["foo"],
setup(props, context) {
let foo = ref(props.foo);
context.emit("foo");
return { foo };
},
};
```
`bar.js` part/file:
```js
// bar.js
import { ref } from "vue";
export default {
props: { bar: { type: String, default: "Bar" } },
emits: ["bar"],
setup(props, context) {
let bar = ref(props.bar);
context.emit("bar");
return { bar };
},
};
```
Note that each part/file is written in normal [Vue 3 Composition API](https://v3.vuejs.org/guide/composition-api-introduction.html) syntax, so you don't have to learn anything new to create them.
Finally, parts must to be assembled with function `compose` in `Foobar.vue` component file:
```html
<!-- Foobar.vue -->
<template>{{ foo }} {{ bar }}</template>
<script>
import { compose } from "@jodolrui/glue";
import foo from "./foo";
import bar from "./bar";
export default compose("Foobar", [foo, bar]);
</script>
```
Function `compose` takes two parameters:
* The `name` of the component (i.e. `"Foobar"`).
* An array of `parts` (i.e. `[foo, bar]`).
__Warning:__ Order of parts in array is very important because it defines order of execution.
You have to import `Foobar.vue` file from your parent component as usual in order to use it:
```js
import Foobar from './Foobar.vue'
// ...
<Foobar />
```
## Exposing variables and functions to the template
### The issue
This is a typical declaration of a variable exposed to the `<template>` in a [Vue 3 Composition API](https://v3.vuejs.org/guide/composition-api-introduction.html) component:
```js
import { ref } from "vue";
export default {
setup() {
let foo = ref("bar");
return { foo };
},
};
```
Notice that you have to:
* Declare the variable (`let foo`) and assign it a value (`= ref("bar")`)
* `return` a literal object containing the variable (`return { foo }`)
### Glue solution
Glue function `expose` allows you to achieve the same without having to `return`:
```js
import { ref } from "vue";
import { expose } from "@jodolrui/glue";
export default {
setup() {
expose("foo", ref("bar"));
},
};
```
In this case, function `expose` takes two parameters:
* The `key` or name by which the element will be referred in the `<template>` (i.e. `"foo"`).
* The `object` of the element itself (i.e. `ref("bar")`).
Function `expose` returns the passed element itself, so you can assign it to a variable:
```js
import { expose } from "@jodolrui/glue";
// ...
const foo = expose("foo", ref("bar"));
```
### Alternative syntax
Another syntax for `expose` is:
```js
import { expose } from "@jodolrui/glue";
// ...
const foo = ref("bar");
expose({ foo });
```
In this case function `expose` takes only one parameter: a literal `object` containing elements to expose.
The advantage of this syntax is that you can expose multiple elements in one line:
```js
import { expose } from "@jodolrui/glue";
// ...
const foo = ref("bar");
const bar = () => console.log(`value is ${foo.value}`);
expose({ foo, bar });
```
Notice that function `expose` can be called throughout function `setup`, so that you can `expose` elements at the very time they are defined or immediately thereafter. This strengthens feature separation as yout don't need to put all them in a `return` statement at the end of `setup` function.
## Sharing variables and functions between parts or components
Elements exposed with Glue can be imported into another parts or components.
An exposed element like this:
```js
const foo = ref("bar");
expose({ foo });
```
or like this:
```js
const foo = ref("bar");
return { foo };
```
can be imported into another part of the same component calling function `exposed()`:
```js
import { exposed } from "@jodolrui/glue";
// ...
const { foo } = exposed();
```
or this way:
```js
import { exposed } from "@jodolrui/glue";
// ...
exposed().foo;
```
You can also import with `typescript` support this way:
```ts
import { exposed } from "@jodolrui/glue";
// ...
type Type = {
foo: Ref<string>;
}
// ...
const { foo } = exposed<Type>();
```
To import from another component `exposed` has to take one parameter:
```js
import { exposed } from "@jodolrui/glue";
// ...
const { foo } = exposed("Foobar");
```
The parameter is the `name` of the component to import from (i.e. `"Foobar"`).
It's important to know that both components must to be created with function `compose` in order this to work.
Notice that only previously exposed elements can be imported, so that order of component mounting and order of parts in the array passed to function `compose` are determining.
If you try to retrieve an nonexistent exposed element (or a misspelled one), Glue will throw an error:
> [Glue error] Unknown key 'foo' in 'exposed' function.
## Limitations on the use of functions 'expose' and 'exposed'
Function `expose` and function `exposed` referring to the same component (aka `exposed()`) only work during setup or lifecycle hooks, as they internally make use of the Vue function [`getCurrentInstance`](https://v3.vuejs.org/api/composition-api.html#getcurrentinstance), wich has such limitation. So Glue will throw an error if they are used in invalid scopes:
> [Glue error] Cannot use 'expose' in this scope.
> [Glue error] Cannot use 'exposed' in this scope.
If you need to use `expose` or `exposed()` outside setup or lifecycle hooks, you can call them on setup and use the instance instead.
## Typescript errors in *.vue file
If using Glue with typescript it is possible that your IDE show errors in the `*.vue` file indicating that variables you exposed to the `<template>` are unknown. This issue doesn't break the application, which should work correctly, but they can be annoying. To avoid this problem I suggest disabling typescript in the `*.vue`.
Remove `*.vue` extension from `tsconfig.json`:
```js
// "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx"]
```
Remove reference to typescript language from `*.vue` file:
```js
// <script lang="ts">
<script>
```
## Separation of html and css
If you want to separate `html` and `css` out of the `*.vue` file, you can do something like this:
```html
<!-- index.vue -->
<script src="./script.js"></script>
<style scoped src="./style.css"></style>
<template src="./template.html"></template>
```
```js
// script.js
import { compose } from "@jodolrui/glue";
import foo from "./parts/foo";
export default compose("Foo", [foo]);
```
```js
// parts/foo.js
import { ref } from "vue";
import { expose } from "@jodolrui/glue";
export default {
props: { foo: { type: String, default: "Foo" } },
emits: ["foo"],
setup(props, context) {
expose("foo", ref(props.foo));
context.emit("foo");
},
};
```
```html
<!-- template.html -->
<p>{{ foo }}</p>
```
```css
/* type.css */
* {
color: black;
}
```
## Using with `<script setup>`
You can use `glue` with [`<script setup>`](https://vuejs.org/api/sfc-script-setup.html) syntactic sugar.
You only have to put your parts into `*.vue` files within a `<script setup>` tag and then assemble them with `compose` function as usual.
Here you have an example:
`foo.vue` part/file:
```html
<!-- foo.vue -->
<script setup>
import { ref } from "vue";
let foo = ref("Foo");
</script>
```
`bar.vue` part/file:
```html
<!-- bar.vue -->
<script setup>
import { ref } from "vue";
import { exposed } from "@jodolrui/glue";
let bar = ref("Bar");
let { foo } = exposed();
let foobar = ref(foo.value + bar.value);
</script>
```
Assemble with function `compose` in `Foobar.vue` component file:
```html
<!-- Foobar.vue -->
<template>{{foo}} + {{bar}} = {{ foobar }}</template> <!-- prints "Foo + Bar = FooBar" -->
<script>
import { compose } from "@jodolrui/glue";
import foo from "./foo.vue"; // don't forget .vue extension
import bar from "./bar.vue"; // don't forget .vue extension
export default compose("Foobar", [foo, bar]);
</script>
```
Note that using `<script setup>` you don't need to use `expose` function.
## Centralizing component state with 'defineState'
Function `defineState` allows you to centralize component state declaration and preseting in one place and use it with `typescript` types in all your component parts.
You have to declare and preset your state variables creating a `useState` function this way:
```js
// state.ts
import { defineState } from "glue";
import { Ref, ref } from "vue";
export function useState() {
return defineState<{
foo: Ref<string>;
bar: Ref<string>;
}>({
foo: ref("Foo"),
bar: ref("Bar")
});
}
```
Then you can access your state calling `useState` function from any component part:
```js
// foo.js
import { ref } from "vue";
import { useState } from "../state";
export default {
props: { foo: { type: String, default: "Foo" } },
emits: ["foo"],
setup(props, context) {
const state = useState(); // state is typed
state.foo.value = props.foo;
context.emit("foo");
},
};
```
You don't need to `return`, `expose` nor `exposed` when using state centralization with `defineState` and `useState`.