- index
- quickstart designers
- tutorials designers
- quickstart developers
- tutorials developers
- elements
- mixins
- AddHasValueAttributeMixin.js
- DraggableElementMixin.js
- DraggableListMixin.js
- FormElementMixin.js
- InputMixin.js
- LabelsMixin.js
- NativeReflectorMixin.js
- NativeValidatorMixin.js
- StyleableMixin.js
- SyntheticValidatorMixin.js
- ThemeableMixin.js
- appendices
- material theme
This is the centrepiece of the nn- elements. Every nn- element has the
characteristic of being basically a native element with theming steroids.
Each nn- element has, in its template, a native element marked as
id="native" which identifies the element they represent. For example
the Button.js file will implement nn-button element, which in turn will
have <button id="native" in its template.
The approach to nn- elements is to reflect as much as possible, in terms
of properties and attributes, from the nn- element down to the native one.
This means that the nn- element is a “gateway” to properties and attributes
of the actual native element inside.
For example writing:
<nn-button label="Some label"></nn-button>
Will imply that the contained <button> element (which is marked as
native) also has the label attribute set to Some label.
The idea is that between <nn-button> and <button> everything is
reflected. This is great in theory, but there is a level of trickery
required to make things work properly. For example, some attributes will
always need to be skipped (id, style, class). Also, it’s impossible
to simply reflect every property, since 1) they could be anywhere in the
prototype chain 2) Some properties should never be reflected (see:
setAttribute(), hasChildNodes(), and so on).
So, the approach is:
- All attributes are reflected, except some that are blacklisted (in
this.skipAttributes) - Only properties/methods listed in
this.reflectPropertiesare reflected. Each element will provide a comprehensive list of reflected properties, which will depend on the HTML specs of the targetednativeelement. - Some “boot” properties are assigned when the element is first updated.
Into the code
First of all, NativeRefletorMixin is declared as a mixing in function:
import { element } from '../lib/htmlApi.js'
export const NativeReflectorMixin = (base) => {
return class Base extends base { // eslint-disable-lineThe firstUpdated method is used to perform one-time work after the element’s template has been created. In this case, it will need to:
- Find the native element (marked with
id="native") - Start reflection of attributes and properties
firstUpdated () {
/* Find the native element */
this.native = this.shadowRoot.querySelector('#native')
/* Reflect all attributes and properties */
/* - all properties are reflected except some (listed in skipAttributes) */
/* - only elected properties are reflected (listed in reflectProperties) */
this._reflectAttributesAndProperties()
}
get reflectProperties () {
return element
}
get skipProperties () {
return ['style']
}
get skipAttributes () {
return ['id', 'style', 'class']
}
afterSettingProperty () {}
getAttribute (attr) {
if (!this.native || this.skipAttributes.indexOf(attr) !== -1) {
return super.getAttribute(attr)
}
return this.native.getAttribute(attr)
/*
const nativeAttribute = this.native.getAttribute(attr)
if (nativeAttribute !== null) return nativeAttribute
This shouldn’t really happen, but it’s here as a fallback TODO: Maybe delete it and always return the native’s value regardless
return super.getAttribute(attr)
*/
}
setAttribute (attr, value) {Set the attribute
super.setAttribute(attr, value)Skip the ones in the skipAttributes list
if (this.skipAttributes.indexOf(attr) !== -1) returnAssign the same attribute to the contained native element, taking care of the ‘nn’ syntax
this._setSubAttr(attr, value)
}
removeAttribute (attr) {Set the attribute
super.removeAttribute(attr)Skip the ones in the skipAttributes list
if (this.skipAttributes.indexOf(attr) !== -1) returnAssign the same attribute to the contained native element, taking care of the ‘nn’ syntax
this._setSubAttr(attr, null)
}
_setSubAttr (subAttr, attrValue) {
const tokens = subAttr.split('::')Safeguard: if this.native is not yet set, it means that an attribute was set BEFORE the element was rendered. If that is the case, simply give up. _reflectAttributesAndProperties() will be run afterwards to sync things up anyway
if (!this.native) returnNo :: found, simply change attribute in native
if (tokens.length === 1) {
(attrValue === null)
? this.native.removeAttribute(subAttr)
: this.native.setAttribute(subAttr, attrValue)Yes, :: is there: assign the attribute to the element with the corresponding ID
} else if (tokens.length === 2) {
const dstElement = this.shadowRoot.querySelector(`#${tokens[0]}`)
if (dstElement) {
attrValue === null
? dstElement.removeAttribute(tokens[1])
: dstElement.setAttribute(tokens[1], attrValue)
}
}
}
_reflectAttributesAndProperties () {STEP #1: ATTRIBUTES FIRST
Assign all starting attribute to the destination element
for (const attributeObject of this.attributes) {
const attr = attributeObject.name
if (this.skipAttributes.indexOf(attr) !== -1) continue
this._setSubAttr(attr, super.getAttribute(attr))
}Observe changes in attribute from the source element, and reflect them to the destination element
const thisObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes') {
const attr = mutation.attributeNameDon’t reflect forbidden attributes
if (this.skipAttributes.indexOf(attr) !== -1) returnDon’t reflect attributes with ::
if (attr.indexOf('::') !== -1) returnCheck if the value has changed. If it hasn’t, there is no point in re-assigning it, especially since the observer might have been triggered by this very mixin
const newValue = this.native.getAttribute(attr)
const thisValue = super.getAttribute(attr)
if (newValue === thisValue) returnAssign the new value
(newValue === null)
? super.removeAttribute(attr)
: super.setAttribute(attr, newValue)
}
})
})
thisObserver.observe(this.native, { attributes: true })STEP #2: METHODS (as bound functions) AND PROPERTIES (as getters/setters)
const uniqProps = [...new Set(this.reflectProperties)]
const proto = Object.getPrototypeOf(this)
if (!proto._alreadyReflecting) {
uniqProps.forEach(prop => {
if (this.skipProperties.indexOf(prop) !== -1) return
Object.defineProperty(Object.getPrototypeOf(this), prop, {
get: function () {
const dst = this.native
if (!this.native) return undefined
if (typeof dst[prop] === 'function') return dst[prop].bind(dst)
else return dst[prop]
},
set: function (newValue) {
const dst = this.nativeIt IS possile that this.native isn’t set yet, since the property observer is on the prototype. So, you could have one nn-input-box without a value assigned (and the observer is installed for prototype) and then another one with a property assigned at creation (observer is set, but this.native is not yet set) If that is the case, it will assign the object’s prop. Then, when firstUpdated() runs, it will forward-assign this value to this.native
if (!dst) {
if (typeof newValue !== 'undefined') {
Object.defineProperty(this, prop, { value: newValue, configurable: true, writable: true })
}
return
}
if (typeof this.beforeSettingProperty === 'function') {
this.beforeSettingProperty(prop, newValue)
}
if (typeof dst[prop] === 'function') return
const oldValue = dst[prop]Set the new value
dst[prop] = newValueThis is required by litElement since it won’t create a setter if there is already one
this.requestUpdate(prop, oldValue)
if (typeof this.afterSettingProperty === 'function') {
this.afterSettingProperty(prop, newValue)
}
},
configurable: true,
enumerable: true
})
})
proto._alreadyReflecting = true
}Assign existing properties, in case the setter had already been triggered BEFORE firstUpdated() (in which case, the setter would have assigned OBJECT properties, without reflection)
uniqProps.forEach(prop => {
if (this.skipProperties.indexOf(prop) !== -1) return
let propValue
if (Object.prototype.hasOwnProperty.call(this, prop)) {
propValue = this[prop]
delete this[prop]
this[prop] = propValue
}
})
}
}
}