With this article I want to share with you a funny idea to build UIs, that is different from the todays declarative web frontend frameworks.

tlrd; It is based on sweetalert and flow from page to page is completely in functional style. Development is very fresh and satisfying but not necessarily an alternative to established frameworks.

background history with alert

I think for many javascript developers, one of the first functions we learn to know is alert. It opens a small box with text and the user can click it away. And also most people directly learn that we should not use it to much.

But, what is that advice of best practise, *not to use alert’, worth anyway? how would it feel to have an application entirely of alert boxes?

To be honest, with just alert the alert function you can not do much. because you do not get any input from the user. We can however get some input from two friends of the alert box.

First is ‘confirm’. It let the user choose between ‘ok’ and ‘abort’. And the return value is a boolean, depending on which button was clicked. The ‘prompt’ function let the user input some text that is then returned.

With this we have in theory everything needed to build complex applications.

We can use ‘prompt’ for navigations, list out application modules and let the user choose. Maybe by typing a number.

We can let the user input data, validate the data, process it and display using na ‘alert’.

In fact, I made it, here is a complete Create-Update-Delete (CRUD) application, that let you manage user accounts.

alert app demo

Is that a good experience? No. This is where SweetAlert comes in.

application flow in the functional programming paradigm

But first, I want you to see the programming style. Everything is functional programming. There are modules and these modules are functions themselves.

The main function is a menu, presenting the user with choices in a infinite loop.

1
2
3
4
5
6
7
8
9
10
11
12
13
function main() {
while (true) {
const r = prompt('main menu\n\nselect a module:\n - users\n - tobias\n - end')
.toLowerCase().trim();

if (r === 'tobias' || r === 't') {
tobiasView();
}
if (r === 'users' || r === 'user' || r === 'u') {
usersView();
}
}
}

The aboutAuthor module shows a good fact about me.

1
2
3
4
5
6
7
function tobiasView() {
alert('Tobias is a ' + getSelfDescription() + '.')
const yes = confirm('need more info?')
if (yes) {
tobiasView();
}
}

The usersModule is also showing a menu with the CRUD operations and an option to return, its structure is similar to the main function. You also see that in this demo, our database is just going to be a javascript array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const userDB = [];

function usersView() {
while (true) {
const r = prompt('User Management\n\nselect a module:\n - list\n - create\n - update\n - delete\n - back')
.toLowerCase().trim();

if (r === 'back') {
return;
}
if (r === 'list') {
listUserView();
}
if (r === 'create') {
createUserView();
}
if (r === 'update') {
updateUserView();
}
if (r === 'delete') {
deleteUserView();
}
}
}

At the listUser view the user can see of all users. noted how I used the word view?

1
2
3
4
5
function listUserView() {
alert(`user list\n\n ${
userDB.map(u=>u.name+', '+u.email).join('\n ')
}`);
}

create Users askes the user to insert all properties, present a complete user and store the user in the db. And yes, the database is a plain old Array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createUserView() {
const name = prompt(`create user\n\n name:`);
const email = prompt(`create user\n\n email:`);
const password = prompt(`create user\n\n password:`);
const ok = confirm(`create user\n\n confirm this user:\n name: ${name},\n email: ${email},\n password: ${password}`);
if (ok) {
userDB.push({
name,
email,
password
});
alert(`user ${name} created.`);
}
}

Updating is almost the same, but it has the current value as default and only update the value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function updateUserView() {
const index = prompt('update user\n\n select the to update by typing his index number:\n\n' + userDB.map((u, i) => i + ' - ' + u.name).join('\n '));
const user = userDB[index];
if (user) {
const name = prompt(`update user\n\n name: ${user.name}`) || user.name;
const email = prompt(`update user\n\n email: ${user.email}`) || user.email;
const password = prompt(`update user\n\n password: ${user.password}`) || user.password;
const ok = confirm(`update user\n\n confirm this user update:\n name: ${name},\n email: ${email},\n password: ${password}`);
if (ok) {
user.name = name;
user.email = email;
user.password = password;
alert(`user ${name} update.`);
}
}
}

DeleteUser allow the user to remove a user from db by typing the users index.

1
2
3
4
5
6
7
8
9
10
11
function deleteUserView() {
const index = prompt('to delete a user type his index number:\n\n' + userDB.map((u, i) => i + ' - ' + u.name).join('\n '));

const user = userDB[index];
if (user) {
userDB.splice(userDB.indexOf(user), 1);
alert('the user ' + user.name + ' deleted');
} else {
alert('nobody deleted')
}
}

