3B Onboarding Portal Components

From 3B Knowledge
Jump to navigation Jump to search

Intro

The 3B Portals platform empowers developers to create custom components that seamlessly integrate into the portal-building process. By utilizing Custom Metadata definitions and Static Resources, developers can design, configure, and implement their own components with ease. This documentation provides a detailed guide on how to develop and integrate custom components within 3B Portals.

Custom Metadata Definition

Parameters

When creating a custom component, developers need to define several essential parameters within the Custom Metadata record. These parameters include:

  1. JS Resource - This parameter specifies the name of the Static Resource that contains the component's logic and dependencies. It serves as the foundation for the custom component.
  2. Web Component Tag (Optional) - Developers can opt to define a custom tag name for their component. Alternatively, they can use the "HTML" field to specify custom HTML content that will be inserted into the builder.
  3. HTML (optional)
  4. Traits JSON - This is a JSON array that maps traits to web component attributes. Each trait allows users to configure a specific attribute via the builder interface.
  5. Category - the builder components category where the component will be added to
  6. Order - An integer that defines the order in which the component will be loaded
  7. Class - a CSS class to be assigned to the created custom component

Static Resource Configuration

Index.js File

The Static Resource should contain a top-level index.js file. This file serves as the entry point and is responsible for loading all the component dependencies and defining the component's behavior. It provides developers with complete control over the component's functionality.

Recommended Approach

For optimal integration and flexibility, we strongly recommend that custom components be created as web components. To achieve this, developers should implement the "Web Component Tag" field in the Custom Metadata record when defining the component configuration. This enables the seamless inclusion of React and Vue environments within the web component, providing a cohesive and adaptable development experience.

Developers have the freedom to define custom JavaScript, import React and Vue environments, and execute code as needed within the Static Resource. This approach ensures that custom components can leverage the full power of these frameworks while being fully compatible with 3B Portals.

Global APIs

Global Object: window.globals

  1. editor (Boolean):
    • Description: This property is a boolean value that indicates whether the custom component is currently being rendered in the builder (during development) or on the published site.
  2. sessionId:
    • Description: This property contains a user session ID, which is available only in authenticated Communities and Digital Experiences.
  3. siteUrl:
    • Description: This property stores the base URL of the running site. It can be useful for constructing URLs and linking to other pages within the site.
  4. user.UserType:
    • Description: This property provides information about the running user type. It can have values like "Guest," "Portal," or "Embedded," indicating the context in which the component is being used.
  5. organization.Id:
    • Description: This property holds the organization's unique identifier (org ID), which can be used for various purposes like org-specific configurations or data access.
  6. useREST:
    • Description: This property is a boolean flag that determines whether to use REST or AJAX for communication with Salesforce. Depending on this setting, the component can adapt its data retrieval and communication methods.
  7. router:
    • Description: This property is an instance of the b3o.GlobalRemotingRouter class, which is used for server communication. It allows the component to interact with server-side processes and retrieve data.

Global Functions

  1. window.debug:
    • Description: This function provides a debugging feature, allowing developers to log messages or inspect variables during runtime. It can be used for troubleshooting and debugging custom components.
  2. Services.callout(ApexClassName, MethodName):
    • Description: This function enables AJAX/REST callouts to Salesforce. It requires two parameters, ApexClassName and MethodName, to specify the server-side Apex class and method to be invoked. This allows the custom component to interact with Salesforce data and processes.

Example Usage in JS:

await Services.callout('ApexClassName', {
    endp: 'classMethod',
    userId: '',
}).then(response => {
    if(response.success){
        console.info(response);
    }else{
        console.warn(response);
    }            
}).catch(error => {
    console.error(error);
}).finally(() => {
    //Good practice is to call the render() method after data is loaded
    this.render();
})

Example Usage in Apex:

global with sharing class ApexClassName implements b3o.GlobalRemotingInterface {
    private static Map<String, Object> requestObj;
    //No args constructor
    public void ApexClassName(){}

    //Remoting Interface
    public String getRemotingData(String params){
        try{
            ApexClassName.requestObj = (Map<String, Object>)JSON.deserializeUntyped(params);
            switch on (String)ApexClassName.requestObj.get('endp') {
                when 'classMethod' {		
                    return classMethod();
                }	
            }
        }catch(Exception e){
            //Handle exceptions
            return JSON.serialize(b3o.Exceptions.HandledException(e));
        }
        //Handle unknown methods
        return JSON.serialize(new ErrorResponse(String.format(
            Label.b3o.CRM_BadRemoteRequest,
            new Object[]{
                ApexClassName.class.getName(),
                params
            }
        )));
    }


    private static String classMethod(){    
        String userId = (String)ApexClassName.requestObj.get('userId');
        
        //... Do something

        return JSON.serialize(new SuccessResponse(new Map<String, Object>{
            'user' => ...,
        }));       
    }

}
  1. Services.showToast(type, message):
    • Description: This function displays a toast notification on the page. It accepts two parameters: type (error, warning, info, or success) and message (the content of the notification). It's a helpful way to communicate important messages to users.
