/** A zero dependency custom web component that displays a value with a label
* Version: See the class code
*/
/** Copyright (c) 2022-2025 Julian Knight (Totally Information)
* https://it.knightnet.org.uk, https://github.com/TotallyInformation
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** TODO
* - Accessibility improvements needed
* - Partially complete - Needs colour/formatting controls
* - Needs managed var and uib integration
* - Allow HTML in the label (via html attribute)
*/
import TiBaseComponent from '../libs/ti-base-component'
/** Only use a template if you want to isolate the code and CSS */
const template = document.createElement('template')
template.innerHTML = /*html*/`
<style>
:host {
--label-before: '';
--label-after: ':';
--output-before: '';
--output-after: '';
--width: 100%;
--label-flex: 1;
--output-flex: 1;
--label-align: left;
--output-align: right;
--label-font: '';
--output-font: '';
display: flex; /* default is inline */
contain: content; /* performance boost */
flex-flow: row nowrap;
width: var(--width);
flex-direction: row;
}
label {
flex-grow: var(--label-flex);
flex-shrink: var(--label-flex);
text-align: var(--label-align);
font: var(--label-font);
}
label::before {
content: var(--label-before);
}
label::after {
content: var(--label-after);
}
output {
flex-grow: var(--output-flex);
flex-shrink: var(--output-flex);
text-align: var(--output-align);
font: var(--output-font);
}
output::before {
content: var(--output-before);
}
output::after {
content: var(--output-after);
}
/* Small screen (37.5em @ 16pt is about 600px) */
@media all and (max-width: 37.5em) {
:host {
/* On small screens, we are no longer using row direction but column */
flex-direction: column;
--output-align: var(--label-align);
}
}
</style>
<!--<div id="label"></div>-->
<!--<div id="value" class="value" aria-labelledby="label"></div>-->
<label></label>
<output></output>
<div><slot></slot></div>
`
/** Namespace
* @namespace Beta
*/
// Define the class and make it the default export
/**
* @class
* @augments TiBaseComponent
* @description A zero dependency custom web component that displays a value with a label
*
* @element labelled-value
* @memberOf Beta
* METHODS FROM BASE: (see TiBaseComponent)
* STANDARD METHODS:
* @function attributeChangedCallback Called when an attribute is added, removed, updated or replaced
* @function connectedCallback Called when the element is added to a document
* @function constructor Construct the component
* @function disconnectedCallback Called when the element is removed from a document
* OTHER METHODS:
* None
* CUSTOM EVENTS:
* "component-template:connected" - When an instance of the component is attached to the DOM. `evt.details` contains the details of the element.
* "component-template:ready" - Alias for connected. The instance can handle property & attribute changes
* "component-template:disconnected" - When an instance of the component is removed from the DOM. `evt.details` contains the details of the element.
* "component-template:attribChanged" - When a watched attribute changes. `evt.details.data` contains the details of the change.
* NOTE that listeners can be attached either to the `document` or to the specific element instance.
* Standard watched attributes (common across all my components):
* @property {string|boolean} inherit-style - Optional. Load external styles into component (only useful if using template). If present but empty, will default to './index.css'. Optionally give a URL to load.
* @property {string} name - Optional. HTML name attribute. Included in output _meta prop.
* Other watched attributes:
* None
* PROPS FROM BASE: (see TiBaseComponent)
* OTHER STANDARD PROPS:
* @property {string} componentVersion Static. The component version string (date updated). Also has a getter that returns component and base version strings.
* Other props:
* By default, all attributes are also created as properties
* @slot Container contents
* See https://github.com/runem/web-component-analyzer?tab=readme-ov-file#-how-to-document-your-components-using-jsdoc
*/
class LabelledValue extends TiBaseComponent {
/** Component version */
static componentVersion = '2025-02-25'
/** Makes HTML attribute change watched
* @returns {Array<string>} List of all of the html attribs (props) listened to
*/
static get observedAttributes() {
return [
// Standard watched attributes:
'inherit-style', 'name',
// Other watched attributes:
'label', 'value',
]
}
/** PRIVATE. Internal version of the label text to show on the label tag @type {string|undefined} */
#label = undefined
/** PRIVATE. Internal version of the value to show on the output tag @type {any} */
#value = undefined
/** PRIVATE. Reference to the label tag @type {HTMLLabelElement} */
#labelEl
/** PRIVATE. Reference to the output tag @type {HTMLOutputElement} */
#outputEl
/** PRIVATE. Reference to the slot tag @type {HTMLSlotElement} */
// #slotEl
/** If true, keeps the slot content when the value changes */
'keep-slot'
/** Set the value to show */
set value(val) {
if (val === null) return
if (val === this.#value) return
this.#value = val
this.#outputEl.textContent = val
this.setAttribute('value', val)
}
/** Get the current shown value
* @returns {any} The current value
*/
get value() {
return this.#value
}
/** Set the label to show */
set label(val) {
if (val === null) return
if (val === this.#label) return
this.#label = val
this.#labelEl.textContent = val
// this.setAttribute('label', val)
}
/** Get the current shown label
* @returns {string} The current label
*/
get label() {
return this.#label
}
/** NB: Attributes not available here - use connectedCallback to reference */
constructor() {
super()
// Only attach the shadow dom if code and style isolation is needed - comment out if shadow dom not required
if (template && template.content) this._construct(template.content.cloneNode(true))
this.#outputEl = this.shadowRoot.querySelector('output')
this.#labelEl = this.shadowRoot.querySelector('label')
// this.#slotEl = this.shadowRoot.querySelector('slot')
}
/** Runs when an instance is added to the DOM */
connectedCallback() {
this._connect() // Keep at start.
this['keep-slot'] = this.getAttribute('keep-slot')
this.label = this.getAttribute('label')
this.value = this.getAttribute('value')
this._ready() // Keep at end. Let everyone know that a new instance of the component has been connected & is ready
}
/** Runs when an instance is removed from the DOM */
disconnectedCallback() {
this._disconnect() // Keep at end.
}
// Runs when an observed attribute changes - Note: values are always strings
attributeChangedCallback(attrib, oldVal, newVal) {
/** Optionally ignore attrib changes until instance is fully connected
* Otherwise this can fire BEFORE everthing is fully connected.
*/
if (!this.connected) return
// Don't bother if the new value same as old
if ( oldVal === newVal ) return
// Create a property from the value - WARN: Be careful with name clashes
this[attrib] = newVal
// Add other dynamic attribute processing here.
// If attribute processing doesn't need to be dynamic, process in connectedCallback as that happens earlier in the lifecycle
// Keep at end. Let everyone know that an attribute has changed for this instance of the component
this._event('attribChanged', { attribute: attrib, newVal: newVal, oldVal: oldVal, })
}
} // ---- end of Class ---- //
// Make the class the default export so it can be used elsewhere
export default LabelledValue
/** Self register the class to global
* Enables new data lists to be dynamically added via JS
* and lets the static methods be called
*/
window['LabelledValue'] = LabelledValue
// Self-register the HTML tag
customElements.define('labelled-value', LabelledValue)