-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[bug3023] Expressions refactoring #32
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import { ATTRIBUTE, EVENT, TEXT } from '@riotjs/util/expression-types' | ||
import { EVENT, TEXT } from '@riotjs/util/expression-types' | ||
import expressions from './expressions/index.js' | ||
import { getTextNode } from './expressions/text.js' | ||
import { REF_ATTRIBUTE } from './constants.js' | ||
|
@@ -15,41 +15,41 @@ export const Expression = { | |
* @returns {Expression} self | ||
*/ | ||
mount(scope) { | ||
// hopefully a pure function | ||
this.value = this.evaluate(scope) | ||
|
||
// IO() DOM updates | ||
apply(this, this.value) | ||
|
||
return this | ||
return this.update(scope) | ||
}, | ||
|
||
/** | ||
* Update the expression if its value changed | ||
* Update the expression | ||
* @param {*} scope - argument passed to the expression to evaluate its current values | ||
* @returns {Expression} self | ||
*/ | ||
update(scope) { | ||
// pure function | ||
const value = this.evaluate(scope) | ||
|
||
if (this.value !== value) { | ||
// IO() DOM updates | ||
apply(this, value) | ||
// ref attributes should be called only once during mount | ||
if (this.name === REF_ATTRIBUTE && !this.value) { | ||
value(this.node) | ||
this.value = value | ||
return this | ||
} | ||
|
||
Comment on lines
+30
to
35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved it here so that the mount/unmount logic is in one place. Removed any ref processing from attribute expressions. |
||
// IO() DOM updates | ||
apply(this, value) | ||
this.value = value | ||
|
||
return this | ||
}, | ||
|
||
/** | ||
* Expression teardown method | ||
* @returns {Expression} self | ||
*/ | ||
unmount() { | ||
// unmount only the event handling expressions | ||
if (this.type === EVENT) apply(this, null) | ||
// ref attributes need to be unmounted as well | ||
if (this.name === REF_ATTRIBUTE) | ||
expressions[ATTRIBUTE](null, this, this.value) | ||
// ref attributes need to be called with null reference | ||
if (this.name === REF_ATTRIBUTE) this.value(null) | ||
|
||
return this | ||
}, | ||
|
@@ -58,16 +58,11 @@ export const Expression = { | |
/** | ||
* IO() function to handle the DOM updates | ||
* @param {Expression} expression - expression object | ||
* @param {*} value - current expression value | ||
* @param {*} newValue - current expression value | ||
* @returns {undefined} | ||
*/ | ||
function apply(expression, value) { | ||
return expressions[expression.type]( | ||
expression.node, | ||
expression, | ||
value, | ||
expression.value, | ||
) | ||
Comment on lines
-66
to
-69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any reason to pass 4 arguments when one of them already contains everything you need |
||
function apply(expression, newValue) { | ||
return expressions[expression.type](expression, newValue) | ||
} | ||
|
||
export default function create(node, data) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,6 @@ import { | |
isObject, | ||
} from '@riotjs/util/checks' | ||
import { memoize } from '@riotjs/util/misc' | ||
import { REF_ATTRIBUTE } from '../constants.js' | ||
|
||
/* c8 ignore next */ | ||
const ElementProto = typeof Element === 'undefined' ? {} : Element.prototype | ||
|
@@ -13,89 +12,76 @@ const isNativeHtmlProperty = memoize( | |
) | ||
|
||
/** | ||
* Add all the attributes provided | ||
* @param {HTMLElement} node - target node | ||
* @param {Object} attributes - object containing the attributes names and values | ||
* @returns {undefined} sorry it's a void function :( | ||
* Check whether the attribute value can be rendered | ||
* @param {HTMLElement} node - target node | ||
* @param {*} value - expression value | ||
* @returns {boolean} true if we can render this attribute value | ||
*/ | ||
function setAllAttributes(node, attributes) { | ||
Object.keys(attributes).forEach((name) => | ||
attributeExpression(node, { name }, attributes[name]), | ||
const shouldSetAttribute = (node, value) => { | ||
return ( | ||
['string', 'number', 'boolean'].includes(typeof value) && | ||
node.getAttribute(name) !== String(value) | ||
) | ||
} | ||
|
||
/** | ||
* Remove all the attributes provided | ||
* @param {HTMLElement} node - target node | ||
* @param {Object} newAttributes - object containing all the new attribute names | ||
* @param {Object} oldAttributes - object containing all the old attribute names | ||
* @returns {undefined} sorry it's a void function :( | ||
* Check whether the attribute should be removed | ||
* @param {*} value - expression value | ||
* @param {boolean} isBoolean - flag to handle boolean attributes | ||
* @returns {boolean} boolean - true if the attribute can be removed | ||
*/ | ||
function removeAllAttributes(node, newAttributes, oldAttributes) { | ||
const newKeys = newAttributes ? Object.keys(newAttributes) : [] | ||
|
||
Object.keys(oldAttributes) | ||
.filter((name) => !newKeys.includes(name)) | ||
.forEach((attribute) => node.removeAttribute(attribute)) | ||
} | ||
const shouldRemoveAttribute = (value, isBoolean) => | ||
isBoolean ? !value && value !== 0 : value == null | ||
|
||
/** | ||
* Check whether the attribute value can be rendered | ||
* @param {*} value - expression value | ||
* @returns {boolean} true if we can render this attribute value | ||
* Get the value as string | ||
* @param {string} name - attribute name | ||
* @param {*} value - user input value | ||
* @param {boolean} isBoolean - boolean attributes flag | ||
* @returns {string} input value as string | ||
*/ | ||
function canRenderAttribute(value) { | ||
return ['string', 'number', 'boolean'].includes(typeof value) | ||
} | ||
const normalizeValue = (name, value, isBoolean) => | ||
// be sure that expressions like selected={ true } will always be rendered as selected='selected' | ||
// fix https://github.com/riot/riot/issues/2975 | ||
value === true && isBoolean ? name : value | ||
|
||
/** | ||
* Check whether the attribute should be removed | ||
* @param {*} value - expression value | ||
* @param {boolean} isBoolean - flag to handle boolean attributes | ||
* @returns {boolean} boolean - true if the attribute can be removed} | ||
* Handle the spread operator {...attributes} | ||
* @param {HTMLElement} node - target node | ||
* @param {Object} value - new expression value | ||
* @param {Object} oldValue - the old expression cached value | ||
* @returns {undefined} | ||
*/ | ||
function shouldRemoveAttribute(value, isBoolean) { | ||
// boolean attributes should be removed if the value is falsy | ||
if (isBoolean) return !value && value !== 0 | ||
// otherwise we can try to render it | ||
return typeof value === 'undefined' || value === null | ||
function handleSpreadAttributes(node, value, oldValue) { | ||
const newKeys = Object.keys(value || []) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the double calculation of the object keys |
||
|
||
// remove all the old attributes not present in the new values | ||
if (oldValue) { | ||
Object.keys(oldValue) | ||
.filter((name) => !newKeys.includes(name)) | ||
.forEach((attribute) => node.removeAttribute(attribute)) | ||
} | ||
|
||
newKeys.forEach((name) => attributeExpression({ node, name }, value[name])) | ||
} | ||
|
||
/** | ||
* This methods handles the DOM attributes updates | ||
* @param {HTMLElement} node - target node | ||
* This method handles the DOM attributes updates | ||
* @param {Object} expression - expression object | ||
* @param {HTMLElement} expression.node - target node | ||
* @param {string} expression.name - attribute name | ||
* @param {boolean} expression.isBoolean - flag to handle boolean attributes | ||
* @param {*} expression.value - the old expression cached value | ||
* @param {*} value - new expression value | ||
* @param {*} oldValue - the old expression cached value | ||
* @returns {undefined} | ||
*/ | ||
export default function attributeExpression( | ||
node, | ||
{ name, isBoolean }, | ||
{ node, name, isBoolean, value: oldValue }, | ||
value, | ||
oldValue, | ||
) { | ||
// is it a spread operator? {...attributes} | ||
// Spread operator {...attributes} | ||
if (!name) { | ||
if (oldValue) { | ||
// remove all the old attributes | ||
removeAllAttributes(node, value, oldValue) | ||
} | ||
|
||
// is the value still truthy? | ||
if (value) { | ||
setAllAttributes(node, value) | ||
} | ||
|
||
return | ||
} | ||
|
||
// ref attributes are treated differently so we early return in this case | ||
if (name === REF_ATTRIBUTE) { | ||
node && node.removeAttribute(node, name) | ||
value(node) | ||
handleSpreadAttributes(node, value, oldValue) | ||
return | ||
} | ||
|
||
|
@@ -107,22 +93,12 @@ export default function attributeExpression( | |
node[name] = value | ||
} | ||
|
||
// remove any attributes with null values or falsy boolean native properties | ||
if (shouldRemoveAttribute(value, isBoolean)) { | ||
node.removeAttribute(name) | ||
} else if (canRenderAttribute(value)) { | ||
} | ||
// set new attributes if necessary | ||
else if (shouldSetAttribute(node, value)) { | ||
node.setAttribute(name, normalizeValue(name, value, isBoolean)) | ||
} | ||
} | ||
|
||
/** | ||
* Get the value as string | ||
* @param {string} name - attribute name | ||
* @param {*} value - user input value | ||
* @param {boolean} isBoolean - boolean attributes flag | ||
* @returns {string} input value as string | ||
*/ | ||
function normalizeValue(name, value, isBoolean) { | ||
// be sure that expressions like selected={ true } will always be rendered as selected='selected' | ||
// fix https://github.com/riot/riot/issues/2975 | ||
return value === true && isBoolean ? name : value | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,13 @@ | ||
import normalizeStringValue from '../util/normalize-string-value.js' | ||
|
||
/** | ||
* This methods handles the input fields value updates | ||
* @param {HTMLElement} node - target node | ||
* This method handles the input fields value updates | ||
* @param {Object} expression - expression object | ||
* @param {HTMLElement} expression.node - target node | ||
* @param {*} expression.value - old expression value | ||
* @param {*} value - new expression value | ||
* @returns {undefined} | ||
*/ | ||
export default function valueExpression(node, expression, value) { | ||
node.value = normalizeStringValue(value) | ||
export default function valueExpression({ node, value: oldValue }, value) { | ||
if (oldValue !== value) node.value = normalizeStringValue(value) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,8 @@ | ||
import { isNil } from '@riotjs/util/checks' | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This thing is not used anywhere in riot except this file, and it's too complicated because there is no difference between |
||
/** | ||
* Normalize the user value in order to render a empty string in case of falsy values | ||
* Normalize the user value in order to render an empty string in case of falsy values | ||
* @param {*} value - user input value | ||
* @returns {string} hopefully a string | ||
*/ | ||
export default function normalizeStringValue(value) { | ||
return isNil(value) ? '' : value | ||
return value == null ? '' : value | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reduce code duplication