Skip to content

Commit

Permalink
feat: add new syntax for simple usages of c-loader-status
Browse files Browse the repository at this point in the history
  • Loading branch information
ascott18 committed Jan 12, 2024
1 parent 81f9f8b commit 0b98025
Show file tree
Hide file tree
Showing 7 changed files with 436 additions and 199 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/components/Prop.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ p + .code-prop {
}
.code-prop {
margin-top: 10px;
font-weight: inherit !important;
.shiki {
margin: 0;
Expand Down
4 changes: 2 additions & 2 deletions docs/.vitepress/theme/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@
}

// Since we force all code backgrounds to dark,
// we need to set an explicit base text color
// we need to set an explicit base text color in light mode
// so that unhighlighted code blocks are readable. #356
.vp-doc div[class*="language-"] code {
html:not(.dark) .vp-doc div[class*="language-"] code {
color: var(--vp-c-bg)
}

Expand Down
87 changes: 65 additions & 22 deletions docs/stacks/vue/coalesce-vue-vuetify/components/c-loader-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ It is highly recommended that all [API Callers](/stacks/vue/layers/api-clients.m

Progress is indicated with a [Vuetify](https://vuetifyjs.com/) [v-progress-linear](https://vuetifyjs.com/en/components/progress-linear) component, and errors are displayed in a [v-alert](https://vuetifyjs.com/en/components/alerts/). [Transitions](https://vuetifyjs.com/en/styles/transitions/) are applied to smoothly fade between the different states the the caller can be in.

::: tip Note
This component uses the legacy term "loader" to refer to [API Callers](/stacks/vue/layers/api-clients.md#api-callers). A new ``c-caller-status`` component may be coming in the future with a few usability improvements - if that happens, `c-loader-status` will be preserved for backwards compatibility.
:::

## Examples

Expand All @@ -37,27 +34,26 @@ Wrap contents of a details/edit page:

Use ``c-loader-status`` to render a progress bar and any error messages, but don't use it to control content:
``` vue-html
<c-loader-status :loaders="{'': [list.$load]}" />
<c-loader-status :loaders="list.$load" />
```


Wrap a save/submit button:
``` vue-html
<c-loader-status
:loaders="{ 'no-loading-content': [person.$save] }"
>
<c-loader-status :loaders="[person.$save, person.$delete]" no-loading-content>
<button> Save </button>
<button> Delete </button>
</c-loader-status>
```

Hides the table before the first load has completed, or if loading the list encountered an error. Don't show the progress bar after we've already loaded the list for the first time (useful for loads that occur without user interaction, e.g. `setInterval`):

``` vue-html
<c-loader-status
:loaders="{
'no-secondary-progress no-initial-content no-error-content': [list.$load]
}"
#default
:loaders="list.$load"
no-initial-content
no-error-content
no-secondary-progress
>
<table>
<tr v-for="item in list.$items"> ... </tr>
Expand All @@ -67,19 +63,43 @@ Hides the table before the first load has completed, or if loading the list enco

## Props

<Prop def="loaders: { [flags: string]: ApiCaller | ApiCaller[] }" lang="ts" />
<Prop def="loaders:
// Flags per component:
| ApiCaller
| ApiCaller[]
// Flags per caller:
| { [flags: string]: ApiCaller | ApiCaller[] } " lang="ts" />

A dictionary object with entries mapping zero or more flags to one or more [API Callers](/stacks/vue/layers/api-clients.md#api-callers). Multiple entries of flags/caller pairs may be specified in the dictionary to give different behavior to different API callers.

The available flags are as follows. All flags default to `true`, and may be prefixed with ``no-`` to set the flag to ``false`` instead of ``true``. Multiple flags may be specified at once by delimiting them with spaces.
<div>

This prop has multiple options that support simple or complex usage scenarios:

#### Flags Per Component
A single instance, or array of [API Callers](/stacks/vue/layers/api-clients.md#api-callers), whose status will be represented by the component. The [flags](#flags) for these objects will be determined from the component-level [flag props](#flags-props).

``` vue-html
<c-loader-status
:loaders="[product.$load, person.$load]"
no-initial-content
no-error-content
/>
```

#### Flags Per Caller
A more advanced usage allows passing different flags for different callers. Provide a dictionary object with entries mapping zero or more [flags](#flags) to one or more [API Callers](/stacks/vue/layers/api-clients.md#api-callers). Multiple entries of flags/caller pairs may be specified in the dictionary to give different behavior to different API callers. These flags are layered on top of the base [flag props](#flags-props).

``` vue-html
<c-loader-status
:loaders="{
'no-initial-content no-error-content': [person.$load],
'no-loading-content': [person.$save, person.$delete],
}"
/>
```

</div>
<br>

| <div style="width:160px">Flag</div> | Description |
| - | - |
| `loading-content` | Controls whether the default slot is rendered while any API caller is loading (i.e. when `caller.isLoading === true`). |
| `error-content` | Controls whether the default slot is rendered while any API Caller is in an error state (i.e. when `caller.wasSuccessful === false`). |
| `initial-content` | Controls whether the default slot is rendered while any API Caller has yet to receive a response for the first time (i.e. when `caller.wasSuccessful === null`). |
| `initial-progress` | Controls whether the progress indicator is shown when an API Caller is loading for the very first time (i.e. when `caller.wasSuccessful === null`). |
| `secondary-progress` | Controls whether the progress indicator is shown when an API Caller is loading any time after its first invocation (i.e. when `caller.wasSuccessful !== null`). |

<Prop def="progressPlaceholder: boolean = true" lang="ts" />

Expand All @@ -89,6 +109,29 @@ Specify if space should be reserved for the progress indicator. If set to false,

Specifies the height in pixels of the [v-progress-linear](https://vuetifyjs.com/en/components/progress-linear) used to indicate progress.

<Prop def="
no-loading-content?: boolean;
no-error-content?: boolean;
no-initial-content?: boolean;
no-progress?: boolean;
no-initial-progress?: boolean;
no-secondary-progress?: boolean;" lang="ts" id="flags-props" />

Component level [flags](#flags) options that control behavior when the simple form of `loaders` (single instance or array) is used, as well as provide baseline defaults that can be overridden by the advanced form of `loaders` (object map) .

## Flags

The available flags are as follows, all of which default to `true`. In the object literal syntax for `loaders`, the `no-` prefix may be omitted to set the flag to `true`.

| <div style="width:160px">Flag</div> | Description |
| - | - |
| `no-loading-content` | Controls whether the default slot is rendered while any API caller is loading (i.e. when `caller.isLoading === true`). |
| `no-error-content` | Controls whether the default slot is rendered while any API Caller is in an error state (i.e. when `caller.wasSuccessful === false`). |
| `no-initial-content` | Controls whether the default slot is rendered while any API Caller has yet to receive a response for the first time (i.e. when `caller.wasSuccessful === null`). |
| `no-progress` | Master toggle for whether the progress indicator is shown in any scenario. |
| `no-initial-progress` | Controls whether the progress indicator is shown when an API Caller is loading for the very first time (i.e. when `caller.wasSuccessful === null`). |
| `no-secondary-progress` | Controls whether the progress indicator is shown when an API Caller is loading any time after its first invocation (i.e. when `caller.wasSuccessful !== null`). |

## Slots

``default`` - Accepts the content whose visibility is controlled by the state of the supplied [API Callers](/stacks/vue/layers/api-clients.md#api-callers). It will be shown or hidden according to the flags defined for each caller.
Expand Down
2 changes: 1 addition & 1 deletion docs/stacks/vue/coalesce-vue-vuetify/components/c-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- MARKER:summary -->

A table component for displaying the contents of a [ListViewModel](/stacks/vue/layers/viewmodels.md). Also supports modifying the list's [sort parameters](/modeling/model-components/data-sources.md#standard-parameters\) by clicking on column headers. Pairs well with a [c-list-pagination](/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.md).
A table component for displaying the contents of a [ListViewModel](/stacks/vue/layers/viewmodels.md). Also supports modifying the list's [sort parameters](/modeling/model-components/data-sources.md#standard-parameters) by clicking on column headers. Pairs well with a [c-list-pagination](/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.md).

<!-- MARKER:summary-end -->

Expand Down
131 changes: 96 additions & 35 deletions src/coalesce-vue-vuetify2/src/components/display/c-loader-status.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,39 @@

<script lang="ts">
import { defineComponent, PropType } from "vue";
import { ItemApiState, ListApiState } from "coalesce-vue";
import { ApiState, ItemApiState, ListApiState } from "coalesce-vue";
type AnyLoader = ItemApiState<any, any> | ListApiState<any, any>;
type AnyLoaderMaybe = AnyLoader | null | undefined;
type YesFlags = keyof Flags;
type NoFlags = `no-${YesFlags}`;
type FlagsString =
| NoFlags
| YesFlags
| ""
| `${NoFlags} ${string}`
| `${YesFlags} ${string}`;
type Camelize<S extends string> = S extends `${infer F}-${infer R}`
? `${F}${Capitalize<Camelize<R>>}`
: S;
class Flags {
progress: boolean | null = null;
initialProgress = true;
secondaryProgress = true;
loadingContent = true;
errorContent = true;
initialContent = true;
progress = true;
"initial-progress" = true;
"secondary-progress" = true;
"loading-content" = true;
"error-content" = true;
"initial-content" = true;
}
const camelizeRE = /-(\w)/g;
const camelize = (str: string): string => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ""));
};
/*
TODO: This component could use a bit of a rewrite (again).
Leave the existing component as it is for backwards compat -
Expand All @@ -78,15 +97,20 @@ class Flags {
- This is a new syntax in addition to the current flags dictionary syntax, which needs to be preserved to support advanced use cases.
*/
type LoadersProp =
| {
[flags in FlagsString]?: AnyLoaderMaybe | AnyLoaderMaybe[];
}
| AnyLoaderMaybe
| AnyLoaderMaybe[];
export default defineComponent({
name: "c-loader-status",
props: {
loaders: {
required: true,
type: Object as PropType<{
[flags: string]: AnyLoaderMaybe | AnyLoaderMaybe[];
}>,
type: Object as PropType<LoadersProp>,
},
/**
Expand All @@ -95,32 +119,67 @@ export default defineComponent({
*/
progressPlaceholder: { required: false, type: Boolean, default: true },
height: { required: false, type: [Number, String], default: 10 },
noProgress: { required: false, type: Boolean, default: null },
noInitialProgress: { required: false, type: Boolean, default: null },
noSecondaryProgress: { required: false, type: Boolean, default: null },
noLoadingContent: { required: false, type: Boolean, default: null },
noErrorContent: { required: false, type: Boolean, default: null },
noInitialContent: { required: false, type: Boolean, default: null },
},
computed: {
baselineFlags() {
const flags = new Flags();
for (const key in flags) {
const noFlag = camelize(("no-" + key) as NoFlags) as Camelize<NoFlags>;
const flagValue = this[noFlag];
if (flagValue != null) flags[key as YesFlags] = !flagValue;
}
return Object.freeze(flags);
},
loaderFlags() {
var ret = [];
for (const flagsStr in this.loaders) {
const flagsArr = flagsStr.split(" ");
const flags: any = new Flags();
for (const flagName in flags) {
const kebabFlag = flagName.replace(
/([A-Z])/g,
(m) => "-" + m.toLowerCase()
);
if (flagsArr.includes(kebabFlag)) {
flags[flagName] = true;
} else if (flagsArr.includes("no-" + kebabFlag)) {
flags[flagName] = false;
}
let loaders: LoadersProp = this.loaders;
if (Array.isArray(loaders)) {
loaders = { "": loaders };
} else if (loaders == null) {
// An attempt to pass a single loader that was nullish.
loaders = { "": [] };
} else if (loaders instanceof ApiState) {
loaders = { "": [loaders] };
}
for (const flagsStr in loaders) {
let loadersForFlags = loaders[flagsStr as keyof typeof loaders];
if (!Array.isArray(loadersForFlags)) {
loadersForFlags = [loadersForFlags];
}
let loaders = this.loaders[flagsStr];
if (!Array.isArray(loaders)) {
loaders = [loaders];
let flags: Flags = this.baselineFlags;
// Parse flags out of the object key for usages of the form:
// <CLS :loaders="{"no-loading-content secondary-progress": [vm.$load]}" />
if (flagsStr != "") {
const flagsArr = flagsStr.split(" ");
flags = { ...flags };
if (flagsArr.length) {
for (const flagName in flags) {
if (flagsArr.includes(flagName)) {
flags[flagName as YesFlags] = true;
} else if (flagsArr.includes("no-" + flagName)) {
flags[flagName as YesFlags] = false;
}
}
}
}
for (const loader of loaders) {
for (const loader of loadersForFlags) {
if (loader) {
ret.push([loader, flags as Flags] as const);
}
Expand All @@ -147,10 +206,12 @@ export default defineComponent({
const isLoading = loader.isLoading;
return (
(flags.initialProgress &&
(flags["initial-progress"] &&
isLoading &&
loader.wasSuccessful == null) ||
(flags.secondaryProgress && isLoading && loader.wasSuccessful != null)
(flags["secondary-progress"] &&
isLoading &&
loader.wasSuccessful != null)
);
});
},
Expand All @@ -161,25 +222,25 @@ export default defineComponent({
this.loaderFlags.some(
(f) =>
f[1].progress !== false &&
f[1].secondaryProgress &&
f[1].loadingContent
f[1]["secondary-progress"] &&
f[1]["loading-content"]
)
);
},
showContent() {
return this.loaderFlags.every(([loader, flags]) => {
if (!flags.loadingContent && loader.isLoading) {
if (!flags["loading-content"] && loader.isLoading) {
// loader is loading, and loading content is off.
return false;
}
if (!flags.errorContent && loader.wasSuccessful === false) {
if (!flags["error-content"] && loader.wasSuccessful === false) {
// loader has an error, and error content is off
return false;
}
if (
// initial content is off, and either:
!flags.initialContent &&
!flags["initial-content"] &&
// loader has not yet loaded
(loader.wasSuccessful == null ||
// or loader has loaded, but it errored and there's no current result
Expand Down
Loading

0 comments on commit 0b98025

Please sign in to comment.