Merge pull request #15012 from ElectronicBlueberry/composables-unify

Unify Composable Naming and add Documentation
This commit is contained in:
Dannon
2022-11-17 14:54:20 -05:00
committed by GitHub
13 changed files with 126 additions and 15 deletions
+107
View File
@@ -0,0 +1,107 @@
# Composables
Composables are way of splitting up your code into distinct, reusable chunks.
They can replace providers, mixins and more. Any code you can put into a component, can also be written as a composable.
Using them effectively can make your code more reusable, decoupled, and easier to follow.
**More about Composables:**
* [Composables Overview](https://vuejs.org/guide/reusability/composables.html)
* [Composition API](https://vuejs.org/api/composition-api-setup.html)
* [\<script setup\>](https://vuejs.org/api/sfc-script-setup.html)
## Using Composables in the Composition API
Example: accessing the current user from the store
```vue
<script setup>
import { useCurrentUser } from "composables/user";
const { currentUser } = useCurrentUser();
</script>
```
You can now access the current user with `currentUser.value`.
## Using Composables in the Options API
Composables are not limited to the composition api. This is the same example from above, using the options api.
```vue
<script>
import { useCurrentUser } from "composables/user";
export default {
setup() {
const { currentUser } = useCurrentUser();
return { currentUser };
}
}
</script>
```
You can now access the current user with `this.currentUser` from anywhere within the component.
## Testing Components with Composable Stores
When writing a test which includes a component that has a composable store (like useCurrentUser),
there are two ways to test it.
### Mocking the store
You can provide the store in the mount function as follows:
```js
const wrapper = shallowMount(TestedComponent,
localVue,
provide: { store },
});
```
`store` must be a Vuex store.
The `mockModule` helper can help creating a store for the required modules:
```js
const store = new Vuex.Store({
modules: {
user: mockModule(userStore),
},
});
```
### Mocking the composable
The second option is to mock the composable:
```js
import { useCurrentUser } from "composables/user";
jest.mock("composables/user");
useCurrentUser.mockReturnValue({
currentUser: {}
});
```
While simpler in this example, you may need to manually mock more return values and composables than the other method, depending on the composables the component is using.
## Using Composables for more than Stores
Composables can be of great use to extract any reactive code from your components. For an example of this, take a look at [userFilterObjectArray](https://github.com/galaxyproject/galaxy/blob/dev/client/src/composables/utils/filter.js).
Usage:
```vue
<script setup>
import { useFilterObjectArray } from "composables/utils/filter";
const filteredArray = useFilterObjectArray(
someReactiveArray,
searchValue,
["name", "description"]
);
</script>
```
It's a simple filtering function, but fully reactive.
Whenever any of the inputs changes, the return value is re-computed, without having to call the function again.
+10 -6
View File
@@ -1,3 +1,7 @@
**Notice** Consider using [Composables](composables.md) instead of Providers. They offer more functionality and need less boilerplate.
---
We are using components in two very distinct ways. The first, "normal", kind of component will
probably look familiar to anybody whis is already passingly familiar with Vue. Here the relevant
information comes in as properties, any internal variables get defined in "data", changes go out as
@@ -27,7 +31,7 @@ restriction that Vue needs a single root element in which to render.
<template>
<AutoComplete
:options="options"
:value="doodad.category"
:value="doodad.category"
@select="saveCategory"
/>
</template>
@@ -35,7 +39,7 @@ restriction that Vue needs a single root element in which to render.
<script>
export default {
props: {
props: {
doodad: { type: Object, required: true },
options: { type: Array, required: true },
},
@@ -51,7 +55,7 @@ export default {
This component accepts a mandatory input object (doodad), lets the user play with a category prop,
then emits a fresh object after it's done. So what, what's the big deal? The important part to walk
away from this dumb example is the things that are NOT in this sample component.
away from this dumb example is the things that are NOT in this sample component.
This component doesn't save the data. This component doesn't make ajax calls, and this component
doesn't mutate its props. What it does do is to allow the user to edit some object named "doodad"
@@ -149,8 +153,8 @@ describe("A renderless component", () => {
},
});
// waits for "updated" Vue lifecycle hook to fire on the renderless
// component. This is often good enough for waiting for
// waits for "updated" Vue lifecycle hook to fire on the renderless
// component. This is often good enough for waiting for
// an initial ajax load to finish, for example
await waitForLifecyleEvent(wrapper.vm, "updated");
})
@@ -161,4 +165,4 @@ describe("A renderless component", () => {
// ...more tests
})
})
```
```
+1 -1
View File
@@ -5,7 +5,7 @@
import { computed } from "vue";
import { getAppRoot } from "onload/loadConfig";
import { useConfig } from "composables/useConfig";
import { useConfig } from "composables/config";
import UtcDate from "components/UtcDate";
import License from "components/License/License";
import ExternalLink from "components/ExternalLink";
@@ -2,9 +2,9 @@ import { mount } from "@vue/test-utils";
import { getLocalVue } from "jest/helpers";
import ToolSection from "./ToolSection";
import { useConfig } from "composables/useConfig";
import { useConfig } from "composables/config";
jest.mock("composables/useConfig");
jest.mock("composables/config");
useConfig.mockReturnValue({
config: {
toolbox_auto_sort: true,
@@ -53,7 +53,7 @@ import ToolPanelLabel from "./ToolPanelLabel";
import ariaAlert from "utils/ariaAlert";
import ToolPanelLinks from "./ToolPanelLinks";
import { useConfig } from "composables/useConfig";
import { useConfig } from "composables/config";
export default {
name: "ToolSection",
@@ -1,6 +1,6 @@
<script setup>
import ToolSource from "./ToolSource.vue";
import { useConfig } from "composables/useConfig";
import { useConfig } from "composables/config";
import { useCurrentUser } from "composables/user";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
@@ -1,7 +1,7 @@
<script setup>
import { watch } from "vue";
import { BButton } from "bootstrap-vue";
import { useTaskMonitor } from "composables/useTaskMonitor";
import { useTaskMonitor } from "composables/taskMonitor";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
const { isRunning, isCompleted, hasFailed, requestHasFailed, waitForTask } = useTaskMonitor();
@@ -6,7 +6,7 @@ import ActionButton from "./ActionButton.vue";
import StsDownloadButton from "components/StsDownloadButton.vue";
import ExportToRemoteButton from "components/Workflow/Invocation/Export/ExportToRemoteButton.vue";
import ExportToRemoteModal from "components/Workflow/Invocation/Export/ExportToRemoteModal.vue";
import { useMarkdown } from "composables/useMarkdown";
import { useMarkdown } from "composables/markdown";
import { Toast } from "composables/toast";
import axios from "axios";
@@ -1,4 +1,4 @@
import { useMarkdown } from "composables/useMarkdown";
import { useMarkdown } from "composables/markdown";
describe("useMarkdown", () => {
describe("renderMarkdown", () => {
@@ -1,7 +1,7 @@
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import flushPromises from "flush-promises";
import { useTaskMonitor } from "composables/useTaskMonitor";
import { useTaskMonitor } from "composables/taskMonitor";
const PENDING_TASK_ID = "pending-fake-task-id";
const COMPLETED_TASK_ID = "completed-fake-task-id";