Hi again, coders! In the first part of this short series we saw the creation and operation of a desktop application to store and encrypt our passwords made with the Wails framework. We also made a description of the Go backend and how we bind it to the frontend side.
In this part, we are going to deal with the user interface. As we stated in that post, Wails allows us to use any web framework we like, even Vanilla JS, to build our GUI. As I said, it seems that the creators of Wails have a preference for Svelte, because they always mention it as their first choice. The Wails CLI (in its current version) when we ask to create a project with Svelte+Typescript (wails init -n myproject -t svelte-ts) generates the scaffolding with Svelte3. As I already told you, if you prefer to use Svelte5 (and its new features) I have a (which I have to say that I love ❤️). But Svelte makes you fall in love from the beginning, and I have to say that it was while experimenting with Wails that I used it for the first time (and I promise to continue using it…). But as comfortable as a web framework is, we must remind backend developers that the frontend is not that easy 😀!!
But let's get to the point.
I - A look at the frontend structure
If you have used any web framework, you will quickly recognize that the Wails CLI uses
II - And now… a dive into HTML, CSS and JavaScript 🤿
/* package.json */
...
},
"dependencies": {
"svelte-copy": "^2.0.0",
"svelte-i18n": "^4.0.1",
"svelte-spa-router": "^4.0.1",
"sweetalert2": "^11.14.5"
}
...
As you can see, there are 4 JavaScript packages I've added to Svelte (apart from the already mentioned Tailwindcss+Daisyui):
svelte-copy, to make it easier to copy username and password to clipboard.
svelte-i18n, for i18n handling, i.e. allowing the user to change the application's language.
svelte-spa-router, a small routing library for Svelte, which makes it easier to change views in the application window, since it's not worth it, in this case, to use the "official" routing provided bySvelteKit.
sweetalert2, basically use it to create modals/dialog boxes easily and quickly.
The entry point of every SPA is the main.js (or main.ts) file, so let's start with that:
/* main.ts */
import { mount } from 'svelte'
import './style.css'
import App from './App.svelte'
import { addMessages, init } from "svelte-i18n"; // ⇐ ⇐
import en from './locales/en.json'; // ⇐ ⇐
import es from './locales/es.json'; // ⇐ ⇐
addMessages('en', en); // ⇐ ⇐
addMessages('es', es); // ⇐ ⇐
init({
fallbackLocale: 'en', // ⇐ ⇐
initialLocale: 'en', // ⇐ ⇐
});
const app = mount(App, {
target: document.getElementById('app')!,
})
export default app
I've highlighted the things I've added to the skeleton generated by the Wails CLI. The for more details):
<script>
import { _ } from "svelte-i18n";
</script>
<svelte:head>
<title>{$_("app_title")}</title>
</svelte:head>
You can also use sites like ). For the simple purpose of changing views in a desktop application, this library more than fulfills its purpose. With the 7 views or pages we create a dictionary (or JavaScript object) that associates routes with views. Then this dictionary is passed as props to the Router component of svelte-spa-router. It's that simple. As we will see later, through programmatic navigation or through user action we can easily change views.
The other thing is that I added a little gadget: when the user presses the Escape key the application closes (on the Settings page a tip clarifies to the user that this key closes the application). Svelte actually makes the job a lot easier, because this simple line: <svelte:window on:keydown={onKeyDown} /> catches the Keyboard event from the DOM triggering the execution of the onKeyDown function which in turn emits a Wails event (which we call "quit") which is listened to in the backend and when received there, closes the application. Since App.svelte is the component that encompasses the entire application, this is the right place to put the code for this action.
The last thing to clarify is why the HTML main tag carries Tailwind's overflow-hidden utility class. Since we're going to use an animation where components appear to enter from the right, which momentarily "increases" the width of the window, overflow-hidden prevents an ugly horizontal scrollbar from appearing.
The first view the user sees when opening the application is the Login view/page. Its logic is similar to that of a login page in any web application. Let's first look at the logic used for the views animations, because it is the same as that followed on the rest of the pages:
/* Login.svelte */
<script lang="ts">
import { onMount } from "svelte";
import { fade, fly } from "svelte/transition";
...
let mounted = false;
...
onMount(() => {
mounted = true;
...
};
...
</script>
{#if mounted}
<div
in:fly={{ x: 75, duration: 1200 }}
out:fade={{ duration: 200 }}
class="flex h-screen"
>
...
</div>
{/if}
The animation requires declaring a variable (state, let mount = false;) which is initially set to false. When the component is mounted, a lifecycle hook (onMount, similar to React) sets it to true and the animation can now begin. An entrance from the right of 1200 milliseconds duration is used (in:fly={{ x: 75, duration: 1200 }}) and a fade (out:fade={{ duration: 200 }}) of 200 milliseconds duration. Simple thanks to Svelte.
When setting up the Login view we also need to know if the user is already registered in the database or if it is the first time he/she enters the application:
/* Login.svelte */
...
let isLogin = false;
...
onMount(() => {
GetMasterPassword().then((result) => {
isLogin = result;
// console.log("Master password exists in DB:", isLogin);
});
...
};
Here we make use of GetMasterPassword which is a binding generated automatically when compiling the application and which was declared as a public method of the struct App (see the first part of this series). This function queries the database and, in case there is a master password registered in it, it considers the user as already registered (it returns a promise that wraps a boolean value), asking him to enter said password to allow him access to the rest of the views. If there is no master password in the database, the user is considered as "new" and what is asked is that he generates his own password to enter the application for the first time.
Finally, when mounting the Login.svelte component we do something that is important for the rest of the application. Although the svelte-i18n library forces us to declare the initial language code, as we have already seen, when mounting Login.svelte we ask the database (using the GetLanguage binding) to check if there is a language code saved. In case the database returns an empty string, that is, if there is no language configured as the user's preference, svelte-i18n will use the value configured as initialLocale. If instead there is a language configured, that language will be set (locale.set(result);) and the "change_titles" event will be emitted, to which the translated titles of the title bar and native dialogs of the app will be passed for the backend to handle:
/* Login.svelte */
<script lang="ts">
import { onMount } from "svelte";
...
import { _, locale } from "svelte-i18n";
import {
...
GetLanguage,
...
} from "../../wailsjs/go/main/App";
...
import { EventsEmit } from "../../wailsjs/runtime/runtime";
...
onMount(() => {
...
GetLanguage().then((result) => {
locale.set(result);
EventsEmit(
"change_titles",
$_("app_title"),
$_("select_directory"),
$_("select_file"),
);
});
...
};
...
</script>
The following is the logic for handling the login:
/* Login.svelte */
<script lang="ts">
...
import { _, locale } from "svelte-i18n";
import {
...
GetMasterPassword,
SaveMasterPassword,
} from "../../wailsjs/go/main/App";
import { push } from "svelte-spa-router";
// states and local variables
let mounted = false,
inputRef: HTMLInputElement | null = null,
isLogin = false,
show = false,
newPassword = "",
visible = false,
toast = "",
tmId1 = 0,
tmId2 = 0;
...
const onLogin = () => {
if (newPassword.length < 6 || !isAscii(newPassword)) {
toast = $_("password_too_short_or_non_ascii_chars");
visible = true;
tmId1 = setTimeout(() => {
toast = "";
visible = false;
}, 2000);
newPassword = "";
inputRef?.focus();
return;
} else if (!isLogin) {
SaveMasterPassword(newPassword).then((result) => {
// console.log("PASSWORD_ID:", result);
result ? push("/home") : false;
});
return;
}
CheckMasterPassword(newPassword).then((result) => {
if (result) {
push("/home");
} else {
toast = $_("wrong_password");
visible = true;
tmId2 = setTimeout(() => {
toast = "";
visible = false;
}, 2000);
newPassword = "";
inputRef?.focus();
}
});
};
const isAscii = (str: string): boolean => /^[\x00-\x7F]+$/.test(str);
</script>
Simply put: newPassword, the state bound to the input that gets what the user types, is first checked by onLogin to see if it has at least 6 characters and that all of them are ASCII characters, i.e. they are only 1 byte long (see the reason for that in part I of this series) by this little function const isAscii = (str: string): boolean => /^[\x00-\x7F]+$/.test(str);. If the check fails the function returns and displays a warning toast to the user. Afterwards, if there is no master password saved in the database (isLogin = false), whatever the user types is saved by the SaveMasterPassword function (a binding generated by Wails); If the promise is resolved successfully (returns a uuid string as the Id of the record stored in the database), the user is taken to the home view by the svelte-spa-router library's push method. Conversely, if the password passes the check for length and absence of non-ASCII characters and there is a master password in the DB (isLogin = true) then the CheckMasterPassword function verifies its identity against the stored one and either takes the user to the home view (promise resolved with true) or a toast is shown indicating that the entered password was incorrect.
The central view of the application and at the same time the most complex is the home view. Its HTML is actually subdivided into 3 components: a top button bar with a search input (TopActions component), a bottom button bar (BottomActions component) and a central area where the total number of saved password entries or the list of these is displayed using a scrollable window (EntriesList component):
/* Home.svelte */
<script lang="ts">
import { onMount } from "svelte";
import { fade, fly } from "svelte/transition";
import { _ } from "svelte-i18n";
import BottomActions from "../lib/BottomActions.svelte";
import TopActions from "../lib/TopActions.svelte";
import { GetPasswordCount } from "../../wailsjs/go/main/App";
import EntriesList from "../lib/EntriesList.svelte";
let mounted: boolean = false,
count: number = 0,
showList: boolean = false,
searchTerms: string = "";
onMount(() => {
mounted = true;
GetPasswordCount().then((result) => (count = result));
});
</script>
{#if mounted}
<div
in:fly={{ x: 75, duration: 1200 }}
out:fade={{ duration: 200 }}
class="flex h-screen relative"
>
<TopActions bind:isEntriesList={showList} bind:search={searchTerms} />
{#if !showList}
<h1 class="text-lg font-light m-auto">
{count} {$_("home_title")}
</h1>
{:else}
<EntriesList bind:listCounter={count} bind:search={searchTerms} />
{/if}
<BottomActions />
</div>
{/if}
Let's take a look at the TopActions and EntriesList components since they are both very closely related. And they are, especially since their props are states of the parent component. This is where that new feature of Svelte5 comes into play: runes. Both components take props declared with the $bindable rune; this means that data can also flow up from child to parent. A diagram may make it clearer:
, the view shows a link to the application repository. Obviously, in a normal web page an anchor tag (<a>) would make us navigate to the corresponding link, but in a desktop application this would not happen if Wails did not have a specific function (BrowserOpenURL) for this in its runtime:
/* About.svelte */
...
<a
onclick={() => BrowserOpenURL("https://github.com/emarifer/Nu-i-uita")}
class="text-xs font-medium hover:text-sky-500 ease-in duration-300 -m-4"
href="http://"
target="_blank"
rel="noopener noreferrer"
>
https://github.com/emarifer/Nu-i-uita
</a>
...
III - A few words about building the Wails app
If you want to build the application executable by packaging everything, including the application icon and all assets (fonts, images, etc.) just run the command:
$ wails build
This will build the binary into the build/bin folder. However, for choosing other build options or performing cross-compiling, you may want to take a look at the Wails that allows you to upload (default option) the generated artifacts to your repository.
Note that if you use the
make create-bundlescommand when running it, it will call the Wails commandswails build -clean -upx(in the case of Linux) orwails build -skipbindings -s -platform windows/amd64 -upx(in the case of Windows). The-upxflag refers to the compression of the binary using the .
I'm sure I'll see you in other posts. Happy coding 😀!!
↗ Original-Artikel auf dev.to lesenVollständiger Original-BerichtAusführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
SOCIAL SHARE CARD GENERATOR