Skip to content

COMPONENT ANNOTATIONS

Overview

Since Still.js is built with vanilla JavaScript, which doesn't support annotations, it uses JavaScript comments and JSDoc to enable annotation-like capabilities. Still.js defines its own specific annotation format for this purpose.

Still.js currently supports 5 annotations, 4 of which are top-level and must appear at the top. These annotations are used for specific scenarios, allowing components to have metadata available at runtime.

Still Command Line Tool

Changes @Path from @ServicePath

If you're using Still.js version <=0.0.17 the @ServicePath annotation is named @Path.



Annotation Required complement Description Use context
@Proptop-level N/A Annotates a component variables to specify that it'll be a property and not a state.
  • Component variable which does not need reactive behavior
  • Component flags (e.g. assign true/false)
  • Internal component business logic flow
@Proxytop-level N/A Define a class property which serve as a link between Parent and a child component (embeded). JSDoc @type can complement providing type hinting.
  • Provide parent with public child methods and property/states
  • Parent-to-child communication
@Injecttop-level JSDoc @type Define a class property which serve as a link between Parent and a child component (embeded)
  • Inject the defined type (@type) as dependency
@Controllertop-level JSDoc @type Alternative to @Inject which in addition to injection adds the specified controller as the main controller of the component allowing it to be referenced as controller. in the template
  • Inject the defined type (@type) as dependency
@Path N/A Complementary to the @Inject or @Controller annotation and recieves the path of the service being injected. If used it needs to come right after @Inject or @Controller annotation
  • Specify the folder path where the service/controller is located


Examples Setup

The examples in this documentation/tutorials will be base in the bellow folder structure, Application Setup (app-setup.js) and routes metadata (route.map.js)

Project folder structure
project-root-folder
|__ @still/
|__ app/
|   |
|   |__ base-components/
|   |   |__ HomeComponent.js
|   |   |
|   |__ person/
|   |   |__ PersonForm.js
|   |   |__ PersonList.js
|   |   |
|   |__ services/
|   |   |__ MainService.js
|   |   |__ user/
|   |   |   |__ UserService.js
|   |   |   |
|__ config/
|    |__ app-setup.js
|    |__ route.map.js
|__  ...
This files stays in the project root folder
import { StillAppMixin } from "./@still/component/super/AppMixin.js";
import { Components } from "./@still/setup/components.js";
import { AppTemplate } from "./app-template.js";
import { HomeComponent } from "./app/base-components/HomeComponent.js";

export class StillAppSetup extends StillAppMixin(Components) {

    constructor() {
        super();
        // This is the service path setup
        this.setServicePath('services/')
        this.setHomeComponent(HomeComponent);
    }

    async init() {
        return await AppTemplate.newApp();
    }

}
This is the where Application context aspects are setup. This file is in the root folder.
export const stillRoutesMap = {
    viewRoutes: {
        regular: {
            HomeComponent: {
                path: "app/base-components",
                url: "/home"
            },
            PersonForm: {
                path: "app/person",
                url: "/person/register"
            },
            PersonList: {
                path: "app/person",
                url: "/person/register"
            }
        },
        lazyInitial: {}
    }
}


@Prop - Defining component property variable

This example takes into consideration the above folder structure, app and routing setup.

By default, variables in a Still.js component are reactive state variables unless it's a special one like isPublic, template, $parent, etc. If reactivity isn't needed, it's recommended to use the @Prop annotation to define a non-reactive variable instead.

This component is placed in the app/base-components/ folder
import { ViewComponent } from "../../@still/component/super/ViewComponent.js";

export class HomeComponent extends ViewComponent {

    isPublic = true;

    /** @Prop */
    retryConnectionCount = 0;

    /** @Prop */ isUserAdmin = true;

    /** @Prop */
    showDate = false;

    template = `
        <div>
            <!-- Property binding does not get reflected is changed -->
            <p>Binding User admin flag: @isUserAdmin</p>
            <button (click)="changeProp()">Update prop</button>
        </div>
    `;

    changeProp() {
        //This property is bound in line 18
        this.isUserAdmin = false;
        this.printPropNewValue();
    }

    printPropNewValue() {
        console.log(`New User Admin flag is: `, this.isUserAdmin);
    }
}

Annotations can be defined in the same line as the variable like in line 10 of previous example. Prop variables can also be bound (line 18) just as State variables, but changes to it won't reflect to binding.

Although Prop does not reactively reflect to binding, it natually have the expected reflection in the component internal flow (e.g. line 30) thereby totally suitable to be used in any business logic.


@Prop Special case of Reactive effect

This example takes into consideration the above folder structure, app and routing setup.

Prop variables are made not to behave reactively, however, when combined with (showIf) directive it will act reactively against it, follow the example with animated result right after:

This component is placed in the app/base-components/ folder
import { ViewComponent } from "../../@still/component/super/ViewComponent.js";

export class HomeComponent extends ViewComponent {

    isPublic = true;

    /** @Prop */ date = new Date().toLocaleDateString();

    /** @Prop */ showDate = false;

    template = `
        <div>
            <p>Press the button to show the date</p>
            <!-- Binding a boolean property to (showIf) directive -->
            <p (showIf)="self.showDate">Today's date is <b>@date</b></p>
            <button 
                (click)="showTheDate()"
            >Show Date</button>
        </div>
    `;

    showTheDate() {
        this.showDate = !this.showDate;
    }

}

Animated result:

Result



Using @Proxy for Parent to child components communication

This example takes into consideration the above folder structure, app and routing setup.

This is an approach provided in Still.js that enables parent-to-child components communication, in this case, parent needs to define a @Proxy for every single child, this needs to be referenced in <st-element proxy="myProxyVar"> tag. It's also nice to have the @type definition as it'll help with development experience in the intelicesnse and type hinting.

Bellow is a code sample with the animated result right after it:

This component is placed in the app/base-components/ folder
import { ViewComponent } from "../../@still/component/super/ViewComponent.js";
import { PersonForm } from "../person/PersonForm.js";
import { PersonList } from "../person/PersonList.js";

export class HomeComponent extends ViewComponent {

    isPublic = true;

    /** @Proxy @type { PersonForm } */
    personFormProxy;

    /** @Proxy @type { PersonList } */
    listPersonProxy;

    template = `
        <div>
            <p>Handle Person form from bellow buttons</p>
            <button (click)="changePersonText()">Change Person</button>
            <button (click)="callFromChild()">Call the Person</button>
            <button (click)="showMoreInList()">Handle Person List</button>
            <p>-</p>
        </div>

        <st-element 
            component="PersonForm"
            proxy="personFormProxy"
        ></st-element>

        <st-element 
            component="PersonList"
            proxy="listPersonProxy"
        ></st-element>
    `;

    changePersonText() {
        //This will update a state variable of the embeded component (PersonForm)
        this.personFormProxy.myText = `<b style="color:red;">Parent assigned value</b>`;
    }

    callFromChild() {
        //doSomething is child component method, and called througg the proxy
        this.personFormProxy.doSomething();
    }

    showMoreInList() {
        this.listPersonProxy.showMore = !this.listPersonProxy.showMore;
    }
}
This component is placed in the app/person/ folder
import { ViewComponent } from "../../@still/component/super/ViewComponent.js";

export class PersonForm extends ViewComponent {

    isPublic = true;

    //This is a state which reactively 
    //updates in case its value gets changed
    //And parent component will be changing it
    myText = 'Initial value';

    template = `
        <div>
            <p>I'm ParsonForm. My state is bound and will be updated by parent</p>
            <p>@myText</p>
        </div>
        <hr/><br/>
    `;

    doSomething() {
        this.myText = `<b style="color:green;">New value from the method</b>`;
    }
}
This component is placed in the app/person/ folder
import { ViewComponent } from "../../@still/component/super/ViewComponent.js";

export class PersonList extends ViewComponent {

    isPublic = true;
    //Parent component is changing this Prop value
    /** @Prop */ showMore = false;

    template = `
        <div>
            <p>
                I have additional content.
                <div (showIf)="self.showMore">
                    This is Person more content
                </div>
            </p>
        </div>
    `;

}

Animated result

Proxy Annotation



@Inject - Use Services for Global store and HTTP services

This example takes into consideration the above folder structure, app and routing setup.

It allows Service dependency injection offering a centralized way to handle data processing and bsiness logic. Services also provide Global Storage and are ideal for implementing HTTP calls.

Still.js uses a setter method at the application level (e.g., in app-setup.js) to define the folder for services. If services are located elsewhere, the @Path annotation can be used to speficy it in the example after this one.

This component is placed in the app/base-components/ folder
import { ViewComponent } from "../../@still/component/super/ViewComponent.js";
import { MainService } from "../services/MainService.js";

export class HomeComponent extends ViewComponent {

    isPublic = true;

    /** 
     * @Inject
     * @type { MainService } 
     * */
    serviceObj;

    template = `
        <div>
            <p>Bellow Person List is being embeded</p>
            <!-- ParsonList also injects the MainService -->
            <st-element component="PersonList"></st-element>
        </div>
    `;

    stAfterInit() {
        //Register to the service onLoad hook
        this.serviceObj.on('load', async () => {
            // Assigning the API response to the Store
            const todos = await this.serviceObj.getTodos();
            // Update the todos Store with API response
            this.serviceObj.storeTodos(todos);
        });
    }

}
This component is placed in the app/person/ folder
import { ViewComponent } from "../../@still/component/super/ViewComponent.js";
import { MainService } from "../services/MainService.js";

export class PersonList extends ViewComponent {

    isPublic = true;

    //This state will be updated with API response
    top2List = null;

    /** @Inject @type { MainService } */ servc;

    template = `
        <div>
            <p>
                <br>
                <b>The top 5 tasks are:</b>
                <hr>
                <div>@top2List</div>
            </p>
        </div>
    `;

    stAfterInit() {
        //Register to the service OnLoad hook
        this.servc.on('load', () => {
            //Assigning data from API, since this data is set by parent
            //component, it might be available once child gets rendered
            const apiResponse = this.servc.todosStore.value.slice(0, 5);

            let result = '';
            for (const user of apiResponse) {
                result += '<p>' + JSON.stringify(user) + '</p>';
            }

            this.top2List = result;

            this.servc.todosStore.onChange((newResult) => {
                //This will update the state in whenever service todosStore
                //gets upated from anywhere
                this.top2List = newResult;
            });
        });
    }

}
This service is placed in the app/services/ folder
import { $still } from "../../@still/component/manager/registror.js";
import { BaseService, ServiceEvent } from "../../@still/component/super/service/BaseService.js";

export class MainService extends BaseService {

    // This store is being accessed in the PersonList
    todosStore = new ServiceEvent([]);

    // get todos from online Fake API, Called in the HomeComponent
    async getTodos() {
        const result = await $still
            .HTTPClient
            .get('https://jsonplaceholder.typicode.com/todos');
        return result;
    }

    // Set new todos to the store. Called in the HomeComponent
    storeTodos(todos) {
        this.todosStore = todos;
    }
}

Result picture:

Result


@Controller - Sharing UI feature between components

Just like the @Inject, the @Controller annotation allow to inject a controller to a component where a special situation as well as a constraint takes place as follow:

  • Special case: The controller annotated with @Controller is marked as the main controller of the component, which can be referenced in the template for HTML event binding using controller.methodName().

  • Contraint: Only one controller can be annotate with @Controller, if more than one, the last one will take precedence. In contrast @Inject can be used more than one.


Example:

The controller
1
2
3
4
5
6
7
//The controller
export class NameOfController extends BaseController {
    getInputValue(){
        const value = document.getElementById('inputId').value;
        return value;
    }
}

Injection in the component
1
2
3
4
5
6
/**
 * @Controller
 * @Path path-to/controller-folder/
 * @type { NameOfController }
 */
 mainController;
Referencing in the component template
1
2
3
4
5
6
<div>
    <form>
        <input type="text" id="inputId">
        <button onclick="controller.getInputValue()">Get Value</button>
    </form>
</div>
Referencing in anoter component template which didn't annotate with @Controller, or didn't inject at all
1
2
3
4
<!-- This should be a child or adjacent component -->
<div>
    <button onclick="controller('NameOfController').getInputValue()">Get Value</button>
</div>

Controllers are a way for components to share features whereas the Services are for Data sharing, therefore, the use case for it is for when we have a component which can have different child or sibling, but, only the main component should inject as @Controller, other components will inject it normally or only reference in the template.



@Path - Specifying service path when @Injecting

This example takes into consideration the above folder structure, app and routing setup.

@Path is a second level annotation that might to come hence that have to come right after @Inject. It recieves the path to the folder where the service is located. Follow the code sample with the animated result:

This component is placed in the app/base-components/ folder
import { ViewComponent } from "../../@still/component/super/ViewComponent.js";
import { UserService } from "../services/user/UserService.js";

export class HomeComponent extends ViewComponent {

    isPublic = true;

    /** 
     * @Inject
     * @Path services/user/
     * @type { UserService } 
     * */
    serviceObj;

    //This prop is bound to the form input (line 21)
    searchParam;

    template = `
        <div style="padding: 10px;">
            <form onsubmit="return false;">
                <input (value)="searchParam" placeholder="Type the user ID">
                <button (click)="searchUser()">Search</button>
            </form>
            <st-element component="PersonList"></st-element>
        </div>
    `;

    async searchUser() {
        const userId = this.searchParam.value;
        const user = await this.serviceObj.getUserById(userId);
        console.log(user);
    }
}
This component is placed in the app/person/ folder
import { ViewComponent } from "../../@still/component/super/ViewComponent.js";
import { UserService } from "../services/user/UserService.js";

export class PersonList extends ViewComponent {

    isPublic = true;

    //This state will be updated with API response
    foundUser;

    /** @Inject @type { UserService } */ servc;

    template = `
        <div>
            <p>
                <br>
                <b>Search result is:</b>
                <hr>
                <div>@foundUser</div>
            </p>
        </div>
    `;

    stAfterInit() {
        this.servc.on('load', () => {
            this.servc.userSearchResult.onChange((newResult) => {
                //This will update the state in whenever service todosStore
                //gets upated from anywhere (e.g. parent component)
                this.foundUser = newResult;
            });
        });
    }

}
This service is placed in the app/services/user/ folder
import { $still } from "../../../@still/component/manager/registror.js";
import { BaseService, ServiceEvent } from "../../../@still/component/super/service/BaseService.js";

export class UserService extends BaseService {

    userSearchResult = new ServiceEvent();

    // This is being called by the HomeComponent, and
    // because the store gets updated, PersonLis get nofitied
    async getUserById(userId) {
        const url = 'https://jsonplaceholder.typicode.com/users/' + userId;
        const user = await $still.HTTPClient.get(url);
        this.userSearchResult = `Name: ${user.name} - email: ${user.email}`;
    }
}

Animated Result:

Result