Widget anatomy

What is widget?

A widget in Paperbits is a UI component consisting of a view template and view model behind it. Depending on the UI framework you use (Knockout, Vue, Angular or the other), both template syntax and view model implementation may vary.

In the simplest form the parts of a widget would look like the following:

HTML (template)
<h1>My component</h1>
Typescript (view model)
class MyComponent { }

... and used like this:

<my-component></my-component>

@Component decorator

Besides binding a view model to its template, this decorator simplifies registration of a component in UI framework. The usage of the decorator is straightforward and looks similarly for different UI frameworks:

@Component({
selector: "my-component",
template: "<h1>My component</h1>"
})
export class MyComponent {
constructor(someService: SomeService) { ... }
}

The only difference is the path from where it gets imported:

Knockout
import { Component } from "@paperbits/core/ko/component";
Vue
import { Component } from "@paperbits/core/vue/component";
Angular
import { Component } from "@angular/core/component";

Lifecycle hooks

Now, when you know how to declare a component, it’s good to know about a few hooks responsible for the lifecycle of the component.

Initialization methods

They are invoked right after a component instance was created. All the input parameters (see below) are available at this time.

Knockout
@OnMounted()
public async initialize(): Promise<void> {
// Your initialization code
}
Vue
@OnMounted()
public async initialize(): Promise<void> {
// Your initialization code
}
Angular
public async ngOnInit(): Promise<void> {
// Your initialization code
}

Input parameters

This is data passed into a component from its host.

Knockout
@Param()
public message: string;

Binding the parameter:

<component [message]="message"></component>
Vue
@Prop()
public message: string;

Binding the parameter:

<component v-bind:message="message"></component>
Angular
@Input()
public message: string;

Binding the parameter:

<component params="onChange: doSomething"></component>

Events

These are callbacks used to notify the component host when needed.

Knockout
@Event()
public onChange: (model: ButtonModel) => void;

Emitting the event:

this.onChange(model);

Subscribing for the event:

<component v-on:onChange="doSomething"></component>
Vue
@Event()
public onChange: (model: ButtonModel) => void;

Emitting the event:

this.onChange(model);

Subscribing for the event:

<component v-on:onChange="doSomething"></component>
Angular
@Output()
public onChange: EventEmitter<ButtonModel>;

Emitting the event:

this.onChange.emit(model);

Subscribing for the event:

<component (onChange)="doSomething()"></component>

Cleanup methods

They are invoked right before component instance gets destroyed.

Knockout
@OnDestroyed()
public async dispose(): Promise<void> {
// Your cleanup code
}
Vue
@OnDestroyed()
public async dispose(): Promise<void> {
// Your cleanup code
}
Angular
public async ngOnDestroy(): Promise<void> {
// Your cleanup code
}

Dependency injection

For Inversion of Control (IoC), by default, we use InversifyJS which is greatly suitable for applications powered by TypeScript. Though, you're welcome to replace it with any other you like.

If you went through "Getting started" tutorial, you have probably seen in startup.design.ts file how we import and instantiate a container:

@import { InversifyInjector } from "@paperbits/common/injection";
cost injector = new InversifyInjector();

... and register components:

injector.bind("myComponent", MyComponent);

... or modules:

injector.bindModule(new CoreModule());

Then, when the container is ready, we can resolve components:

const instance = injector.resolve("myComponent");

Data contract, Model, ModelBinder, ViewModelBinder

Data contract - is an interface for a raw JSON data object that represents configuration of a widget or a service (and which gets usually stored on the backend). For example:

interface PictureContract {
/**
* Key of a permalink referencing a source of the picture.
*/
sourceKey: string;
    /**
* Caption on the picture, used also as alternative text.
*/
caption: string; // "blank"
}

Model - is a class that represents a structure fulfilled with data from one or more Contracts and other sources.

class PictureModel {
sourceKey: string;
caption: string;
}

View model - is a structure that represents the component in specific UI framework. For example:

Knockout
@Component({ selector: "picture" })
class PictureViewModel {
sourceUrl: KnockoutObservable<string>; // Note that here source key was replaced by real URL.
caption: KnockoutObservable<string>;
}

<img data-bind="attr: { src: sourceUrl, alt: caption }" />
Vue
@Component({ selector: "picture" })
class PictureViewModel {
sourceUrl: string;
caption: string;
}

<img v-bind:src="sourceUrl" v-bind:alt="caption" >

As you might have guessed, the transformation of the data contract into a model is done by ModelBinder, and model data flows into a view model through ViewModelBinder (which knows the specifics of UI framework).

Design-time, publish-time, run-time

In Paperbits there are three clearly distinguished scopes:

Design-time - This is the execution scope where components required for editing content are running.

Publish-time - The scope in which static website and resources are being generated.

Run-time - Kicks in when the published website is loaded into browser.

Components of each scope run independently and do not know of each other. They have separate DI containers, their own configurations, lifecycle, etc. They may even be powered by different UI frameworks.

In the demo project you can see respective files:

startup.[scope].ts - a script where the components get bootstrapped;

config.[scope].json - configuration for a scope;

webpack.[scope].js - webpack definition for building scoped components;

Stay in touch
Do you like it?
Start on Github

Copyright © 2022 Paperbits LLC. All rights reserved.