ft-client: modals and other annoying parts
intro⌗
ft-client is a client for the remote file browser ft
. If you browse through ft’s code, you would notice that it’s relatively small with tons of unit tests, many things were skipped and left for the client to handle.
The reasoning behind this is quite simple, there is no need to increase complexity on the server as it makes testing the server quite hard. The server should be reliable and relatively simple. The client doesn’t need to but could use it.
Either way, much of the complexity of ft-client
comes from how dynamic Javascript is. It is its greatest strength and weakness.
javascript is dynamic⌗
Javascript is an extremely dynamic language. For one, you cannot naturally enforce data types, any variable can change datatypes. While this is good for a lot of cases, at times it could be causing the bug that you’ve lost hair over.
Take the following request for example:
fetch("http://localhost:8080", {
body: "hello",
method: "post",
}).then(function(e) {
return e.json()
}).then(function(val) {
console.log("my value is:", val);
});
The code snippet above fetches /
at localhost:8080
. Simple enough, right? Well, yes, unless the request doesn’t return a body or returns a body that is different from the one you are counting on because an error occured in your request. You’d think this doesn’t happen often but it does.
However, Javascript being dynamic can work in your favor, such as when you want to use one of the amazing array modifying functions such as forEach
, concat
, (my favorite) map
or even filter
. Since I like to write Go and Go is pretty much the opposite of Javascript, let’s compare the two languages in the domain of array modification.
const myArr = [1, 2, 3, 4];
// 1, 4, 9, 16
function square(arr) { return arr.map(function(v) { return v * v }) }
// lessThan(myArr, 3)
// [1, 2]
function lessThan(arr, num) { return arr.filter(function(v) { return v < num }) }
// 1, 2, 3, 4, 5, 6
myArr.concat([5, 6])
myArr.forEach(function(val) {
alert(val)
});
// warning, this code is extremely verbose
func square(arr []int) []int {
ret := []int{}
for _,v := range arr {
ret = append(ret, v*v)
}
return ret
}
// lessThan
func lessThan(arr []int, n int) {
ret := []int{}
for _, v := range arr {
if v < n {
ret = append(ret, v)
}
}
return arr
}
// go's concat
append([]int{1, 2, 3, 4}, []int{5, 6})
// go's forEach
for _, v := range arr {
// this returns an error as alert isn't defined
alert(v)
}
Javascript can be rather good, don’t you think?
JSX templates⌗
JSX templates are basically embedded HTML templates mixed with Javascript code. Take a look at the following code snippet:
// instead of this ugliness
let myElem = document.createElement("h1");
myElem.innerText = "Hello world"
// you could do this
let myElem = <h1>Hello world</h1>;
I know what you’re thinking: “Oh wow, this is the best invention since sliced bread”, I have two things to say to you:
- sliced bread isn’t that cool
- no, this sucks, you’re only making HTML’s issues more apparent
UI can be defined in a much better way through S-expression that are used in Lisp-based languages. Compare the first JSX template to the second Lisp-based example and you’ll understand:
let myElem = <h1>Hello World</h1>
(defvar myElem (h1 "Hello World"))
It doesn’t show now but once you have multiple nested elements with variable attributes, the S-expression triumph over JSX complicated syntax.
function myComponent(myVariable2) {
let myElem = <h1>Hello World</h1>
return <div className={`${true ? "light" : "dark"} bg-red-500`}>
<p>{myVariable2}</p>
{myElem}
</div>
}
(defun +str (lst)
(apply #'concatenate 'string lst))
(defun myComponent (myVariable2)
(let ((myElem (h1 "Hello World")))
(div `(:className ,(+str `(,(if true "light" "dark") " bg-red-500")))
(p myVariable2)
myElem))
Add to that the amount of work that an editor has to do to filter out JSX from normal Javascript code. Also, the fact that you cannot calling functions(or better known as functional components) is a bit awkward:
function myComponent() {
return <MyOtherComponent {...{name: "lemondev", value: "bad jsx"}} />
}
Anyhoo, JSX is a lot better than normal functional Javascript when used to generate elements. At times, I resort to using good ol’ Brackets {}
for anything functional because it is a lot more concise and also it doesn’t cause me any random errors.
<p><MyComponent {...myObj} /></p>
<p>{MyComponent(myObj)}</p>
modals: an old man’s resort⌗
I hate modals but I have to use them. I hate how they look, I hate that there is no cool way to animate them, I hate how they have been used since the dawn of time; to me they’re less vintage and more decrepit.
Modals and Dialogs are synonyms for the same dumb concept. That concept is: display something over the page so that the user does something specific.
In ft client
’s case, at times, we need to select an Operation to modify, or a directory or a handful of files. Users will use the modal to go through those procedures.
I can’t help but shake the feeling that this is lazy and dishonest; not that modals are bad but there has to be a better approach. Maybe redirecting users to a different page but that’d require the server-side parameters. Strangely enough, through client-side javascript, one can decipher GET parameters and redirect to the original request. Take a look at this diagram:
/fs
-> /fs-dialog?data-name=my-path&redirect=%2Foperation-dialog%3Fdata-name=my-operation-id%26redirect=%2F
-> /operation-dialog?my-path=asd&redirect=%2Ffs
-> /fs?my-path=asd&my-operation-id=asdasd
My petty feelings aside, controlling modal logic adds complexity slowly, take a look at the following code:
export function MyComponent() {
const [ shouldShow, setShouldShow ] = useEffect(true);
return {
<Dialog {...{
show: shouldShow,
close: () => setShouldShow(!shouldShow),
}} />
}
}
With one modal, things are relatively simple. You call a function to hide or show your element. But things get more complex when you start using several modals or modals that follow up after each other.
An example of this is copying a list of files to a specific directory at a specific operation. An elegant solution of this could be through using a variable that isn’t a boolean but rather an object, an array, or a string.
export function inElegantComponent() {
const [showOp, setShowOp] = useState(boolean);
const [showFiles, setShowFiles] = useState(boolean);
return {
<div>
<OperationDialog {...{
show: showOp,
close: () => setShowOp(false),
}} />
<FilesDialog {...{
show: showFiles,
close: () => setShowFiles(false),
}} />
</div>
}
}
export function elegantComponent() {
const [dialog, setDialog] = useState("");
return {
<div>
<OperationDialog {...{
show: dialog === "operation",
close: () => setDialog(""),
}} />
<FilesDialog {...{
show: dialog === "files",
close: () => setDialog(""),
}} />
</div>
}
}
The array approach is better as it can contain specific data for each dialog.
export function evenMoreElegantComponent() {
const [dialog, setDialog] = useState([]);
return {
<div>
<OperationDialog {...{
show: dialog.length === 1,
close: () => setDialog([]),
}} />
<FilesDialog {...{
show: dialog.length === 2,
close: () => setDialog([]),
}} />
</div>
}
}
Because I haven’t really weighed either option as better, I’ll stick with the Dialog component throughout ft-client
’s source code.
global variables⌗
The framework that’s used in ft-client
, Next JS, doesn’t really allow for using global variables in an idiomatic way. To pass a global variable, one must pass them into components one by one, or use a state management framework like redux.
Luckily though, ft-client
, doesn’t use multiple views, only two. Those two views are the file system dashboard and the operations dashboard, others are components.
The reason ft-client
needs to pass global variables is to listen to events from the SSE connection. For example, a removal of a file FsRemoveEvent
should trigger a refresh in the file systems whilst OperationNew
should add that new operation to the global list of operations and so on.
one caveat: i’m bad at clients⌗
I usually write servers. There is something that speaks to me whenever I write a REST server, be it in Go, Common Lisp, PHP, whatever, backend is my preference.
However, that doesn’t mean that I should suck at clients; with every server I’ve written I’ve also written a client for it. Do note that I tend to not follow the latest trend because I am not interested half of the time.
Moreover, this is my first time using React JS and I feel a tiny amount of regret because I should’ve learned it before. It’s not as [bloated, slow, inefficient, ugly] as I thought it was, there are other alternatives I’d try because React has its flaws, but at the end of day it’s actually pretty solid.
One of these alternatives is the library snabbdom, I haven’t tried it persay but it does draw my interest as it source code contains 200 SLOC. I like tiny stuff. Previously, I’ve used Alpine JS and that has it’s own tales and stories.
conclusion⌗
Javascript comes with its annoyances but in no way is it bad. Most issues can be solved, albeit inelegantly, but solved nonetheless because Javascript’s greatest sin is its greatest virtue, which is its dynamicity.