528
edits
(Created page with "== 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 cust...") |
No edit summary |
||
Line 463: | Line 463: | ||
"default": "{{contactUser.Id}}" | "default": "{{contactUser.Id}}" | ||
}] | }] | ||
</syntaxhighlight> | |||
== New Custom Component Shell == | |||
Below is an example of a new custom component implementation | |||
=== Using Shadow Root === | |||
<syntaxhighlight lang="javascript" line="1"> | |||
//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 | |||
</syntaxhighlight> | </syntaxhighlight> | ||
[[Category:3B Portals]] | [[Category:3B Portals]] |