Services.showToast(message = '', type = 'warning', time = 3000)
  1. Utils.getPathValue(objectPath):
    • Description: This function retrieves a value from an object using a specified path. It accepts objectPath as the parameter and returns the value found at that path within the object. This can be useful for data manipulation.
let foo = {
    bar: 'Hello'
}
let results = Utils.getPathValue(foo, 'foo.bar')    //Renders "Hello"
  1. Utils.stringFormat(template, argsArray):
    • Description: This function formats a string by replacing merge tags (e.g., {0}, {1}) with values from the argsArray. It helps in creating dynamic and customized strings.
Utils.stringFormat(
    'Hello {0}! ', 
    [
        'World'
    ]
)
  1. Utils.getParamsMap():
    • Description: This method retrieves a map of URL parameters for the current page. It can be used to access and utilize URL parameters in custom component logic.
  2. Utils.loadScript(scriptUrl):
    • Description: This function loads an external script on the page. It's useful for dynamically adding JavaScript functionality to custom components.
await Utils.loadScript(
    `https://maps.googleapis.com/maps/api/js?key=${this.placesApiKey}&libraries=places`
);
  1. Reactivity.addTemplateBindings():
    • Description: This function adds reactivity to web components, enabling them to respond to data changes and user interactions. It's essential for creating dynamic and interactive custom components.
<template>
    <input data-bind-onchange="handleChange" value="" type="text"/>
    <a data-bind-onclick="handleClick">Submit</a>
</template>
<script>
class NewComponent extends HTMLElement {
    //...
    renderedCallback(){
        Reactivity.addTemplateBindings.bind(this).call();
    }
    handleClick(event){
        console.log(event.target)   //the element clicked
        console.log(event.target.dataset)   //the element data attributes
        //....
    }
    handleChange(event){
        console.log(event.detail.value) //the changed value of the input
    }
    //...
}
</script>

Util Web Components

These util web components are available globally for use in custom component development:

<loading-spinner>:

Description: This component provides a spinner that can be displayed during loading or processing operations. It offers a visual indicator to inform users of ongoing tasks

<loading-spinner active="true" backdrop="true" variant="loading-bar"></loading-spinner>
<loading-spinner active="true" backdrop="true" variant="spinner"></loading-spinner>
<loading-spinner active="true" backdrop="true" variant="spinner" position="absolute"></loading-spinner>
<loading-spinner active="true" backdrop="true" variant="spinner" position="relative"></loading-spinner>

<custom-alert>: Description: This component renders customizable alert messages with various variants, including warning, info, success, and error. It's a versatile tool for notifying users of different events and conditions

<custom-alert variant="info">This is info</custom-alert>
<custom-alert variant="warning">This is warning</custom-alert>
<custom-alert variant="error">This is error</custom-alert>
<custom-alert variant="success">This is success</custom-alert>

<custom-modal>: Description: This component allows you to create modal dialogs that can display content, forms, or other information in a popup window. It's a valuable tool for user interactions and data input.

<custom-modal active="true">
    <loading-spinner active="true" class="iframe-loader"></loading-spinner>
    <div class="iframe-content"></div>
</custom-modal>

These global APIs and util web components enhance the capabilities of custom components within the 3B Portals platform, enabling developers to create dynamic, interactive, and user-friendly web experiences.

Traits JSON

Traits JSON in the Portal Custom Metadata item is used to define component properties that can be configured in the builder. Create an array of objects with the following properties:

"label": "Contact Id" - the label of the property to be displayed in the builder

"type": "text" - the type of property

"name": "user-id" - the attribute name that will be passed the inputted value in the builder

"default": "{{contactUser.Id}}" - the default value for the property

Here are a few examples:

[{
  type: 'text', // If you don't specify the type, the `text` is the default one
  name: 'my-trait', // Required and available for all traits
  label: 'My trait', // The label you will see near the input
  // label: false, // If you set label to `false`, the label column will be removed
  placeholder: 'Insert text', // Placeholder to show inside the input
},
{
  type: 'number',
  // ...
  placeholder: '0-100',
  min: 0, // Minimum number value
  max: 100, // Maximum number value
  step: 5, // Number of steps
},
{
  type: 'checkbox',
  // ...
  valueTrue: 'YES', // Value to assign when is checked, default: `true`
  valueFalse: 'NO', // Value to assign when is unchecked, default: `false`
},
{
  type: 'select',
  // ...
  options: [ // Array of options
    { id: 'opt1', name: 'Option 1'},
    { id: 'opt2', name: 'Option 2'},
  ]
},
{
  type: 'color',
  // ...
}]

