Skip to content

Inter Communication

Work in Progress

Thee is yet a work in progress in this documentation, it means that some addiotnal content, scenarios and example are yet to be brought, nevertheless the current content cover from the basics to more elaborated scenarios.

ViewComponent and BaseService import in CDN/Lone component mode

The import (e.g. import { ViewComponent } from "@still/component/super/ViewComponent.js") of ViewComponent and BaseService classes is inexistent CDN mode setup, as this is already provided by CDN itself.


Overview

In order to provide ways from components to communicate to each other, Still.js provade with different options thereby covering all scenarios such as Parent to child, Sibling to Sibling, any component to any other(s), follow the documentation.

Initial Setup

  • Throughout these tutorials/examples the StillAppSetup (app-setup.js) will be as follow:
This is the where Application context aspects are setup. This file resides in the root folder.
import { StillAppMixin } from "./@still/component/super/AppMixin.js";
import { Components } from "./@still/setup/components.js";
import { AppTemplate } from "./app-template.js";
import { BiddingDisplay } from "./app/components/bidding/BiddingDisplay.js";

export class StillAppSetup extends StillAppMixin(Components) {

    constructor() {
        super();
        //First component to load when running the app
        this.setHomeComponent(BiddingDisplay);
        //Set the service folder path (takes place only for service injection like point 3)
        this.servicePath = 'service/';
    }

    /** Launch the components in the App container */
    async init() {
        return await AppTemplate.newApp();
    }
}


1. Parent to child change Subscription using Proxy (Pub/Sub)

Refer to StillAppSetup above example to view how the first component to load is set.

State Subscription is a way for one Component to listen to another component State changes by subcribing to it. The route.map.js file maps components location. Follow the example:

This is the parent component
import { ViewComponent } from "../../../@still/component/super/ViewComponent.js";
import { BidOffersComponent } from "./BidOffersComponent.js";

export class BiddingDisplay extends ViewComponent {

    isPublic = true;

    /** @Proxy @type { BidOffersComponent } */
    bidOfferProxy; //This is assignet in the proxy prop of the <st-element> line 14

    template = `
        <st-element 
            component="BidOffersComponent"
            proxy="bidOfferProxy"
        >
        </st-element>
        <st-element component="BiddersList"></st-element>
    `;

    /** Component Hook which takes place when it's completly render and startder */
    stAfterInit() {
        /** This is needed so parent component only try to subscribe to state 
         * after child is completly loaded */
        this.bidOfferProxy.on('load', () => {
            // This proxy represents BidOffersComponent component
            this.bidOfferProxy.offerAmmount.onChange((value) => {
                console.log(`Theres is a new offer of ${value}`);
            });
        });
    }

}
This child component has a proxy in the parent
import { ViewComponent } from "../../../@still/component/super/ViewComponent.js";

export class BidOffersComponent extends ViewComponent {

    isPublic = true;

    offerAmmount = 0;

    template = `
        <button (click)="increase()">Increase my offer</button>
        I'm willing to offer @offerAmmount.
    `;

    /* Child component updates itself, but parent classe 
     * can also call this method thereby beng able to update
     * the child value 
     * */
    increase(){
        this.offerAmmount = this.offerAmmount.value + 5;
    }

}
import { ViewComponent } from "../../../@still/component/super/ViewComponent.js";

export class BiddersList extends ViewComponent {

    isPublic = true;
    template = `
        <p>Here goes the list of bidders.</p>
    `;

}
export const stillRoutesMap = {
    viewRoutes: {
        regular: {
            BiddingDisplay: {
                path: "app/components/bidding",
                url: "/bid/display"
            },
            BidOffersComponent: {
                path: "app/components/bidding",
                url: "/bid/offer"
            },
            BiddersList: {
                path: "app/components/bidding",
                url: "/bid/bidder"
            }
        },
        lazyInitial: {}
    }
}
Project folder structure
project-root-folder
|__ @still/
|__ app/
|    |
|    |__ components/
|    |   |__ bidding/
|    |   |   |__ BiddingDisplay.js
|    |   |   |__ BidOffersComponent.js
|    |   |   |__ BiddersList.js
|    |   |   |
|__ config/
|    |__ app-setup.js
|    |__ route.map.js
|__  ...

Proxy approach consideration

Parent component can Subscribe to its childrens State through 2 means, the @Proxy way, and/or the reference (ref) way.

  • For using a proxy, the Parent component needs to create a property annotated with both @Proxy and @type annotation, and @Proxy comes first, the type will be the component class which the Proxy represents.

  • Proxy readiness is needed to subscribe to its state, in this case subscription happens when child component is fully ready, hence we're doing this.bidOfferProxy.on('load', callBackFunction) which is placed in the stAfterInit() Hook, and my callBackFunction is where I implement the state subscription itself.

  • Parent can access anything which is public from the Proxy property as shown in line 25, therefore, to subscribe to a State of the child, we only need to specify .onChange() that which recieves a closure/function, this closure recieves the new values as first parameter.

  • A component can subscribe to change to itself, therefore this is automatically done when the State is bound to the template, in addition to that, it can also be done through the .onChange() method, and this is done by using the stAfterInit() Hook as well.


2. Adjacent (sibling) components reactive communication using Reference (Pub/Sub)

Refer to StillAppSetup above example to view how the first component to load is set.

State Subscription is a way for one Component to listen to another component State changes by subcribing to it. The route.map.js file maps components location. Follow the example:

This is the parent component
import { ViewComponent } from "../../../@still/component/super/ViewComponent.js";

export class BiddingDisplay extends ViewComponent {

    isPublic = true;

    template = `
        <st-element 
            component="BidOffersComponent"
            ref="BidOffersDisplayRef"
        >
        </st-element>
        <st-element component="BiddersList"></st-element>
    `;

}
This child component has a ref stated in the parent
import { ViewComponent } from "../../../@still/component/super/ViewComponent.js";

export class BidOffersComponent extends ViewComponent {

    isPublic = true;
    totalBidders = 0;

    template = `
        Total bidders now is @totalBidders.
    `;

    /* Child component updates itself, but parent and sibling classe can 
    * also call this method thereby beng able to update the child value 
    * */
    onNewBiderEntering() {
        console.log(`New bidder enter to the list and his bid is 0 for now`);
    }

}
This child component access his sibling through the reference
import { ViewComponent } from "../../../@still/component/super/ViewComponent.js";
import { BidOffersComponent } from "./BidOffersComponent.js";

export class BiddersList extends ViewComponent {

    isPublic = true;

    /** @Prop @type { BidOffersComponent } */
    bidOfferInstance;

    template = `
        Here goes the list of bidders.
        <button (click)="addBidder()">Admit new Bidder</button>
    `;

    constructor() {
        super(); //Supper is needed to be called according to JavaScript standards

        // stWhenReady() Hook detects when the sibling component is available
        this.stWhenReady(() => {
            // Assigning the sibling instance to the property
            this.bidOfferInstance = Components.ref('BidOffersDisplayRef');
        });

    }

    addBidder() {
        // Call sibling component method
        this.bidOfferInstance.onNewBiderEntering();

        // Updating the sibling component State through the reference
        const prevTotalBidders = this.bidOfferInstance.totalBidders.value;
        this.bidOfferInstance.totalBidders = prevTotalBidders + 1;
    }

}
export const stillRoutesMap = {
    viewRoutes: {
        regular: {
            BiddingDisplay: {
                path: "app/components/bidding",
                url: "/bid/display"
            },
            BidOffersComponent: {
                path: "app/components/bidding",
                url: "/bid/offer"
            },
            BiddersList: {
                path: "app/components/bidding",
                url: "/bid/bidder"
            }
        },
        lazyInitial: {}
    }
}
Project folder structure
project-root-folder
|__ @still/
|__ app/
|    |
|    |__ components/
|    |   |__ bidding/
|    |   |   |__ BiddingDisplay.js
|    |   |   |__ BidOffersComponent.js
|    |   |   |__ BiddersList.js
|    |   |   |
|__ config/
|    |__ app-setup.js
|    |__ route.map.js
|__  ...

Reference (ref) approach consideration

  • Just like the @Proxy, reference (ref) way needs to be defined in the tag itself, which then makes it available to be accessed through the Components.ref() method as we can see in the second sibling ( BiddersList ).

  • Proxies and references are alike except for the scope they can be accessed, as Proxy only the Father can acces it (see proxy example), and the reference can be access by any component as long as it's sibling or active in the same moment, therefore, Parent component can access child through reference the same way the sibling does.


3. Global state management Reactively - Components communication with Service

Refer to StillAppSetup above example to view how the first component to load is set.

Services enable component communication without being tied to a specific component, ensuring state persistence even after a component is unloaded. The route.map.js file maps components location, follow the code example:

This is the parent component
import { ViewComponent } from "../../../@still/component/super/ViewComponent.js";
import { BiddingService } from "../../service/BiddingService.js";

export class BiddingDisplay extends ViewComponent {

    isPublic = true;

    /** Service declaration, will get injected automatically from the service
     *  path defined in the application level (StillAppSetup) in app-setup.js (last tab)
     * @Inject
     * @type { BiddingService } */
    bService;


    template = `
        <st-element component="BiddersList"></st-element>
        <st-element 
            component="BidOffersComponent"
        >
        </st-element>
    `;

    /** Component Hook which takes place when it's completly render and startder */
    stAfterInit() {
        /** This is needed so parent component only try to subscribe to state 
         * after child is completly loaded */
        this.bService.on('load', () => { //Check service readiness
            //Bellow, it Subscribe to ServiceEvent variable (countryStore)
            this.bService.countryStore.onChange(newValue => {
                console.warn(`New country entered the Bid, follow the list: `, newValue);
            });
        });

    }

}
This child component has a ref stated in the parent
import { ViewComponent } from "../../../@still/component/super/ViewComponent.js";
import { BiddingService } from "../../service/BiddingService.js";

export class BidOffersComponent extends ViewComponent {

    isPublic = true;

    /** @Inject @type { BiddingService } */
    bService;

    template = `
        <p>
            <button (click)="exitTheBid()">Exit the bidding</button>
        </p>
    `;

    exitTheBid() {
        let countryStore = this.bService.countryStore.value;
        const myCountry = 'Australia';

        // Filter out every country but Australia
        countryStore = countryStore.filter(country => country != myCountry);

        // Update the store with Australia removed from the list
        this.bService.countryStore = countryStore;
    }

}
This child component access his sibling through the reference
import { ViewComponent } from "../../../@still/component/super/ViewComponent.js";
import { BiddingService } from '../../service/BiddingService.js';

export class BiddersList extends ViewComponent {

    isPublic = true;

    /** 
     * @Inject
     * @type { BiddingService } */
    bService;

    /** Not a state but a prop, hence the annotation
     * @Prop */
    countriesList = ['Bulgaria', 'Canada', 'Denmark', 'Ethiop', 'France', 'Ghana']

    template = `
        <p>Here goes the list of bidders.</p>
        <button (click)="addMoreCountry()">Adde new country</button>
    `;

    addMoreCountry() {
        /** Retrieve and log the current/initial state */
        const countryState = this.bService.countryStore.value;
        console.log(`----> Country Store before updating: `, countryState);

        /** Get the next country from the  */
        const newCountry = this.countriesList[countryState.length - 1];
        /** Updating the store and re-assigning it to the service */
        countryState.push(newCountry);
        this.bService.countryStore = countryState;

        /** Retrieve and log the state after update from store */
        const updatedCountryState = this.bService.countryStore.value;
        console.log(`----> Country Store after updating: `, updatedCountryState);
    }
}
Service Path is defined in StillAppSetup class
1
2
3
4
5
6
7
8
import { BaseService, ServiceEvent } from "../../@still/component/super/service/BaseService.js";

export class BiddingService extends BaseService {

    /** An array with a single country is being assigner */
    countryStore = new ServiceEvent(['Australia']);

}
export const stillRoutesMap = {
    viewRoutes: {
        regular: {
            BiddingDisplay: {
                path: "app/components/bidding",
                url: "/bid/display"
            },
            BidOffersComponent: {
                path: "app/components/bidding",
                url: "/bid/offer"
            },
            BiddersList: {
                path: "app/components/bidding",
                url: "/bid/bidder"
            }
        },
        lazyInitial: {}
    }
}
Project folder structure
project-root-folder
|__ @still/
|__ app/
|    |
|    |__ components/
|    |   |__ bidding/
|    |   |   |__ BiddingDisplay.js
|    |   |   |__ BidOffersComponent.js
|    |   |   |__ BiddersList
|    |   |   |
|    |__ service/
|    |   |__ bidding/Service.js
|    |   |
|__ config/
|    |__ app-setup.js
|    |__ route.map.js
|__  ...

Still.js Service considerations

  • Services are singleton and for them to be injected we use the @Inject annotation which can also be combined with @Path annotation allowing for specification of the service file path, also, it's required to define the injection @type just like we do for a Proxy.

  • Just the same as @Proxy, Service need to be ready to be used, then again we call the the serviceName.on('load', callBackFunc), where serviceName is the variable name annotated with @Inject.

  • The concept behind the service is that it can be composed by different type of features such as store/ServiceEvent, regular state and methods (e.g. to implement API call or complex logic implementation), for this example only ServiceEvent/Store is being used.

  • ServiceEvent variable in the Service are the ones which allow for reactive behavior through the means os subscription the same way it happens with component state.


3.1. Defining Service path using @Path annotation

Refer to StillAppSetup above example to view how the first component to load is set.

Still.js offers the @Path annotation to specify the folder path of an injecting service, ensuring a more granular project structured organization. The route.map.js file maps component locations. Follow the example:

This is the parent component which subscribe to the service store
import { ViewComponent } from "../../../@still/component/super/ViewComponent.js";
import { CustomersService } from "../../service/api/CustomersService.js";

export class BiddingDisplay extends ViewComponent {

    isPublic = true;

    /** Service declaration, will get injected automatically due to Inject anottation
     *  from the specified Path path due to the annotation
     * @Inject
     * @Path service/api/
     * @type { CustomersService } */
    custService;

    template = `
        <st-element component="BiddersList"></st-element>
    `;

    /** Component Hook which takes place when it's completly render and startder */
    stAfterInit() {

        this.custService.on('load', () => { //Check service readiness
            //Bellow, it Subscribes to ServiceEvent variable (totalCustomers)
            this.custService.totalCustomers.onChange(newValue => {
                console.log(`Total customer was updated to: `, newValue);
            });
        });

    }

}
This is the parent component which subscribe to the service store
import { ViewComponent } from "../../../@still/component/super/ViewComponent.js";
import { CustomersService } from '../../service/api/CustomersService.js'

export class BiddersList extends ViewComponent {

    isPublic = true;

    /** Parameter after sertice path is infact the folder
     *  where it's (CustomersService.js) placed
     * @Inject
     * @Path service/api/
     * @type { CustomersService } */
    customerService;

    template = `
        <p>Here goes the list of bidders.</p>
        <button (click)="updateTotalCustomer()">Up customers</button>
    `;

    updateTotalCustomer() {
        const currentValue = this.customerService.totalCustomers.value;
        /** Update the value of the of the Store variable in the service */
        this.customerService.totalCustomers = currentValue + 10;
    }
}
This is the parent component which subscribe to the service store
1
2
3
4
5
6
import { BaseService, ServiceEvent } from '../../../@still/component/super/service/BaseService.js';
export class CustomersService extends BaseService {

    totalCustomers = new ServiceEvent(0);

}
export const stillRoutesMap = {
    viewRoutes: {
        regular: {
            BiddingDisplay: {
                path: "app/components/bidding",
                url: "/bid/display"
            },
            BidOffersComponent: {
                path: "app/components/bidding",
                url: "/bid/offer"
            },
            BiddersList: {
                path: "app/components/bidding",
                url: "/bid/bidder"
            }
        },
        lazyInitial: {}
    }
}
Project folder structure
project-root-folder
|__ @still/
|__ app/
|    |
|    |__ DIRcomponents
|    |   |__ bidding/
|    |   |   |__ BiddingDisplay.js
|    |   |   |__ BiddersList.js
|    |   |   |
|    |__ service/
|    |   |__ api
|    |   |   |__ CustomersService.js
|    |   |   |
|__ config/
|    |__ app-setup.js
|    |__ route.map.js
|__  ...


In addition the the different component intecommunication approached in thispage, Still.js also provide Parent to Child communication approach which allowpassing properties and methods from parent to child as explained in the Component to Component communication.