kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
167 wiersze
13 KiB
Markdown
167 wiersze
13 KiB
Markdown
![]() |
# Using Components
|
||
|
|
||
|
We distinguish between components that are coupled with funkwhale-specific datatypes and "pure" user interface components:
|
||
|
|
||
|
- Funkwhale-specific components such as `Activity` or `AlbumCard` import from `types.ts`.
|
||
|
- Pure UI components (found in `src/components/ui`) are independent from Funkwhale. Think of `Button`, `Tabs` or `Layout`
|
||
|
|
||
|
---
|
||
|
|
||
|
[[toc]]
|
||
|
|
||
|
## Anatomy of a component file
|
||
|
|
||
|
### Imports
|
||
|
|
||
|
First, import vue features and external libraries. Add the sub-components you want to use last. Order each block of imports by alphabet to prevent commit diff noise.
|
||
|
|
||
|
### Script
|
||
|
|
||
|
Add a blank line between Imports and script. Use modern typescript-friendly features such as `defineModel` and `defineProps` [as documented in the Vue Docs](https://vuejs.org/api/sfc-script-setup.html#definemodel) instead of Macros.
|
||
|
|
||
|
### Template
|
||
|
|
||
|
If you are new to Vue, read the docs, especially [the chapter about Single-File Components](https://vuejs.org/guide/scaling-up/sfc), to get familiar.
|
||
|
|
||
|
### Style
|
||
|
|
||
|
Don't pollute the global namespace. Funkwhale compiles a single stylesheet (used in the app, the blog and the website). If you need specific styles in your component, use vue's [SFC features](https://vuejs.org/api/sfc-css-features.html#sfc-css-features) such as `module`. Vue will give you a `$style` object containing all locally defined classes.
|
||
|
|
||
|
```vue
|
||
|
<script setup>
|
||
|
import { ref } from "vue";
|
||
|
const theme = ref({
|
||
|
color: "red"
|
||
|
});
|
||
|
</script>
|
||
|
|
||
|
<style module>
|
||
|
.content {
|
||
|
color: v-bind("theme.color");
|
||
|
}
|
||
|
</style>
|
||
|
|
||
|
<template>
|
||
|
<div :class="$style.content"></div>
|
||
|
</template>
|
||
|
```
|
||
|
|
||
|
::: details Tip: Debugging styles
|
||
|
|
||
|
We have enabled [the vite feature `css.devSourcemap: true`](https://v2.vitejs.dev/config/#css-devsourcemap) so that in your browser devtools, you can trace the code responsible for module styles:
|
||
|
|
||
|
<img alt="For each class, the browser devTools will link the corresponding `<style module>` code" width="420" src="data:image/png;base64,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
|
||
|
|
||
|
:::
|
||
|
|
||
|
::: info What about the global style?
|
||
|
|
||
|
As of now, class and variable names from the global styles are not available as typescript objects. We should definitely add this feature at some point.
|
||
|
|
||
|
:::
|
||
|
|
||
|
## Using UI components in your views
|
||
|
|
||
|
```vue
|
||
|
<script setup>
|
||
|
import Alert from "~/components/ui/Alert.vue";
|
||
|
import Button from "~/components/ui/Button.vue";
|
||
|
</script>
|
||
|
|
||
|
<style module></style>
|
||
|
|
||
|
<template>
|
||
|
<Alert yellow />
|
||
|
<Button />
|
||
|
</template>
|
||
|
```
|
||
|
|
||
|
## Limitations of component props
|
||
|
|
||
|
While Vue can infer props based on a type, it will fail with mysterious errors if the type is an exclusive union or has union types as keys.
|
||
|
|
||
|
I hope this will be resolved soon so we can use this more elegant way of injecting non-trivial props with full autocomplete, 21st century style:
|
||
|
|
||
|
```vue
|
||
|
<script setup>
|
||
|
type A = 'either' | 'or'
|
||
|
type B = 'definitely'
|
||
|
type Props = { [k in `${A}-${B}`]?: true }
|
||
|
</script>
|
||
|
|
||
|
<template>
|
||
|
<Component either-definitely />
|
||
|
<Component or-definitely />
|
||
|
<Component />
|
||
|
{{ Error: <Component either /> }} {{ Error: <Component definitely /> }}
|
||
|
</template>
|
||
|
```
|
||
|
|
||
|
::: details Example
|
||
|
|
||
|
````ts
|
||
|
// Color from props
|
||
|
|
||
|
type SingleOrNoProp<T extends string> = RequireOneOrNone<Record<T, true>, T>;
|
||
|
type SingleProp<T extends string> = RequireExactlyOne<Record<T, true>, T>;
|
||
|
|
||
|
export type Props = Simplify<
|
||
|
SingleProp<Color | Default | Pastel> &
|
||
|
SingleOrNoProp<Variant> &
|
||
|
SingleOrNoProp<"interactive"> &
|
||
|
SingleOrNoProp<"raised">
|
||
|
>;
|
||
|
|
||
|
// Limit the choices:
|
||
|
|
||
|
export type ColorProps = Simplify<
|
||
|
SingleProp<Color> & SingleOrNoProp<Variant> & SingleOrNoProp<"raised">
|
||
|
>;
|
||
|
|
||
|
export type PastelProps = Simplify<
|
||
|
SingleProp<Pastel> & SingleOrNoProp<"raised">
|
||
|
>;
|
||
|
|
||
|
// Note that as of now, Vue does not support unions of props.
|
||
|
// So instead, we give it a single string:
|
||
|
|
||
|
export type ColorProp = Simplify<`${Color}${
|
||
|
| ""
|
||
|
| `-${Variant}${"" | "-raised"}`}`>;
|
||
|
export type PastelProp = Simplify<`${Pastel}${"" | "-raised"}`>;
|
||
|
|
||
|
// Using like this:
|
||
|
// type Props = {...} & { [k in ColorProp]? : true }
|
||
|
// This will also lead to runtime errors. Why?
|
||
|
|
||
|
export const isColorProp = (k: string) =>
|
||
|
!![...colors, ...defaults, ...pastels].find(k.startsWith);
|
||
|
|
||
|
console.log(true, isColorProp("primary"));
|
||
|
console.log(true, isColorProp("secondary"));
|
||
|
console.log(true, isColorProp("red"));
|
||
|
console.log(false, isColorProp("Jes"));
|
||
|
console.log(false, isColorProp("raised"));
|
||
|
|
||
|
/**
|
||
|
* Convenience function in case you want to hand over the props in the form
|
||
|
* ```
|
||
|
* <Component primary solid interactive raised >...</Component>
|
||
|
* ```
|
||
|
*
|
||
|
* @param props Any superset of type `Props`
|
||
|
* @returns the corresponding `class` object
|
||
|
*
|
||
|
* Note: Make sure to implement the necessary classes in `colors.scss`!
|
||
|
*/
|
||
|
export const colorFromProps = (props: Record<string, unknown>) =>
|
||
|
color(
|
||
|
Object.keys(props)
|
||
|
.filter(isColorProp)
|
||
|
.join(" ")
|
||
|
.replace("-", " ") as ColorSelector
|
||
|
);
|
||
|
````
|
||
|
|
||
|
:::
|