Examples

Here's a full implementation of a new Web Component that will display a simple button, which when clicked, will show a toast.

Static Resource Structure

Create a new static resource called "AlertMe". Inside the static resource, create the following files:

index.js

/components/alert-me/alertMe.js

/components/alert-me/alertMe.html.js

/components/alert-me/alertMe.css.js


Example of the Index.js file

//The entire contents of the index.js file
import AlertMe from './components/alert-me/alertMe.js';
window.customElements.define('alert-me', AlertMe);

Example of the /components/alert-me/alertMe.js file

import css from './alertMe.css.js';
import html from './alertMe.html.js';

const APEX_CONTROLLER = 'b3o.ApexClassName';
class AlertMe extends HTMLElement {
    _isLoading = false;
    get isLoading() {
        return this._isLoading;
    }
    set isLoading(val) {
        this._isLoading = val;
        document.querySelector('loading-spinner.loading-spinner').setAttribute('active', ''+val);
    }

    labels = {

    }

    /** 
     * Called when component is created 
     **/
    constructor() {
        super();
    }
    
    /** 
     * Called when component is inserted
     */ 
    async connectedCallback() {
        if(window?.globals?.siteUrl === undefined){
            console.warn('Globals -> siteUrl is not defined');
        }
        //Merge labels with global labels
        this.getLabelOverrides();

        //Obligatory render call
        this.render();

        //Register listeners here

        //Init component
        await this.initComponent();
    }

    getLabelOverrides(){
        if(!window.globals?.labels) return;
        this.labels = Object.assign(this.labels, window.globals?.labels);
    }

    /** 
     * Component removed from DOM
     */
    disconnectedCallback() {
        //Remove global listeners to avoid memory leaks
    }

    /**
     * Component HTML updated
     */ 
    renderedCallback(){
        //Execute logic after rendering

        //Add reactivity to components
        Reactivity.addTemplateBindings.bind(this).call();
    }

    async initComponent(){
        debug(`Job Post: UserId [${this.userId}]: `, this.jobRecordId);
        this.isLoading = true;
        await Services.callout(APEX_CONTROLLER, {
            endp: 'classMethod',
        }).then(response => {
            if(response.success){
                console.info(response);
            }else{
                console.warn(response);
            }            
        }).catch(error => {
            console.error(error);
        }).finally(() => {
            this.isLoading = false;
            this.render();
        })
    }

    async handleClick(){
        Services.showToast('Clicked!', 'info');
    }
    
    
    /**
     * Observed attributes
     */ 
    static get observedAttributes() {
        return [
            'user-id', 
        ];
    }
    /**
    * Attributes that will cause a re-render if changed
    */ 
    get rerenderAttributes() {
        return [
            'user-id', 
        ];
    }

    /**
     * On observed attribute change listener
     */ 
    attributeChangedCallback(attrName, oldVal, newVal) {
        //Convert hyphen case to camelcase
        const attrKey = attrName.replace(/-([a-z])/g, function(k){
            return k[1].toUpperCase();
        });
        if (oldVal !== newVal) {
            this[attrKey] = newVal;

            //Re-render template if attribute changes and it requires a re-render
            if(this.rerenderAttributes.includes(attrName)){
                this.render();
            }            
        }
    }


    /**
     * Main template renderer
     * - This assumes we are creating light DOM elements
     * - You can also use shadow DOM
     */ 
    render(){
        this.innerHTML = this.getTemplateElement().innerHTML;
        this.renderedCallback();
    }

    /**
     * Get element HTML 
     */ 
    getTemplateElement() {
        let templateElement = document.createElement('template');
        templateElement.innerHTML = `
            <style>
                ${css.bind(this).call()}
            </style>
            ${html.bind(this).call()}
        `;
        return templateElement;
    } 

    reportValidity(){
        return true;
    }

    checkValidity(){
        return this.reportValidity();
    }
   
    //Atribute values here - once the user-id attribute is passed, this will be updated
    userId
}

export default AlertMe

Example of the /components/alert-me/alertMe.css.js file

export default function css(){
    return `
      button{
        color: blue;
      }
    `;
}

Example of the /components/alert-me/alertMe.html.js file

export default function html(){
    return `
        <button data-binding-onclick="handleClick">Click Me!</button>
    `;
}

Custom Portal Metadata

Create a new Custom Metadata record of the type Portal.

  • Label: Alert Me
  • HTML:
  • Category: Custom Components
  • Order: 1
  • Class: custom-component-class
  • JS Resource: AlertMe
  • Web Component Tag: alert-me
  • Traits JSON:
[{
"label": "Contact Id",
"type": "text",
"name": "user-id",
"default": "{{contactUser.Id}}"
}]

New Custom Component Shell

Below is an example of a new custom component implementation

Using Shadow Root

//from ./myNewComponent.html.js
export default function html(){
    return `
    <div>
        <loading-spinner active="false" position="relative" variant="loading-bar" class="loading-spinner"></loading-spinner>
    </div>
    `;
}

//from ./myNewComponent.css.js
export default function css(){
    return `
      /* Only Style This Component */
    `;
}

//from ./myNewComponent.js
import { Component } from '../../../b3o__GeneralUtils/framework/component.js'
import css from './myNewComponent.css.js';
import html from './myNewComponent.html.js';

const APEX_CONTROLLER = 'namespace.MyApexClassName';
class MyNewComponent extends Component {
    template;

    _isLoading = false;
    get isLoading() {
        return this._isLoading;
    }
    set isLoading(val) {
        this._isLoading = val;
        this.template.querySelector('loading-spinner.loading-spinner').setAttribute('active', ''+val);
    }
    
    /**
     * This is an example of handling user logged in session
     */ 
    get contactId(){
        //Get the contact Id for the logged in user from the attribute
        if(this.userId && this.userId.length > 0) return this.userId;
        //Attribute user-id was not provided, attempt to get from URL
        const urlParams = Utils.getParamsMap();
        return urlParams?.get('userId');
    }

    set contactId(val){
        //When contact id is set, cache it in local storage 
        localStorage.setItem('USER_ID', val);
    }

    /**
     * Component Labels
     * - These are overriden through the window.globals.labels object
     */ 
    labels = {
        btnRefreshLabel: 'Refresh',
    }


    constructor() {
      super();
      //Create a shadow root and attach it to the template
      this.template = this.attachShadow({mode: 'open'});
    }
    
    /** 
     * Component created/moved in DOM
     */ 
    async connectedCallback() {
        if(window?.globals?.siteUrl === undefined){
            console.warn('Globals -> siteUrl is not defined');
        }
        //Merge external labels
        this.getLabelOverrides();
        //Obligatory render call
        this.render();

        //Register listeners
        this.template.addEventListener('customEvent', this.customEventHandler.bind(this), false);

        //Init component
        await this.initComponent();
    }

    /**
     * If the window.globals object contains a labels
     * object, merge the provided label translations with 
     * the internal component labels
     */ 
    getLabelOverrides(){
        if(!window.globals?.labels) return;
        this.labels = Object.assign(this.labels, window.globals?.labels);
    }

    /** 
     * Component removed from DOM
     */
    disconnectedCallback() {
        //Remove global listeners to avoid memory leaks
        this.template.removeEventListener('customEvent', this.customEventHandler.bind(this), false);
    }

    /**
     * Component HTML created
     */ 
    renderedCallback(){
        //Add reactivity to the component
        Reactivity.addTemplateBindings.bind(this).call();
    }

    async initComponent(){
        this.isLoading = true;
        await Services.callout(APEX_CONTROLLER, {
            endp: 'myMethod',
            userId: this.contactId
        }).then(response => {
            if(response.success){
                console.info(response);
            }else{
                console.warn(response);
            }            
        }).catch(error => {
            console.error(error);
        }).finally(() => {
            this.isLoading = false;
            this.render();
        })
    }
    
    /**
     * Observed Attributes - add any html attributes
     * that we need to reflect into the component class
     */ 
    static get observedAttributes() {
        return [
            'user-id'
        ];
    }

    /**
     * Add any attributes that would cause a re-render
     * of the component if they changed
     */ 
    get rerenderAttributes() {
        return [
            'user-id'
        ];
    }

    /**
     * On observed attribute change listener
     */ 
    attributeChangedCallback(attrName, oldVal, newVal) {
        //Convert hyphen case to camelcase
        const attrKey = attrName.replace(/-([a-z])/g, function(k){
            return k[1].toUpperCase();
        });
        debug(`${attrName}: `, newVal);
        if (oldVal !== newVal) {
            this[attrKey] = newVal;

            //Re-render template if attribute changes and it requires a re-render
            if(this.rerenderAttributes.includes(attrName)){
                this.render();
            }            
        }
    }


    /**
     * Main template renderer
     */ 
    render(){
        this.template.innerHTML = this.getTemplateElement().innerHTML;
        this.renderedCallback();
    }

    /**
     * Get element HTML 
     */ 
    getTemplateElement() {
        let templateElement = document.createElement('template');
        templateElement.innerHTML = `
            <style>
                ${css.bind(this).call()}
            </style>
            ${html.bind(this).call()}
        `;
        return templateElement;
    } 

    reportValidity(){
        return true;
    }

    checkValidity(){
        return this.reportValidity();
    }
   
    userId;
}

export default MyNewComponent