Skip to content

Instantly share code, notes, and snippets.

@vdepizzol
Created April 11, 2022 16:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vdepizzol/69bb9f458dd113fdd7df10141574dcd5 to your computer and use it in GitHub Desktop.
Save vdepizzol/69bb9f458dd113fdd7df10141574dcd5 to your computer and use it in GitHub Desktop.
class-map basic web component
// #region [ function helpers ]
const ucFirst = (text) => {
if (!text) return text;
return `${text[0].toUpperCase()}${text.substring(1)}`
};
const getAttributeUpdatedMethodName = (name) => `attributeChanged${ucFirst(name)}`;
// #endregion
class ClassMapWebComponent extends HTMLElement {
static get defaultClasses() { return []; }
static get classMap() { return {}; }
static get possibleAttributeValues() { return {}; }
constructor() {
super();
this.classList.add(...this.constructor.defaultClasses);
}
// #region [ WC lifecycle ]
attributeChangedCallback(name, oldValue, newValue) {
if (!this.hasAttributeUpdatedMethod(name)) return;
const methodName = getAttributeUpdatedMethodName(name);
this[methodName](name, oldValue, newValue);
}
// #endregion
// #region [ style/attribute management ]
/**
* Force an update of the element style with the current attribute values
*/
updateStyle() {
this.constructor.observedAttributes.forEach((attributeName) => {
this.updateAttribute(attributeName);
})
}
/**
* Force an update of an attribute
*/
updateAttribute(name) {
if (!this.hasAttributeUpdatedMethod(name)) return;
const methodName = getAttributeUpdatedMethodName(name);
const value = this.getAttribute(name);
this[methodName](name, undefined, value);
}
// #endregion
// #region [ Helpers ]
hasAttributeUpdatedMethod(name) {
const methodName = getAttributeUpdatedMethodName(name);
return methodName in this;
}
updateClassByAttributeClassMap(name, value) {
this.classList.remove(
...Object.values(this.constructor.classMap[name])
);
if (this.isValueAccepted(name, value)) {
this.classList.add(this.constructor.classMap[name][value]);
return true;
}
return false;
}
isValueAccepted(attributeName, value) {
if (!value) return false;
value = value.trim();
return !!this.constructor.possibleAttributeValues[attributeName]?.includes(value);
};
// #endregion
}
.Stack {
--Stack-gap: var(--scale-16);
display: flex;
flex-flow: column;
gap: var(--Stack-gap);
/* Gap */
&.Stack--condensed {
--Stack-gap: var(--scale-16);
}
@media ($query-whenNotMobile) {
&.Stack--spacious {
--Stack-gap: var(--scale-24);
}
}
/* Direction */
&.Stack--inline {
flex-flow: row;
}
&.Stack--block {
flex-flow: column;
}
/* Wrap */
&.Stack--wrap {
flex-wrap: wrap;
}
}
// #region [Definitions]
//attributes
const attributeList = Object.freeze(['direction', 'gap', 'wrap']);
const possibleAttributeValues = Object.freeze({
gap: ['normal', 'condensed', 'spacious'],
direction: ['inline', 'block'],
wrap: ['wrap', 'nowrap'],
});
//classes and styles
const stackClassName = 'Stack';
const defaultClasses = Object.freeze(['Stack', 'Stack--normal', 'Stack--block']);
const classMap = (() => {
const classMap = {};
classMap.gap = possibleAttributeValues.gap
.reduce((map, value) => {
map[value] = `${stackClassName}--${value}`
return map;
}, {});
classMap.direction = possibleAttributeValues.direction
.reduce((map, value) => {
map[value] = `${stackClassName}--${value}`
return map;
}, {});
classMap.wrap = possibleAttributeValues.wrap
.reduce((map, value) => {
map[value] = `${stackClassName}--${value}`
return map;
}, {});
return Object.freeze(classMap);
})();
const customPropertyMap = Object.freeze({
gap: `--${stackClassName}-gap`
});
// #endregion
class UiStack extends ClassMapWebComponent {
// #region [Definitions]
static get observedAttributes() {
return attributeList;
}
static get possibleAttributeValues() {
return possibleAttributeValues;
}
static get defaultClasses() {
return defaultClasses;
}
static get classMap() {
return classMap;
}
static get customPropertyMap() {
return customPropertyMap;
}
// #endregion
constructor() {
super();
}
// #region [ WC lifecycle ]
attributeChangedCallback(name, oldValue, newValue) {
super.attributeChangedCallback(name, oldValue, newValue);
}
// #endregion
// #region [ attribute updates ]
attributeChangedGap(name, _, newValue) {
const customProperty = UiStack.customPropertyMap[name];
if (customProperty) {
this.style.removeProperty(customProperty);
}
const wasUpdated = this.updateClassByAttributeClassMap(name, newValue);
if (wasUpdated) return;
if (!newValue || !newValue.trim()) return;
this.style.setProperty(customProperty, newValue);
}
attributeChangedDirection(name, _, newValue) {
this.updateClassByAttributeClassMap(name, newValue);
}
attributeChangedWrap(name, _, newValue) {
if (typeof newValue === 'string' && (!newValue || !newValue.trim())) newValue = 'wrap';
this.updateClassByAttributeClassMap(name, newValue);
}
// #endregion
}
// enabling custom element as a DOM interface
customElements.define('ui-stack', UiStack);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment