Vue.js Composition
At this point, we have to put it all this together. From a container component in our application we will provide services to its children.
Our main component will be the place where we bootstrap the dependencies expected by its children.
We already declared and implemented the following.
Abstractions
We have 3 types of dependencies that can be injected:
StorageUseCase
: Actually not injected, but provides a generic type for other abstract classesLocalStorageUseCase
: Provides a type for local storage.FileSaveUseCase
: Provides a type for local file saving.
Implementations
On top of these types we wrote 2 concrete classes:
LocalStorage
: Will actually store data in browser'slocalStorage
. It is found by children withLocalStorageUseCase
keyFileSaver
: Will save content fromlocalStorage
in a local file. It is found by children byFileSaveUseCase
key.
Provider Component
Now we need a components wrapper that will provide these functionalities to its children.
// storage-provider.component.vue
<script setup lang="ts">
import { VueDiodBuilder, VueDiodInjectable, VueDiodScope } from 'vue-diod';
import {
// Abstractions.
LocalStorageUseCase,
FileSaveUseCase,
// Implementations.
LocalStorage,
FileSaver,
} from './modules/services';
// Create a new builder.
const builder = new VueDiodBuilder();
// Define the prefix to set / get values from localStorage.
const STORAGE_PREFIX: string = 'vue-diod-example';
// Define services and the abstraction they implement.
const injectables: Array<VueDiodInjectable> = [
{
register: LocalStorageUseCase,
useFactory: () => {
// Here we use a factory to be able tp set our
// prefix in the constructor.
return new LocalStorage(STORAGE_PREFIX);
},
// As browser's 'localStorage' is unique and LocalStorage service keeps
// values in cache, we want to get a singleton when injecting this
// service.
scope: VueDiodScope.Singleton,
},
// This will be passed to LocalStorage 'save' method.
{ register: FileSaveUseCase, use: FileSaver },
];
// Bootstrap and build the dependency container.
builder.bootstrap({ injectables });
</script>
<template>
<!-- Here we could use multiple components. -->
<storage-service />
</template>
Injector Component(s)
Now, we have to create our components to inject the services we bootstrapped in our main provider component.
NOTA BENE
Everything is put in a single component here for simplicity, but we could have split the following component into a few ones, each with its specific functionality and injecting necessary dependencies.
<script setup lang="ts">
import type { Ref } from 'vue';
import { ref, watch } from 'vue';
import { useVueDiod } from 'vue-diod';
import { LocalStorageUseCase, FileSaveUseCase } from '../modules/services';
const { injectServiceInstance } = useVueDiod();
const localStorage = injectServiceInstance(LocalStorageUseCase);
const fileSaver = injectServiceInstance(FileSaveUseCase);
/**
* Store.
* THAT IS THE IMPORTANT PART!
*
* The 'content' value is set by tranforming the LocalStorage service's
* content from an object to an array of arrays that contains:
* - [0]: The key (string)
* - [1]: The value (string | number)
*/
const content: Ref<Array<[string, string | number]>> = ref([]);
watch(
localStorage!.content,
() => {
content.value = Object.entries(localStorage!.content.value);
},
{ immediate: true, deep: true }
);
const store = () => {
if (!disabled.value) {
// See 'key' below.
key.value = key.value.trim();
// See 'value' below.
value.value = value.value.trim();
localStorage?.set(key.value, value.value);
// We could also write...
// localStorage?.set(Symbol(key.value), value.value);
resetInputs();
}
};
const get = (storageKey: string) => {
const result: Record<string, any> | undefined =
localStorage!.get(storageKey);
alert(
`Local storage service returned\n\n${JSON.stringify(result, null, 2)}`
);
};
const remove = (storageKey: string) => {
localStorage?.remove(storageKey);
};
const save = () => {
// LocalStorage service handles itself the case FileSaver is undefined.
localStorage?.save(fileSaver);
};
/**
* END OF THE IMPORTANT PART!
* See next section for the end of the <script setup>
*/
</script>
<script setup lang="ts">
/**
* Component's state handling.
*/
const keyInputRef = ref(null);
const key: Ref<string> = ref('');
const validateKey = () => {
if (keyInputRef.value) {
const keyElement: HTMLInputElement = keyInputRef.value;
// Triggers change immediately.
key.value = keyElement.value.replace(/[^a-z0-9]/gi, '').trim();
}
};
const valueInputRef = ref(null);
const value: Ref<string> = ref('');
const validateValue = () => {
if (valueInputRef.value) {
const valueElement: HTMLInputElement = valueInputRef.value;
value.value = valueElement.value
.replace(/[^a-z 0-9]/gi, '')
.replace(/ +(?= )/g, '');
// Triggers change immediately.
valueElement.value = value.value;
}
};
const disabled = ref(true);
watch([key, value], () => {
disabled.value = key.value.length === 0 || value.value.length === 0;
});
const resetInputs = () => {
if (keyInputRef.value && valueInputRef.value) {
// Reset and blur.
const keyElement: HTMLInputElement = keyInputRef.value;
const valueElement: HTMLInputElement = valueInputRef.value;
key.value = '';
value.value = '';
keyElement.value = key.value;
valueElement.value = value.value;
keyElement.blur();
valueElement.blur();
}
};
<template>
<template>
<div class="container">
<div>
<span style="font-weight: bold">Prefix: </span>
<span>{{ localStorage?.prefix }}</span>
</div>
<div class="form">
<div>
<input
ref="keyInputRef"
type="text"
placeholder="Enter the key"
class="item"
@input="validateKey"
@keyup.enter="store"
/>
<input
ref="valueInputRef"
type="text"
placeholder="Enter the value"
class="item"
@input="validateValue"
@keyup.enter="store"
/>
<button
:disabled="key.length == 0 || value.length == 0"
class="item"
:style="{
backgroundColor: disabled ? 'grey' : '',
borderColor: disabled ? 'darkgrey' : '',
}"
@mouseup="store"
>
Store
</div>
</div>
</div>
<div
style="
align-self: center;
width: 100%;
border-top: 1px solid var(--vp-c-divider);
margin-top: 24px;
"
>
<table>
<thead>
<th>Key</th>
<th>Value</th>
</thead>
<tbody>
<template v-if="content.length > 0">
<tr v-for="entry in content">
<td style="font-weight: 600">
<div
style="
display: flex;
align-items: center;
width: 100%;
flex-wrap: wrap;
"
>
<div>
{{ entry[0] }}
</div>
<div style="margin-left: auto">
<button @mouseup="get(entry[0])">Get</button>
</div>
</div>
</td>
<td>
<div
style="
display: flex;
align-items: center;
flex-grow: 1;
width: 100%;
flex-wrap: wrap;
"
>
<div>{{ entry[1] }}</div>
<div style="margin-left: auto">
<button @mouseup="remove(entry[0])">Remove</button>
</div>
</div>
</td>
</tr>
</template>
<template v-else>
<tr style="width: 100%">
<td
colspan="2"
style="text-align: center"
>
Nothing stored yet
</td>
</tr></template
>
</tbody>
</table>
</div>
<div>
<button @mouseup="save">Save to File</button>
</div>
</div>
</template>
<style>
<style scoped>
.container {
display: flex;
flex-direction: column;
}
.form {
display: flex;
}
.form .item {
margin-top: 24px;
}
input {
border: 1px solid var(--vp-c-text-1);
border-radius: 0.5rem;
color: var(--vp-c-text-1);
padding: 0.2rem 0.2rem 0.2rem 1rem;
margin-right: 1rem;
}
table {
display: table;
width: 100%;
min-width: 100%;
table-layout: fixed;
overflow-wrap: break-word;
}
</style>
We now have a fully functional component expecting functionalities without knowing their concrete implementations. Obviously, if we wanted to add more it would require to modify the code of the 'injector' component... or not... 🙃
But this will be the subject of another example.