funkwhale/front/ui-docs/using-components.md

13 KiB

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 instead of Macros.

Template

If you are new to Vue, read the docs, especially the chapter about Single-File Components, 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 such as module. Vue will give you a $style object containing all locally defined classes.

<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 so that in your browser devtools, you can trace the code responsible for module styles:

For each class, the browser devTools will link the corresponding `<style module>` code

:::

::: 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

<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:

<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

// 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
  );

:::