Nice things I'm learning while creating my framework

Nice things I'm learning while creating my framework

ยท

5 min read

Currently, I've been developing a TypeScript framework for creating single-page applications (SPAs).

I decided to do it because I want to improve my knowledge of TypeScript language, create a nice open-source project, and learn how to apply some concepts that I've found in other frameworks, like Angular and React.

This challenge is helping me learn how to use a lot of things that I always wanted to understand better: decorators, template and object observers, some design patterns, command-line applications with JavaScript, and much more.

And I'm very excited to share with you some features that I'm already using in my project after learning more about them.

Sometimes, we need to know when something in DOM changes to run a proper script after that action. MutationObserver comes to help us do that.

First, we define the target element:

const target = document.getElementById('root');

Now, we create a configuration object for the things that will be observed:

const config = {
  attributes: true, // eg. "src", "class"
  childList: true, // target chidren
  characterData: true // text content
};

Finally, we define a callback function for changes, initialize MutationObserver, and start observing the target element:

// mutations handler
const callback = (mutationsList: MutationRecord[], observer: MutationObserver) => {
  // handler
}

// observer (still not observing)
const rootObserver = new MutationObserver(callback);

// start observing the target
rootObserver.observe();

It's possible to stop observing changes when the observer is no longer needed:

rootObserver.disconnect();

When an asynchronous operation inserts new components in the current view, the observer callback injects the necessary template and style code inside them.

Proxy is a mediator between the program and a target object.

See an example:

const blog = { name: "Blog do Lipe ๐Ÿ‘Œ๐Ÿป", username: "luizfilipezs" };

When we ask for the property name, our program will get it directly from the object blog, as it follows:

blog.name; // "Blog do Lipe ๐Ÿ‘Œ๐Ÿป"

We could change that behavior by using a Proxy:

const blogProxy = new Proxy(blog, {
  get: (target, name) => {
    console.log(target, name);
    return name in target ? target[name] : null;
  }
  set: (target, key, value) => {
    console.log(key, value);
    return value;
  }
});

The second parameter of the Proxy constructor is an object that handles the operations we want to control. In the example above, we changed the way properties are read and set in blog.

Now, this is what happens when we use blogProxy:

blogProxy.name; // Prints "Blog do Lipe ๐Ÿ‘Œ๐Ÿป"
blogProxy.username = "lipe"; // Prints "username" and "lipe"

I currently use this approach to control object changes and update templates in real-time.

Here I'll talk about decorators for types, but they also can be used with basic methods and even function parameters. See all documentation about decorators here.

Using decorators allows us to handle a given type before it gets its instances created.

Take a look at the example below:

@Component({
  selector: "home-component",
  template: "<p>Home component works!</p>"
})
export class HomeComponent {
  // Component logic goes here...
  foo: string = "bar";
  bar: string = "baz";
}

@Component decorator is a function. So, behind the scenes, when the code above runs, Component is called and handles the class we passed. That decorator could be written like this:

// Represents a class
type Type<T> = { new (...args: any[]): T }; 

// Things a component must have
interface ComponentProps {
  foo: string;
  bar: string;
}

// Object we pass inside the decorator function
interface ComponentConfig {
  selector?: string;
  template: string;
}

// Component decorator
export const Component = (config: ComponentConfig) =>
  <T extends Type<ComponentProps>>(type: T): T => {
    doSomething(type, config);
    return type;
  };

As you can see, a decorator is a function that returns another function, which expects a type (class) - that is automatically given - and returns it. In our example, type parameter must extend (or implement) ComponentProps. This way, we'll be sure that type contains the properties that will be read in doSomething().

Note that a decorator function doesn't run every time we initialize an object using the class we declared after it. It only runs when the code is read for the first time.

In simples words, we can say that TypeScript decorators are good to prepare the way for instances of specific object types, or just handling the types you declare.

I use decorators to store information about the components and views that will be rendered in the application. Thus I'm able to separate DOM information from component logic.

In my article 5 tips to become a better developer, one of my tips was challenge yourself. That's what I'm doing now, and I already learned a lot of cool things that are improving the quality of my applications and my ability on drawing ideas.

In future posts, I hope to share more content about things I'm learning from this and other challenges I'm delegating to myself.

Soon I'll be sharing, in this blog, the repository of my framework (currently, it's private) and you'll be able to use it, notify issues, and contribute to its development. ๐Ÿš€

Buy me a coffee