You get the point, right? When navigating in the application, the user is navigating through these functions. When a module is finished, the parent module can continue. And I believe this is a very interesting programming paradigm. When coding in this way, the flow within the application is very clear.

Using this paradigm with sweetalert

Sweetalert is a little library that provides a single function called swal. With it you can open a modal dialog box. Depending on the options passed in, the dialog can have a title, text, buttons or any custom content. Within a created dialog, you can show images, complete forms (instead of a single input field in prompt).

My favorite feature of the swal function however is that it returns a promise, that will resolve once the user close the modal, or clicked one of the buttons. Once the promise is resolved, values can be read from a shown form or the choice that a user made can be processed.

Now. Before I show you how to make a similar application as before, an app to organize users. I want to you experience it yourself and take a look at the swalapp.

When you open this app in a new page, you will see that it even has routing. When you navigate to the users module, the route in the browsers URL bar is /users and when you navigate to the create user page the route is /users/create. And when refreshing the browser the app can bring you right back where you left of.

Also after storing the user, it is easy to display a success screen for a very breve moment. When opening the app there is a little splash screen, that shows a picture.

Implementation with sweetAlert2

The main function again let the user choose a module. But in this solution the it also need to handle routing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

main('/', window.location.pathname.substring(1).split('/'));

/**
* The application could be part of an even bigger aplication, so it gets as first argument the `pathToMe`
* and as second parameter a split path that itself can use for routing.
*/
async function main(pathToMe: string = '', _path?: string[]) {
let [r, ...rest] = _path || [];
while (true) {
// update the URL in the window.
window.history.replaceState({}, '', pathToMe);
if (!r) {
r = await swal({
title: 'sweetalert2 app',
buttons: {
about: true,
developer: true,
users: true,
},
});
}
if (r === 'users') {
await users('/users', rest);
}
if (r === 'developer') {
await tobiasView();
}
if (r === 'about') {
await aboutPage();
}

// clear the arguments, to disable routing in the second round of the loop
r = undefined;
rest = [];
}
}

With all that routing this function look much more ugly from before, but the routing is worth it. And the core loop is basically the same. You see instead of prompt we can use sweetalerts buttons.

The aboutPage and the tobiasView, both are very similar and showing a simple message just that the tobiasView is allowing to show more texts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export async function tobiasView() {
while (true) {
const r = await swal({
title: 'developer',
text: 'Tobias is a ' + getSelfDescription() + '.',
buttons: { next: true, back: true }
});
if (r !== 'next') {
return;
}
}
}

export async function aboutPage() {
await swal({
title: 'About',
text: `This app is a demonstration for developing complete applications using the sweetalert2 library. You can read more about it at https://tnickel.de/.`,
});
}

For the users module, we again need a menu page that give access to read, create, update and delete functions. With the routing functionality this function looks similar to the main function. Each sub module is in its own function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const userDB = [];
export async function users(pathToMe: string, _path?: string[]) {
let [r, ...rest] = _path || [];
while (true) {
if (!r) {
window.history.replaceState({}, '', pathToMe);
r = await swal({
title: 'Users',
// text: 'lets go',
text: 'user crud operations',
// content: { element: 'input' },
buttons: {
list: { text: 'list', className: 'left' },
create: { text: 'create' },
update: { text: 'update' },
delete: { text: 'delete' },
back: { text: 'back' }
}
});
}
if (r === 'back' || r === null) {
return;
}
if (r === 'list') {
window.history.replaceState({}, '', pathToMe + '/list');
await listUsers();
}
if (r === 'delete') {
window.history.replaceState({}, '', pathToMe + '/delete');
await deleteUser();
}
if (r === 'create') {
window.history.replaceState({}, '', pathToMe + '/create');
await createUser();
}
if (r === 'update') {
window.history.replaceState({}, '', pathToMe + '/create');
await updateUser();
}
r = undefined;
rest = [];
}
}

The list users page, is showing the users in a unordered html list. The element function is similar to jQuery, it creates an element from html.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function listUsers(){
await swal({
title: 'listUsers',
content: element(`<ul>
${userDB.map(u => '<li>' + u.name + ', ' + u.email + '</li>').join('')}
</ul>`),
});
}

export function element(html: string) {
const p = document.createElement('div');
p.innerHTML = html;
return p.children[0] as any;
}

The createUsers page is also using the element function. This time it creates a complete form, to input name, email and password. The getFormValues function makes it very convenient to read the data form the form. This is so much better than having a separate prompt window for each field.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
async function createUser() {
const form = element(`<form>
<p><label>name:</label><input name="name"></p>
<p><label>email:</label><input name="email"></p>
<p><label>password:</label><input name="password" type="password"></p>
</form>`);

const choice = await swal({
title: 'create user',
content: form,
buttons: {
create: true, abort: true
}
});

if (choice === 'create') {
const newUser = getFormValues(form) as { name: string, email: string, password: string };
userDB.push(newUser);
await swal({ icon: "success", timer: 1800 });
}
}

export function getFormValues(element: HTMLElement) {
const inputs = element.querySelectorAll('input[name], textarea[name], select[name]') as NodeListOf<(HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)>;
const out = {} as { [x: string]: string };
inputs.forEach(input => {
out[input.getAttribute('name')] = input.value;
});
return out;
}

To delete a user, we show the user a select box where the user can choose which user to delete.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function deleteUser(){
const select = element(`<select><option value='null' selected> / </option>
${userDB.map((user, index) => `<option value='${index}'>${user.name}</option>`).join('')}
</select>`);

await swal({
title: 'delete user',
text:'select user to delete:',
content: select
});

const user = userDB[select.value];
if (user) {
userDB.splice(userDB.indexOf(user), 1);
await swal('the user ' + user.name + ' deleted');
} else {
await swal('nobody deleted');
}
}

The updateUser view is like a combination of delete and create. First let the user select a user, then show the form with the selected users data prefilled. I do not show the source here. Do you want to implement this function yourself?

How do you like this coding style? quite fun right? I really like to have to follow only a single flow throughout the application. Please note: I do not want to promote describing the views with html strings. Feel free to use react, or any other ui library to build a pages UI.

how to do better

Now I want to show you two helpers that will make developing these kind of apps much more efficient. One is to a menuFactory function that takes a configurations for routing, navigation buttons and views that can be routed to.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export function menuViewFactory(config) {
const buttons: { [x: string]: { text: string, module: ((...r: any[]) => Promise<any>) } } = {
r: { text: 'back', module: async () => undefined }
};
config.modules.forEach(m => {
buttons[m.name] = { text: m.name, module: m.module };
});
if (config.disableReturn) {
delete buttons.r;
}
return async function (pathToMe: string = '', _path?: string[]) {
let [r, ...rest] = _path || [];
while (true) {
window.history.replaceState({}, '', pathToMe);
if (!r) {
r = await swal({
title: config.title,
content: config.content,
text: config.text,
buttons,
});
}
if (r === 'r' && !config.disableReturn) {
return;
}
const b = buttons[r];
if (b) {
await b.module(pathToMe + '/' + r, rest);
}

r = undefined;
rest = [];
}
}
}

it can be used like this:

1
2
3
4
5
6
7
8
const appView = menuViewFactory({
title: 'mainMenu',
disableReturn: true,
modules: [
{ name: 'users', module: users },
{ name: 'about the developer', module: selfDescription },
]
});

This helper is ok and when working with it, I am sure we can still improve on it. For example: to make route and button text be able to be different.

The second helper that can help us to make the routing between modules possible. It consists of two parts.

  • sweetAlertApp is the entry into the app. It takes the main view of the app, and handles an error with a dedicated redirect property.
  • redirect is meant to be used for redirecting the user to a different part of the application. It does so, by throwing an error with a .redirect property so that the current views get exited and the application can route fresh to a new location.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export  async function sweetAlertApp(view: (...args: any) => any) {
showImg('/img/sweetalert.svg')
await sleep(2000);
try {
await view('/', window.location.pathname.substring(1).split('/'));
} catch (err) {
if (err.redirect) {
await view('/', err.redirect.substring(1).split('/'));
}
}
}

export function redirect(path:string){
if(!path.startsWith('/')){ throw new Error('only absolute paths allowed') }
throw { redirect: path };
}

With this, we can provide cross navigation. The user can go from the selfDescription view directly to the userList view, without navigating through the main menu and the user menu.

conclusion

Wow, if you kept reading up until this point, I guess you like the idea. I believe it is so surprisingly effective. It was really quick to bring these peaces together. At least for building application prototypes, I have never experienced anything more effective.

Contents
  1. 1. background history with alert
  • application flow in the functional programming paradigm
  • Using this paradigm with sweetalert
  • Implementation with sweetAlert2
  • how to do better
  • conclusion