My first finished project in 7 years!!! :)

Keff - Oct 3 '23 - - Dev Community

Hey! It's been a while since I've written anything here. It's been a weird last few months. But I'm glad to be writing again. Hope you're all doing great!

I never thought I'd say this, but I finished a side-project for the first time in my career!

Yup, that's right. In my 7-year-long career, I've NEVER, ever, finished a project, how rare.

Not because I did not have them, I have them in the hundreds. Some in GitHub, some in my hard drive, some lost to time. But one thing's for sure, they are not finished, and most of them are a big mess of code that does not know what it is.

This post is not a tutorial and it's not meant to teach anything. But it's great if you take something from it!! This is more of a log and a little story about my journey.

A little retrospective

I've always loved starting new side projects. Always striving to make something better than before, and always trying to add some cool twist. But the ambition has always been to create an elegant, clean, and sturdy piece of software. I don't care what the software is. I could easily set to build some Android app, make video games, create a compiler, make coding art, a js library, apis... I've recreated an FTP server/client, and created an HTML preprocessor (kind of), I've learned at least 15 languages, some in-depth, some not. I've done stuff with blockchain, which I regret xD

You get the point, I've done a lot of stuff.

The fact I have some sort of undiagnosed ADD or ADHD might've helped, looking at it now xD

I think one of the reasons why I always end up letting the projects aside is because they are too ambitious to do in a short amount of time. And I get bored of them quite fast.

One other reason is most times I don't have a good vision of what I want the project to be and do. I tend to add features and extra fluff without thinking. This can get messy very quickly, as features compete with one and another, priorities shift, features might require changing the original plan to fit in, making the original idea fade. And projects start to become way too big and messy, removing my motivation to keep working on them.

How have I finished the project?

Well, as I mentioned before, the main thing has been time. It's been short enough that I've not become bored of it. The second is that I had a very good vision of what I wanted, since is something I've attempted at least 3 times before and failed each time.

This time I had a very clear focus on what I wanted, and every single decision and action I took went towards that vision. Even when I steered away, which I did a few times, I soon realised and went back.

So yeah, I'm basically fucking Dennis Ritchie right here!

Kidding, of course, I'm more of a Donald Knuth guy 😂

So what is this project about?

Glad you asked, I was gonna ask you the same question! I mean, yeah, I know what it is, yes.

Okay, enough diversion for now!

Yeah, so, you know HTML right? That beautiful piece of engineering. I hate it. Not really, but yes. Not the language itself, but its syntax. Not just that, but I feel it's quite redundant, and could be simplified a lot in some ways.

So over the years, I've attempted to create some kind of tool to help me not write it.

The first thing I did was try to write an HTML pre-processor from scratch. I created a language and wrote a pre-processor to generate html from that. It kind of worked, but was a mess. I did not know what I was doing at the time, just got out of school! The project is available over on GitHub if you're curious, just don't judge too hard, I had less than a year of experience coding seriously!

Here's a little snippet of the language (it's called STML):

div#target(.bold .row)[click='doX()']
  div(.col-4)
    span 'Heyy'
  div(.col-6)
    span 'You like stones?'
Enter fullscreen mode Exit fullscreen mode

After that, I tried making frameworks that leverage the need to touch HTML but had no luck on that front.

That takes us a couple of weeks back. When I found the project mentioned before, I thought to revisit it and re-write it.

So I did, I started by taking a fresh look at the language and trying to break it. It did not take long. And the language was incomplete and had a lot of limitations. That motivated me to create another language and start from the beginning. I wrote the language and created a very rude transpiler.

While doing it I thought that the process of transpiling was kind of redundant. Why not let the user write the transpiler? Well, that's kind of incorrect, the user would just create what would be the parse or syntax tree if you will.

It would also offer the tools for creating the tree in a clean, simple and effective way. As well as offer the option to convert the tree into HTML code.

Now, that sounded quite interesting.

Let's look at an example. Take this small sample:

div#target(.bold .row)
    p 'Hello world'
Enter fullscreen mode Exit fullscreen mode

This internally would become a tree, representing the hierarchy and relations between tags. And each tag would contain information about itself.

Something like this, but more complex of course:

div { 
    id: 'target', 
    class: 'bold row', 
    children: [ 
        p { children: ['Hello world'] } 
    ] 
}
Enter fullscreen mode Exit fullscreen mode

Then I would take that tree and convert it into HTML.

In theory, if I made an API simple enough to not make it a chore to write, it could be possible to let the user just write in JavaScript or typescript instead of a custom language. It would also be easier to make and maintain.

And so it starts!

At that moment I had one vision, and I saw it. Here are the key points:

  • Simple - Easy to understand, and write (after learning a bit of course)
  • Light - API should not have much fluff, and make methods short to help keep code compact. It might look weird at first but I think you get used to it quite fast. I'll talk about it later!
  • Complete language generation - It must be able to generate any HTML you want.
  • Must do 2 things: It must do only 2 things, give the tools to build a tree representing an HTML document. And convert that tree into html, and nothing more.
  • Typed - Must be typed, IDE autocomplete everywhere that is possible
  • Tested - Extensively tested (extra)

So, the first step for me usually is, in cases like this one, to write an example of how I want the code to look. As that's a big part of the vision.

This is what I came up with initially:

const myPage = doc();

tag('data');

div().id('test').ac('container')
  .div()
  .p(['Hello ', p(['world']).ac('bold', 'red')]);
Enter fullscreen mode Exit fullscreen mode

tag creates any tag you want. div creates a div, .id() sets the tag id, .ac() sets the class of the tag, etc...

for the chained .div, the idea was that by calling .<tag_name>() in another tag, it would be added as a child.

This approach became a problem pretty fast. On one hand, it's not intuitive. Who's child is p in the example above? Well, it depends on how I code it. And that adds complexity that's not needed. But I insisted as I liked it. I tried both ways, but it did not feel simple and intuitive.

I ditched the idea of adding tags directly and decided to just pass the children in, or use .append.

const myPage = doc();

tag('data');

div([
    div().append(span()),
    p(['Hello ', p(['world']).ac('bold', 'red')])
]).id('test').ac('container');
Enter fullscreen mode Exit fullscreen mode

This felt better, more intuitive and more familiar. This is done all the time, meanwhile, the other approach was made up. But you might have noticed a little inconvenience with this approach. The tag information is after the children. Imagine HTML, where the class is on the closing tag xD.


I went off track!

At this point, I for some reason started tinkering with the idea of adding logic to the project. State, events, all that frameworky kind of stuff. I modified it to work at runtime and started messing around. It was starting to become quite weird, complex and not very useful to be honest. But somehow I regained the vision and luckily backtracked away from that. Having that defined vision made me take a step back and re-think. What does this project need to do: "create tree" -> "generate html". Not be a framework or anything else.


... time to re-think about it. This makes it weird and makes it hard to read.

The previous version was a pseudo-builder-pattern implementation to call it something. It was a class acting as a builder. That meant that I had to first create the tag and then I could add attributes.

I rewrote it to follow a correct builder pattern. But, for some reason I wanted to not have to call the builder each time you want to create a tag, those extra pair of parenthesis... This ended up working fine. But oh man, was it tricky to get working.

With parenthesis:

div().id('test').ac('container')
    .b([
        div().append(span()).b(),
        p(['Hello ', p().ac('bold', 'red').b(['world'])]).b()
    ]);
Enter fullscreen mode Exit fullscreen mode

.b() build the tag with children if passed in

Without parenthesis:

div.id('test').ac('container')
    .b([
        div.append(span()).b(),
        p.b(['Hello ', p.ac('bold', 'red').b(['world'])]).b()
    ]);
Enter fullscreen mode Exit fullscreen mode

It's a minor thing, but now it looks nice. It reads nice, and makes sense I think.

Oh, those square brackets, not needed, remove them.

So instead of receiving an array, we receive a spread of arguments. And now it looks like this:

div.id('test').as('color', 'red')
    .b(
        div.append(span()).b(),
        p.b('Hello ', p.ac('bold', 'red').b('world'))
    );
Enter fullscreen mode Exit fullscreen mode

Yup, now it looks perfect...

Just one last thing, it would just be a moment. It also seems redundant to need to call .b() every single time.

This is what I want:

div.id('test').as('color', 'red')
    .b(
        div.append(span()), // No need to call, just pass builder in
        p('Hello ', p.ac('bold', 'red').b('world')) // Call the builder directly
    );
Enter fullscreen mode Exit fullscreen mode

Now, you can call .b, not call, or call the builder directly. Gives a lot of flexibility and makes it cleaner.

This might sound trivial, but it was quite difficult to make. I had to juice out all of my JS knowledge to make it work.

How it works: Technically speaking div, span, p, and so on are instances of class TagBuilder. Which is an instance of a function. TagBuilder is callable. Every time you change something, like calling .ac(), a new TagBuilder is returned. I would've preferred this to not be the case, but I could not get it working without it.

This makes it possible to do this:

div.ac('some-class');
div(div.text("hey"), div.b(), img);
// 
Enter fullscreen mode Exit fullscreen mode

Oh, there's also another layer to this. From the start, I wanted to give some way of adding children without having to reference the parent tag. Magic!

I made it so that you can "attach" to tags. This means that whenever you create a tag, it will be created as a child of the attached tag. This makes this:

const root = div();
root.append(span("One"));
root.append(span("Two"));
root.append(span("Three"));
root.append(span("Four"));
Enter fullscreen mode Exit fullscreen mode

Into this:

const root = div();
attach(root);
span("One");
span("Two");
span("Three");
span("Four");
Enter fullscreen mode Exit fullscreen mode

All 4 spans will be added as children of root.

There's a problem though, what if we do this?:

const root = div();
attach(root);
span("One");
span("Two", span("Three"));
Enter fullscreen mode Exit fullscreen mode

Where does span three end up? Well, both as a child of span two and the root div. That's not good.

This took me a bit of thinking to come up with a solution. The best I could come up with is to make attaching optional. I did not want to make it a method, or an argument. I decided to make it a getter. So the TagBuilder had another layer now.

This is how it ended up:

const root = div();
attach(root);
span.a.ca("one-class");
span.a.append("Two", span("Three"));
Enter fullscreen mode Exit fullscreen mode

.a attaches the builder to the attached tag. Then when the tag is built, it will also be added as a child of its parent.

I also added support for CSS styles (with nesting 🙌), and scripts:

style.a({
    '.wrapper': {
        background: 'black',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        ':hover': {
            background: 'red'
        }
    },
});

script.a(() => {
  const btn = document.querySelector('#button-id');
});
Enter fullscreen mode Exit fullscreen mode

The content of the script function will be added to the script tag. Did you know you can get the string representation of a function in JS? Kinda cool!

So yeah, that's it. That's Hobo! Please tell me what you think! A couple more notes left before I leave though.

Types

I wrote Hobo using typescript, for that vision of having it as typed as possible. Good decision, typing it with jsdoc would've been a pain or maybe impossible. For example when adding autocomplete for CSS values based on the property name (i.e. showing the named colors for the background-color property).

Now, hobo is mostly typed. Not all property values are included, but will be adding more as I go and PR are very welcome if you fancy it!

Benefits of this approach

Before wrapping this article, I just want to list some benefits of this approach:

  • Familiarity
    • No need to learn a custom language
  • Access to all JS features
    • Loops, conditionals, etc...
    • If you need 3 divs, just use a for loop, or some es6 feature.
  • Ease of development and maintenance
    • If I want to change something or add a new feature, I don't need to modify my custom language
  • Easy to extend and customize
    • The user can extend and customize it

What I've taken from this?

I learned a few things from this. Some about me, some about development in general.

I've learned that I have a propensity to deviate from the plan and vision, which in the end makes me ditch the projects. I also realized that I'm a bit of a perfectionist, even though it's very hard for me to make something perfect as I get bored quite easily. I should prefer short projects I can make in a week or less.

I also realized something that might seem obvious to some of you, but not to me until now. Development is about focus, vision and realizing when you got off track and steer back.
Know what your project is about, and try to stick to it. Don't be afraid to experiment, but realize when something does not fit in or would make the original idea fail, and don't be afraid to throw code away if it does not fit in. I know it's hard to throw away code you've spent time writing, but it's better to throw it than to create a mess.

Another thing I learned is to take a step back, and really think if the current approach and idea is the best option. And try to look for simpler options that might fit the project better.

Please check Hobo out if you find it interesting

GitHub logo nombrekeff / hobo-js

Little library to generate any HTML with JavaScript/TypeScript.

Hobo.js

Welcome to Hobo. A little utility to generate html inside your js/ts code. Meant as a side-project, but after writing it I thought it might be useful to some people in some scenarios.

Who's this for?

I have no idea! I might use it some time. But if you use it, and feel like letting me know, you can either leave a star, contribute or reach out! I would be interested in knowing how it's being used, if at all!

What does it do?

Well, in essence it allows us to create html documents inside js or ts with ease. If I've not missed anything obvious, I think it's possible to generate any kind of html document. Or maybe other stuff like XML too 🤷🏻‍♂️

You can generate any tag you want. Add classes, ids, styles, and attributes. Create css and add scripts. All…




That's a wrap!

Yup, I think that will do it. Thanks for taking the time to read all that rant hehe, and let me know what you think! 👍

Further Reading

If you've enjoyed this article and want to read more stuff from me, I think you'll enjoy these:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .