Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/vue-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tryabby/vue": minor
---

feat(vue): add Vue integration
1 change: 1 addition & 0 deletions packages/vue/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
136 changes: 136 additions & 0 deletions packages/vue/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# @tryabby/vue

Vue 3 integration for [Abby](https://docs.tryabby.com).

## Installation

```bash
pnpm add @tryabby/vue
```

## Usage

```ts
// abby.ts
import { createAbby } from "@tryabby/vue";

export const {
AbbyProvider,
useAbby,
useFeatureFlag,
useRemoteConfig,
__abby__,
} = createAbby({
projectId: "YOUR_PROJECT_ID",
currentEnvironment: "production",
environments: ["development", "production"],
tests: {
"signup-button": {
variants: ["control", "variant-a"],
},
},
flags: ["new-dashboard"],
remoteConfig: {
headline: "String",
},
});
```

Wrap your application in the `AbbyProvider` component:

```vue
<script setup lang="ts">
import { AbbyProvider } from "./abby";
</script>

<template>
<AbbyProvider>
<RouterView />
</AbbyProvider>
</template>
```

The provider can also be passed data that was fetched on the server, which
avoids a request on the client and makes the values available during SSR:

```vue
<AbbyProvider :initial-data="abbyData">
<App />
</AbbyProvider>
```

## Composables

### `useAbby`

Returns the selected variant of an A/B test as a computed ref, plus an
`onAct` function that reports an action for the test.

```vue
<script setup lang="ts">
import { useAbby } from "./abby";

const { variant, onAct } = useAbby("signup-button");
// or map the variant to a value:
const { variant: buttonLabel } = useAbby("signup-button", {
control: "Sign up",
"variant-a": "Create account",
});
</script>

<template>
<button @click="onAct">{{ buttonLabel }}</button>
</template>
```

Variant values are rendered as an empty string on the first render to avoid
SSR hydration mismatches. Once mounted, `variant.value` contains the
selected variant.

### `useFeatureFlag`

Returns the value of a feature flag as a computed ref.

```vue
<script setup lang="ts">
import { useFeatureFlag } from "./abby";

const showNewDashboard = useFeatureFlag("new-dashboard");
</script>

<template>
<NewDashboard v-if="showNewDashboard" />
</template>
```

### `useRemoteConfig`

Returns the value of a remote config variable as a computed ref. The type of
the value is inferred from the remote config definition (`String`, `Number`
or `JSON`).

```vue
<script setup lang="ts">
import { useRemoteConfig } from "./abby";

const headline = useRemoteConfig("headline");
</script>

<template>
<h1>{{ headline }}</h1>
</template>
```

## Other exports

`createAbby` also returns the following helpers:

- `useFeatureFlags()` - a computed ref containing all flags with their values
- `useRemoteConfigVariables()` - a computed ref containing all remote config values
- `getFeatureFlagValue(name)` - read a flag outside of a component
- `getRemoteConfig(name)` - read a remote config value outside of a component
- `getABTestValue(name, lookup?)` - read a test variant outside of a component
- `getVariants(name)` - all variants of a test
- `getABResetFunction(name)` - reset the persisted variant of a test
- `updateUserProperties(user)` - update the user properties used for rules
- `__abby__` - the underlying `Abby` instance
46 changes: 46 additions & 0 deletions packages/vue/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@tryabby/vue",
"version": "0.0.0",
"description": "",
"main": "dist/index.js",
"files": ["dist"],
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"scripts": {
"build": "tsup src/",
"dev": "pnpm run build --watch",
"test": "vitest"
},
"homepage": "https://docs.tryabby.com",
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/js-cookie": "^3.0.3",
"@vue/server-renderer": "3.4.24",
"@vue/test-utils": "2.4.6",
"jsdom": "^20.0.3",
"msw": "^0.49.1",
"node-fetch": "^3.3.0",
"tsconfig": "workspace:*",
"tsup": "^6.5.0",
"typescript": "5.5.4",
"vite": "5.4.0",
"vitest": "2.0.5",
"vue": "3.4.24"
},
"peerDependencies": {
"vue": "^3.3.0"
},
"dependencies": {
"@tryabby/core": "workspace:*",
"js-cookie": "^3.0.1"
}
}
76 changes: 76 additions & 0 deletions packages/vue/src/StorageService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
type IStorageService,
type StorageServiceOptions,
getABStorageKey,
getFFStorageKey,
getRCStorageKey,
} from "@tryabby/core";
import Cookie from "js-cookie";

const DEFAULT_COOKIE_AGE = 365;

class ABStorageService implements IStorageService {
get(projectId: string, testName: string): string | null {
const retrievedValue = Cookie.get(getABStorageKey(projectId, testName));
if (!retrievedValue) return null;

return retrievedValue;
}

set(
projectId: string,
testName: string,
value: string,
options?: StorageServiceOptions
): void {
Cookie.set(getABStorageKey(projectId, testName), value, {
expires: options?.expiresInDays
? options.expiresInDays
: DEFAULT_COOKIE_AGE,
});
}

remove(projectId: string, testName: string): void {
Cookie.remove(getABStorageKey(projectId, testName));
}
}

class FFStorageService implements IStorageService {
get(projectId: string, flagName: string): string | null {
const retrievedValue = Cookie.get(getFFStorageKey(projectId, flagName));
if (!retrievedValue) return null;

return retrievedValue;
}

set(projectId: string, flagName: string, value: string): void {
Cookie.set(getFFStorageKey(projectId, flagName), value, {
expires: DEFAULT_COOKIE_AGE,
});
}

remove(projectId: string, flagName: string): void {
Cookie.remove(getFFStorageKey(projectId, flagName));
}
}

class RCStorageService implements IStorageService {
get(projectId: string, key: string): string | null {
const retrievedValue = Cookie.get(getRCStorageKey(projectId, key));
return retrievedValue ?? null;
}

set(projectId: string, key: string, value: string): void {
Cookie.set(getRCStorageKey(projectId, key), value, {
expires: DEFAULT_COOKIE_AGE,
});
}

remove(projectId: string, key: string): void {
Cookie.remove(getRCStorageKey(projectId, key));
}
}

export const TestStorageService = new ABStorageService();
export const FlagStorageService = new FFStorageService();
export const RemoteConfigStorageService = new RCStorageService();
Loading