@jinntec/fore
Version:
Fore - declarative user interfaces in plain HTML
424 lines (373 loc) • 13.4 kB
JavaScript
import '../fx-model.js';
import ForeElementMixin from '../ForeElementMixin.js';
import { ModelItem } from '../modelitem.js';
import { Fore } from '../fore.js';
import getInScopeContext from '../getInScopeContext.js';
import { evaluateXPathToFirstNode } from '../xpath-evaluation.js';
function isDifferent(oldNodeValue, oldControlValue, newControlValue) {
if (oldNodeValue === null) {
return false;
}
/*
if the oldControlValue is null we know the widget is used for the first time and is not considered
a value change.
*/
if(oldControlValue === null) return false;
if (newControlValue && oldControlValue && newControlValue.nodeType && oldControlValue.nodeType) {
return newControlValue.outerHTML !== oldControlValue.outerHTML;
}
if (oldControlValue === newControlValue) {
return false;
}
return true;
}
/**
* `AbstractControl` -
* is a general base class for control elements.
*
*/
export default class AbstractControl extends ForeElementMixin {
constructor() {
super();
this.value = null;
this.display = this.style.display;
this.required = false;
this.readonly = false;
this.widget = null;
this.visited = false;
this.force = false;
// this.attachShadow({ mode: 'open' });
}
// eslint-disable-next-line class-methods-use-this
getWidget() {
throw new Error('You have to implement the method getWidget!');
}
/**
* (re)apply all modelItem state properties to this control. model -> UI
*/
async refresh(force) {
if (force) this.force = true;
// console.log('### AbstractControl.refresh on : ', this);
// Save the old value of this control. this may be the stringified version, contrast to the node in `nodeset`
const oldValue = this.value;
// if(this.repeated) return
if (this.isNotBound()) return;
// await this.updateComplete;
// await this.getWidget();
this.evalInContext();
this.oldVal = this.nodeset ? this.nodeset : null;
// console.log('oldVal',this.oldVal);
// todo this if should be removed - see above
if (this.isBound()) {
// this.control = this.querySelector('#control');
if (!this.nodeset) {
const create = this.closest('[create]');
if (create) {
// ### check if parent element exists
let attrName;
let parentPath;
let parentNode;
if (this.ref.includes('/')) {
parentPath = this.ref.substring(0, this.ref.indexOf('/'));
const inscope = getInScopeContext(this.parentNode, this.ref);
parentNode = evaluateXPathToFirstNode(parentPath, inscope, this);
if (parentNode && parentNode.nodeType === Node.ELEMENT_NODE) {
if (this.ref.includes('@')) {
attrName = this.ref.substring(this.ref.indexOf('/') + 2);
parentNode.setAttribute(attrName, '');
} else {
Fore.dispatch(this, 'warn', {
message: '"create" is not implemented for elements',
});
}
}
} else {
const inscope = getInScopeContext(this, this.ref);
if (this.ref.includes('@')) {
attrName = this.ref.substring(this.ref.indexOf('@') + 1);
inscope.setAttribute(attrName, '');
} else {
Fore.dispatch(this, 'warn', { message: '"create" is not implemented for elements' });
// inscope = getInScopeContext(this.parentNode, this.ref);
}
}
} else {
// ### this actually makes the control nonrelevant
// todo: we should call a template function here to allow detachment of event-listeners and resetting eventual state
// this.style.display = 'none';
this.setAttribute('nonrelevant', '');
}
return;
}
this.modelItem = this.getModelItem();
// console.log('refresh modelItem', this.modelItem);
if (this.modelItem instanceof ModelItem) {
// console.log('### XfAbstractControl.refresh modelItem : ', this.modelItem);
if (this.hasAttribute('as') && this.getAttribute('as') === 'node') {
// console.log('as', this.nodeset);
// this.modelItem.value = this.nodeset;
this.modelItem.node = this.nodeset;
this.value = this.modelItem.node;
} else {
this.value = this.modelItem.value;
}
// console.log('newVal',this.value);
// console.log('value of widget',this.value);
/*
* todo: find out on which foreign modelitems we might be dependant on when no binds are used.
*
* e.g. filter expr on 'ref' 'instance('countries')//country[@continent = instance('default')/continent]'
*
* the country node is dependant on instance('default')/continent here (foreign node).
*
* possible approach:
* - pipe ref expression through DependencyNotifyingDomFacade to get referred nodes.
* - lookup modelItems of referred nodes
* - add ourselves to boundControls of foreign modelItem -> this control will then get refreshed when the foreign modelItem is changed.
*/
// const touched = FxBind.getReferencesForRef(this.ref,Array.from(this.nodeset));
// console.log('touched',touched);
/*
this is another case that highlights the fact that an init() function might make sense in general.
*/
if (!this.modelItem.boundControls.includes(this)) {
this.modelItem.boundControls.push(this);
}
// console.log('>>>>>>>> abstract refresh ', this.control);
// this.control[this.valueProp] = this.value;
await this.updateWidgetValue();
this.handleModelItemProperties();
// if(!this.closest('fx-fore').ready) return; // state change event do not fire during init phase (initial refresh)
if (this.getOwnerForm().initialRun) {
Fore.dispatch(this, 'init', {});
}
if (!this.getOwnerForm().ready) return; // state change event do not fire during init phase (initial refresh)
// if oldVal is null we haven't received a concrete value yet
if (!(this.localName === 'fx-control' || this.localName === 'fx-upload')) return;
if (isDifferent(this.oldVal, oldValue, this.value)) {
const model = this.getModel();
Fore.dispatch(this, 'value-changed', {
path: this.modelItem.path,
value: this.modelItem.value,
oldvalue: oldValue,
instanceId:this.modelItem.instanceId,
foreId:this.getOwnerForm().id
});
}
}
}
}
refreshFromModelItem(modelItem) {}
/**
*
* @returns {Promise<void>}
*/
// eslint-disable-next-line class-methods-use-this
async updateWidgetValue() {
throw new Error('You have to implement the method updateWidgetValue!');
}
handleModelItemProperties() {
// console.log('handleModelItemProperties',this.modelItem);
this.handleRequired();
this.handleReadonly();
if (this.getOwnerForm().ready) {
this.handleValid();
}
this.handleRelevant();
// Relevance.handleRelevance(this);
// todo: handleType()
}
_getForm() {
return this.getModel().parentNode;
}
_dispatchEvent(event) {
if (this.getOwnerForm().ready) {
Fore.dispatch(this, event, {});
}
}
// eslint-disable-next-line class-methods-use-this
handleRequired() {
// console.log('mip required', this.modelItem.required);
this.widget = this.getWidget();
const wasRequired = this.isRequired();
if (!this.modelItem.required) {
this.widget.removeAttribute('required');
this.removeAttribute('required');
if (wasRequired !== this.modelItem.required) {
this._dispatchEvent('optional');
}
return;
}
// ### modelItem is required
if (this.visited || this.force) {
if (this.modelItem.value === '') {
this.classList.add('isEmpty');
this._toggleValid(false);
} else {
this.classList.remove('isEmpty');
this._toggleValid(true);
}
}
this.widget.setAttribute('required', '');
this.setAttribute('required', '');
if (wasRequired !== this.modelItem.required) {
this._dispatchEvent('required');
}
/*
if (this.isRequired() !== this.modelItem.required) {
this._updateRequired();
}
*/
}
_updateRequired() {
if (this.modelItem.required) {
// if (this.getOwnerForm().ready){
if (this.visited || this.force) {
// if (this.visited ) {
// if (this.widget.value === '') {
if (this.modelItem.value === '') {
this.classList.add('isEmpty');
this._toggleValid(false);
} else {
this.classList.remove('isEmpty');
this._toggleValid(true);
}
}
this.widget.setAttribute('required', '');
this.setAttribute('required', '');
this._dispatchEvent('required');
} else {
this.widget.removeAttribute('required');
this.removeAttribute('required');
this._dispatchEvent('optional');
}
}
_toggleValid(valid) {
if (valid) {
this.removeAttribute('invalid');
this.setAttribute('valid', '');
} else {
this.removeAttribute('valid');
this.setAttribute('invalid', '');
}
}
handleReadonly() {
// console.log('mip readonly', this.modelItem.isReadonly);
if (this.isReadonly() !== this.modelItem.readonly) {
if (this.modelItem.readonly) {
this.widget.setAttribute('readonly', '');
this.setAttribute('readonly', '');
this._dispatchEvent('readonly');
}
if (!this.modelItem.readonly) {
this.widget.removeAttribute('readonly');
this.removeAttribute('readonly');
this._dispatchEvent('readwrite');
}
}
}
// todo - review alert handling altogether. There could be potentially multiple ones in model
handleValid() {
// console.log('mip valid', this.modelItem.required);
// console.log('late modelItem', mi);
if (this.isValid() !== this.modelItem.constraint) {
if (this.modelItem.constraint) {
// if (alert) alert.style.display = 'none';
this._dispatchEvent('valid');
this.setAttribute('valid', '');
this.removeAttribute('invalid');
} else {
this.setAttribute('invalid', '');
this.removeAttribute('valid');
// ### constraint is invalid - handle alerts
/*
if (alert) {
alert.style.display = 'block';
}
*/
if (this.modelItem.alerts.length !== 0) {
const controlAlert = this.querySelector('fx-alert');
if (!controlAlert) {
const { alerts } = this.modelItem;
// console.log('alerts from bind: ', alerts);
alerts.forEach(modelAlert => {
const newAlert = document.createElement('fx-alert');
// const newAlert = document.createElement('span');
newAlert.innerHTML = modelAlert;
this.appendChild(newAlert);
// newAlert.style.display = 'block';
});
}
}
// this.dispatchEvent(new CustomEvent('invalid', {}));
this._dispatchEvent('invalid');
}
}
}
handleRelevant() {
// console.log('mip valid', this.modelItem.enabled);
const item = this.modelItem.node;
this.removeAttribute('relevant');
this.removeAttribute('nonrelevant');
if (Array.isArray(item) && item.length === 0) {
this._dispatchEvent('nonrelevant');
this.setAttribute('nonrelevant', '');
// this.style.display = 'none';
return;
}
if (this.isEnabled() !== this.modelItem.relevant) {
if (this.modelItem.relevant) {
this._dispatchEvent('relevant');
// this._fadeIn(this, this.display);
this.setAttribute('relevant', '');
// this.style.display = this.display;
} else {
this._dispatchEvent('nonrelevant');
// this._fadeOut(this);
this.setAttribute('nonrelevant', '');
// this.style.display = 'none';
}
}
}
isRequired() {
return this.hasAttribute('required');
}
isValid() {
return !this.hasAttribute('invalid');
}
isReadonly() {
// const widget = this.querySelector('#widget');
return this.hasAttribute('readonly');
}
isEnabled() {
return !this.hasAttribute('nonrelevant');
}
// eslint-disable-next-line class-methods-use-this
_fadeOut(el) {
el.style.opacity = 1;
(function fade() {
// eslint-disable-next-line no-cond-assign
if ((el.style.opacity -= 0.1) < 0) {
el.style.display = 'none';
} else {
requestAnimationFrame(fade);
}
})();
}
// eslint-disable-next-line class-methods-use-this
_fadeIn(el, display) {
el.style.opacity = 0;
el.style.display = display || 'block';
(function fade() {
let val = parseFloat(el.style.opacity);
// eslint-disable-next-line no-cond-assign
if (!((val += 0.1) > 1)) {
el.style.opacity = val;
requestAnimationFrame(fade);
}
})();
}
}
if (!customElements.get('fx-abstract-control')) {
window.customElements.define('fx-abstract-control', AbstractControl);
}