Skip to content
Azarattum edited this page Nov 23, 2020 · 6 revisions

Model component defines user-independent behavior.

Models are stored in:

src
└───components
    └───app
        └───models

Dos and Don't

Models are not allowed to:

  • Depend on UI
  • Depend on non-model components
  • Have side effects

Models are allowed to:

  • Be used in other components
  • Depend on other models

Example

Example model included in the source code looks like this:

/**
 * Represents some example model
 */
export default class Example {
	public constructor() {
		///Model class construction goes here
	}
}

As you see, it is just a boilerplate code for a simple JavaScript class without any complex logic or dependencies.

A side note about TheFramework's comment style:

Comments starting from /// are used as placeholders. As a rule you should not have any /// comments in your production code because you typically use them for ///TODO: or ///FIX:.

Use Cases

A typical use case is to model some kind of structure. Anything from a user to a neural network.

User Model (Example)

A simple example of modeling a user:

import Utils from "../../common/utils.class";

/**
 * Represents a user
 */
export default class User {
	/**User's display name */
	public name: string;
	/**User's internal id */
	public id: string;

	public constructor(firstName: string, lastName: string) {
		this.name = `${firstName} ${lastName}`;
		this.id = Utils.generateID();
	}
}

Here we make use of TF's generateID function which is part of Utils class.

We can freely add to our user model any methods to further process its data.

export default class User {
	...
	public hasLastName(lastName: string): boolean {
		//Let's assume that `name` and `last name` 
		// have been already validated and do not contain any spaces.
		return this.name.split(" ")[1] === lastName;
	}
	...
}

But we should not write any logic that is not suitable for models. For example, rendering which cause side-effects and has a UI dependency:

export default class User {
	...
	/**
	 * THIS IS WRONG!
	 */
	public render(): void {
		const element = document.getElementById(`user-${this.id}`);
		if (!element) return;
		element.textContent = this.name;
	}
	...
}

We want to use a controller for that purpose instead:

import Controller from "../../common/controller.abstract";
import User from "../models/user.class";

export default class Renderer extends Controller<never>() {
	private users: User[] = [];

	public initialize(users: User[]): void {
		//Let's assume that users have been passed from the config
		this.users = users;
	}

	/**
	 * THIS IS RIGHT
	 */
	public renderUsers(): void {
		//Render all the users that we want
		this.users.forEach(user => {
			//Note that we reduced rendering to the controller's scope
			const element = this.container.querySelector(`#user-${user.id}`);
			if (!element) return;
			element.textContent = user.name;
		});
	}
}

Read more about controllers.

Language Model (Example)

Another common use case is modeling a configurable language for your application. In this example, model is used as some kind of global state, that is created once and can be passed between components later (or even upon initialization).

/**
 * A language model
 */
export default class Language {
	private langs: Record<string, Record<string, string>>;

	public constructor(lang = "en") {
		//This could be fetched from somewhere
		this.langs = {
			en: {
				hello: "Hello!",
				bye: "Goodbye..."
			},
			ru: {
				hello: "Привет!",
				bye: "Пока..."
			}
		};

		this.changeLanguage(lang);
	}

	public changeLanguage(lang: string): void {
		//Reassigns language properties right on this object
		Object.assign(this, this.langs[lang]);
	}
}

Model from the code above assigns language strings to its instance, that makes it extremely elegant to use:

const lang = new Language();
lang.hello //Hello!
lang.changeLanguage("ru");
lang.hello //Привет!

Now let's hook this up to a controller with data-binding (more on that in controller docs)

import Controller, { bind, Relation } from "../../common/controller.abstract";
import Language from "../models/language.class";

export default class Linguist extends Controller<never>(Relation.Binding) {
	@bind
	private lang: Language = new Language();
}

That already gives us access to our language strings in layout:

div(controller="linguist")
    - lang = {} //Default value, used on render time before the controller kicks in
    p=lang.hello
    p=lang.bye

Moreover, let's say we want to change our language after 5 seconds since the page load:

export default class Linguist extends Controller<never>(Relation.Binding) {
	...
	public initialize(): void {
		setTimeout(() => {
			this.lang.changeLanguage("ru");
		}, 5000);
	}
	...
}

The layout will be automatically updated thanks to data-binding!