diff --git a/demo/observer.html b/demo/observer.html index 69254ae..b0a8d9d 100644 --- a/demo/observer.html +++ b/demo/observer.html @@ -13,51 +13,58 @@
  1. diff --git a/index.html b/index.html index ebe09e5..4bac795 100644 --- a/index.html +++ b/index.html @@ -1304,7 +1304,7 @@

    Storing to Local Storage:

    class="ml-3 mt-2 text-lg font-medium font-mono" :text="$storedValue" > - Update and Reload + Loading... @@ -1400,9 +1400,12 @@

    Multi Select:

    :click="selectedTags = selectedTags.add(tag); filteredTags = filteredTags.remove(selectedTag); selectedTag = filteredTags.first" - :mouseover="selectedTag = tag" - :class="selectedTag == tag ? 'bg-blue-100 text-blue-700' : 'text-gray-700'" - class="font-mono font-semibold py-2 px-3 rounded cursor-pointer hover:bg-blue-100 hover:text-blue-700" + :mouseover="el.isHovering = true" + :mouseout="el.isHovering = false" + :class="(selectedTag == tag ? 'text-blue-700' : 'text-gray-700') + (selectedTag == tag ? 'bg-blue-100' : '') + (el.isHovering && selectedTag != tag ? 'bg-gray-50' : '')" + class="font-mono font-semibold py-2 px-3 rounded cursor-pointer" :text="tag" >
  2. @@ -1502,13 +1505,13 @@

    Multi Select:

    grid-template-rows: 0fr 1fr; } -
    +
    About Us @@ -1523,10 +1526,10 @@

    Multi Select:

    Contact Us @@ -1541,9 +1544,12 @@

    Multi Select:

    - + Team 3
    @@ -1996,11 +2002,11 @@

    Tonic Dialog:

    Open Modal diff --git a/lib/entity.js b/lib/entity.js index ff82aaa..48f05d8 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -2,6 +2,7 @@ import { Interpreter, ClassInterpreter } from './generators/interpreter' import { Lexer } from './generators/lexer' import { Events } from './entity/events' import { Attributes } from './entity/attributes' +import { State } from './state' export default class Entity { constructor(el) { @@ -12,8 +13,11 @@ export default class Entity { this.events = new Events(this) this.attributes = new Attributes(this) + MiniJS.state.addEntity(this) if (MiniJS.debug) this.element.dataset.entityId = this.id + + this.attributes.evaluateParent() } setAsParent() { @@ -25,6 +29,10 @@ export default class Entity { return !!this.uuid } + isExists() { + return document.documentElement.contains(this.element) + } + getVariables() { this._getVariablesFromAttributes() this._getVariablesFromEvents() @@ -86,31 +94,44 @@ export default class Entity { _initVariables() { this.variables = [...new Set(this.variables)] - MiniJS.variables = [...new Set(MiniJS.variables.concat(this.variables))] this.variables.forEach((variable) => { - if (variable.startsWith('el.')) { + if (State.isElState(variable)) { this.setAsParent() - if (!this.parent) this.parent = this.getParent() + if (window[this.id] == null) { + window[this.id] = MiniJS.state.create({}, this.id) + } - const varName = variable.replace('el.', '') + MiniJS.state.addVariable(this.id, this.id) - if (!window[this.uuid]) window[this.uuid] = {} + if (variable !== 'el') { + const [_, varName] = variable.split('.') + MiniJS.state.addEntityVariable(this.id, varName, this.id) + } + } else if (State.isParentState(variable)) { + if (!this.parent) this.parent = this.getParent() - // ! FIXME: Any changes to el.varName isn't being watched - window[this.uuid][varName] = MiniJS.tryFromLocal( - variable.replace('el.', this.uuid + '.') - ) + if (window[this.parent.id] == null) { + window[this.parent.id] = MiniJS.state.create({}, this.parent.id) + } + + MiniJS.state.addVariable(this.parent.id, this.id) - if (!this.variables.includes(this.uuid)) this.variables.push(this.uuid) + if (variable !== 'parent') { + const [_, varName] = variable.split('.') + MiniJS.state.addEntityVariable(this.parent.id, varName, this.id) + } } else if (typeof window[variable] === 'function') { this.variables.splice(this.variables.indexOf(variable), 1) - MiniJS.variables.splice(MiniJS.variables.indexOf(variable), 1) } else { - window[variable] = variable.startsWith('$') - ? MiniJS.tryFromLocal(variable) - : window[variable] + const [identifier] = variable.split('.') + + window[identifier] = variable.startsWith('$') + ? MiniJS.tryFromLocal(identifier) + : window[identifier] + + MiniJS.state.addVariable(identifier, this.id) } }) } @@ -118,12 +139,15 @@ export default class Entity { async _interpret(expr, options = {}) { const Engine = options.isClass ? ClassInterpreter : Interpreter const engine = new Engine(expr, options) - const ids = { $: 'document.querySelector' } + const ids = { + $: 'document.querySelector', + el: `proxyWindow['${this.id}']`, + } - if (this.parent?.uuid) ids.el = `proxyWindow['${this.parent.uuid}']` + if (this.parent) ids.parent = `proxyWindow['${this.parent.id}']` this.variables.forEach((variable) => { - if (variable.startsWith('el.') || variable === 'el') return + if (State.isElState(variable) || State.isParentState(variable)) return ids[variable] = `proxyWindow-${variable}` }) @@ -133,8 +157,6 @@ export default class Entity { return await engine.interpret(this) } - /* Note: I don't this getParent() is needed, - since el. variables should use the current element's uuid instead. */ getParent() { if (this.isParent()) { return this @@ -145,15 +167,13 @@ export default class Entity { currentElement = parentNode parentNode = currentElement.parentNode } - const entity = MiniJS.elements.find( - (e) => e.uuid == parentNode.dataset.uuid - ) + const entities = Array.from(MiniJS.state.entities.values()) + const entity = entities.find((e) => e.uuid == parentNode.dataset.uuid) return entity } } generateEntityUUID() { - // Suggestion: we can use crypto.randomUUID(). Tho crypto only works in secure contexts return 'Entity' + Date.now() + Math.floor(Math.random() * 10000) } @@ -161,8 +181,6 @@ export default class Entity { this.getVariables() this.events.apply() await this.attributes.evaluate() - - MiniJS.elements.push(this) } initChildren() { @@ -183,47 +201,31 @@ export default class Entity { dispose() { const elements = [this.element, ...this.element.querySelectorAll('*')] + const entities = Array.from(MiniJS.state.entities.values()) const variables = [] // Remove event bindings for (const element of elements) { if (element.nodeType !== 1) continue - const entity = MiniJS.elements.find( - (entity) => entity.element === element - ) + const entity = MiniJS.state.getEntityByElement(element, entities) + if (!entity) continue variables.push(...entity.variables) entity.events.dispose() + MiniJS.state.removeEntity(entity) } - // Remove disposed elements - MiniJS.elements = MiniJS.elements.filter( - (entity) => !elements.includes(entity.element) - ) - // Clean up unused variables - const usedVariables = MiniJS.elements.reduce( - (acc, entity) => acc.concat(entity.variables), - [] - ) + const usedVariables = entities + .filter((entity) => !elements.includes(entity.element)) + .reduce((acc, entity) => acc.concat(entity.variables), []) const unusedVariables = variables.filter( (variable) => !usedVariables.includes(variable) ) - MiniJS.variables = MiniJS.variables.filter( - (variable) => !unusedVariables.includes(variable) - ) - - unusedVariables.forEach((variable) => { - if (variable.startsWith('el.')) { - const varName = variable.replace('el.', '') - if (window[this.uuid]?.[varName]) delete window[this.uuid] - } else { - delete window[variable] - } - }) + MiniJS.state.disposeVariables(this.id, unusedVariables) } } diff --git a/lib/entity/attributes.js b/lib/entity/attributes.js index 6c8ead4..6ef9d13 100644 --- a/lib/entity/attributes.js +++ b/lib/entity/attributes.js @@ -2,15 +2,24 @@ import { Events } from './events' import { escapeHTML } from '../helpers/sanitize' export class Attributes { - static CUSTOM_ATTRIBUTES = [':class', ':text', ':value', ':checked', ':each'] + static CUSTOM_ATTRIBUTES = [ + ':class', + ':text', + ':value', + ':checked', + ':each', + ':parent', + ] + static FORBIDDEN_ATTRIBUTES = [':innerHTML', ':innerText'] static isValidAttribute(attribute, element) { if (!attribute.startsWith(':')) return false + if (Attributes.FORBIDDEN_ATTRIBUTES.includes(attribute)) return false if (Events.isValidEvent(attribute)) return false if (Attributes.CUSTOM_ATTRIBUTES.includes(attribute)) return true const [nativeAttr] = attribute.replace(':', '').split('.') - if (element[nativeAttr] !== undefined) return false + if (element[nativeAttr] === undefined) return false return true } @@ -50,7 +59,8 @@ export class Attributes { async evaluateAttribute(attr) { if (!Attributes.isValidAttribute(attr, this.base.element)) return - if (attr === ':class') await this.evaluateClass() + if (attr === ':parent') this.evaluateParent() + else if (attr === ':class') await this.evaluateClass() else if (attr === ':text') await this.evaluateText() else if ([':value', ':checked'].includes(attr)) await this.evaluateValue() else if (attr === ':each') await this.evaluateEach() @@ -61,58 +71,84 @@ export class Attributes { } } + evaluateParent() { + if (!this.base.element.hasAttribute(':parent')) return + if (this.base.isParent()) return + this.base.setAsParent() + } + async evaluateClass() { const expr = this.base.element.getAttribute(':class') if (!expr) return - const updatedClassNames = await this.base._interpret(expr, { - base: this.initialState.classList, - isClass: true, - }) + try { + const updatedClassNames = await this.base._interpret(expr, { + base: this.initialState.classList, + isClass: true, + }) - this.base.element.setAttribute('class', updatedClassNames) + this.base.element.setAttribute('class', updatedClassNames) + } catch (error) { + if (!this.base.isExists()) return + throw error + } } async evaluateText() { const textExpr = this.base.element.getAttribute(':text') if (!textExpr) return - const newText = await this.base._interpret(textExpr) + try { + const newText = await this.base._interpret(textExpr) - if (newText || newText == '') this.base.element.innerText = newText + if (newText || newText == '') this.base.element.innerText = newText + } catch (error) { + if (!this.base.isExists()) return + throw error + } } async evaluateValue() { - const valueExpr = this.base.element.getAttribute(':value') + try { + const valueExpr = this.base.element.getAttribute(':value') - if (valueExpr) { - const newValue = await this.base._interpret(valueExpr) + if (valueExpr) { + const newValue = await this.base._interpret(valueExpr) - if (this.base.element.value !== newValue && newValue != null) - this.base.element.value = newValue - } + if (this.base.element.value !== newValue && newValue != null) + this.base.element.value = newValue + } - const checkedExpr = this.base.element.getAttribute(':checked') + const checkedExpr = this.base.element.getAttribute(':checked') - if (checkedExpr) { - const newValue = await this.base._interpret(checkedExpr) + if (checkedExpr) { + const newValue = await this.base._interpret(checkedExpr) - if (newValue) this.base.element.checked = newValue + if (newValue) this.base.element.checked = newValue + } + } catch (error) { + if (!this.base.isExists()) return + throw error } } async evaluateOtherAttributes() { - for (const attr of this.dynamicAttributes) { - if (Attributes.CUSTOM_ATTRIBUTES.includes(attr)) continue - - const expr = this.base.element.getAttribute(attr) - if (!expr) return - - const newValue = await this.base._interpret(expr) - const nativeAttr = attr.slice(1) - - if (this.base.element[nativeAttr] !== newValue && newValue != null) - this.base.element[nativeAttr] = newValue + try { + for (const attr of this.dynamicAttributes) { + if (Attributes.CUSTOM_ATTRIBUTES.includes(attr)) continue + + const expr = this.base.element.getAttribute(attr) + if (!expr) return + + const newValue = await this.base._interpret(expr) + const nativeAttr = attr.slice(1) + + if (this.base.element[nativeAttr] !== newValue && newValue != null) + this.base.element[nativeAttr] = newValue + } + } catch (error) { + if (!this.base.isExists()) return + throw error } } diff --git a/lib/entity/events.js b/lib/entity/events.js index d5b6c73..10b4244 100644 --- a/lib/entity/events.js +++ b/lib/entity/events.js @@ -20,6 +20,7 @@ export class Events { ':change', ':clickout', ':press', + ':load', ...Events.CUSTOM_KEY_EVENTS, ] @@ -36,7 +37,8 @@ export class Events { Events.validEvents = [...events, ...Events.CUSTOM_EVENTS] } - static applyEvents(entities) { + static applyEvents() { + const entities = Array.from(MiniJS.state.entities.values()) entities.forEach((entity) => { entity.events.apply() }) @@ -70,6 +72,7 @@ export class Events { apply() { this.dispose() + this.evaluate(':load') this.setChangeEvent() this.setClickoutEvent() @@ -79,6 +82,7 @@ export class Events { // Other Event Bindings Array.from(el.attributes).forEach((attr) => { + if (attr.name === ':load') return if (!Events.isValidEvent(attr.name)) return const isKeyEvent = Events.CUSTOM_KEY_EVENTS.some((keyType) => @@ -225,6 +229,17 @@ export class Events { async evaluate(attr) { const value = this.base.element.getAttribute(attr) if (!value) return + + if (attr === ':load') { + const elVariables = this.base.variables + .filter((v) => v.startsWith('el.') && v !== 'el') + .map((v) => v.replace('el.', '')) + const variables = this.base.variables.filter((v) => !v.startsWith('el.')) + + MiniJS.state.attachVariableHelpers(variables) + MiniJS.state.attachVariableHelpers(elVariables, this.base.id) + } + await this.base._interpret(value) } diff --git a/lib/generators/lexer.js b/lib/generators/lexer.js index a651ab4..a7582b6 100644 --- a/lib/generators/lexer.js +++ b/lib/generators/lexer.js @@ -31,6 +31,14 @@ function setMemberIdentifier(node, members = []) { } } +/* + TODO + - being read as undefined.toLocalTimeString() + :click="const time = new Date().toLocaleTimeString())" +- add support for: + :click="const [a, b] = [1, 2]" +*/ + export class Lexer { static debug = false static ID_TYPE = { diff --git a/lib/main.js b/lib/main.js index 508f769..e415396 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,39 +1,14 @@ import Entity from './entity' -import MiniArray from './helpers/array' import { Lexer } from './generators/lexer' import { observeDOM } from './generators/observer' +import { State } from './state' import { Events } from './entity/events' let nativeProps = Object.getOwnPropertyNames(window) const MiniJS = (() => { - window.proxyWindow = null - let _debug = false - let _elements = [] - let _variables = [] - - const watchHandler = { - set: function (target, property, value) { - // Set variable to new value - target[property] = value - - // Store to localstorage - if (property[0] === '$') { - localStorage.setItem(property, JSON.stringify(value)) - } - - if (_variables.includes(property)) { - updateStates(property) - _addMethodsToVariables([property]) - } - - return true - }, - get: function (target, property) { - return target[property] - }, - } + const state = new State() async function init() { // Automatically initialize when the script is loaded @@ -41,16 +16,13 @@ const MiniJS = (() => { let startTime = performance.now() _setDebugMode() - _setProxyWindow() + state.setProxyWindow() Events.initValidEvents() _findElements() _initializeGlobalVariables() - _addMethodsToVariables() - Events.applyEvents(_elements) - updateStates() + Events.applyEvents() + state.evaluate() _listenToDOMChanges() - // Temporarily commented out - to be reviewed - // _evaluateLoadEvents(); const endTime = performance.now() const executionTime = endTime - startTime console.log(`MiniJS took ${executionTime}ms to run.`) @@ -63,15 +35,14 @@ const MiniJS = (() => { record.type === 'attributes' && record.attributeName.startsWith(':') ) { - const entity = _elements.find( - (entity) => entity.element === record.target - ) + const entity = state.getEntityByElement(record.target) + // TODO: Add support for dynamically inserted events like :click entity?.attributes.evaluateAttribute(record.attributeName) } record.removedNodes.forEach((node) => { if (node.nodeType !== 1) return - const entity = _elements.find((entity) => entity.element === node) + const entity = state.getEntityByElement(node) entity?.dispose() }) @@ -85,30 +56,15 @@ const MiniJS = (() => { }) } - function _addMethodsToVariables(variables = _variables) { - variables.forEach((variable) => { - if ( - Array.isArray(proxyWindow[variable]) && - !(proxyWindow[variable] instanceof MiniArray) - ) { - proxyWindow[variable] = new MiniArray(...proxyWindow[variable]) - } - }) - } - - function _setProxyWindow() { - proxyWindow = new Proxy(window, watchHandler) - } - function _setDebugMode() { - if (_debug) { - console.log('MiniJS Debug Mode Enabled') - Lexer.debug = true - } + if (!_debug) return + console.log('MiniJS Debug Mode Enabled') + Lexer.debug = true } function _initializeGlobalVariables() { - _elements.forEach((entity, index) => { + const entities = Array.from(state.entities.values()) + entities.forEach((entity, index) => { entity.getVariables() }) } @@ -128,25 +84,6 @@ const MiniJS = (() => { } } - function _evaluateLoadEvents() { - _elements.forEach((entity) => { - entity.events.evaluate(':load') - }) - } - - async function updateStates(property = null) { - for (const entity of _elements) { - if ( - entity.variables.includes(property) || - property == null || - entity.uuid == property || - entity.parent?.uuid == property - ) { - await entity.attributes.evaluate() - } - } - } - function _findElements() { const elems = document.body.getElementsByTagName('*') @@ -154,8 +91,7 @@ const MiniJS = (() => { const elem = elems[i] if (elem.nodeType !== 1) continue - const entity = new Entity(elem) - _elements.push(entity) + new Entity(elem) } } @@ -181,20 +117,11 @@ const MiniJS = (() => { set debug(value) { _debug = !!value }, - get elements() { - return _elements - }, - set elements(newElements) { - _elements = newElements - }, - get variables() { - return _variables - }, - set variables(newVarList) { - _variables = newVarList - }, get window() { - return proxyWindow + return state.window + }, + get state() { + return state }, tryFromLocal, } diff --git a/lib/state.js b/lib/state.js new file mode 100644 index 0000000..ff88777 --- /dev/null +++ b/lib/state.js @@ -0,0 +1,190 @@ +import MiniArray from './helpers/array' + +export class State { + static isLocalState(variable) { + return variable[0] === '$' + } + + static isElState(variable) { + return variable.startsWith('el.') || variable === 'el' + } + + static isParentState(variable) { + return variable.startsWith('parent.') || variable === 'parent' + } + + constructor() { + this.window = null + + this.entities = new Map() // key: entityID, value: entity + this.variables = new Map() // key: variable, value: entityID + this.entityVariables = new Map() // key: entityID.variable, value: entityID + } + + setProxyWindow() { + this.window = this.create(window) + } + + getEntityByElement(el, entities = Array.from(this.entities.values())) { + return entities.find((entity) => entity.element === el) + } + + addEntity(entity) { + this.entities.set(entity.id, entity) + } + + removeEntity(entity) { + this.entities.delete(entity.id) + + const variables = [...this.variables.entries()] + variables.forEach(([key, value]) => { + if (key === entity.id) this.variables.delete(key) + else if (value.includes(entity.id)) + this.variables.set( + key, + value.filter((id) => id !== entity.id) + ) + }) + + const entityVariables = [...this.entityVariables.entries()] + entityVariables.forEach(([key, value]) => { + const [entityID] = key.split('.') + if (entityID === entity.id) this.entityVariables.delete(key) + else if (value.includes(entity.id)) + this.entityVariables.set( + key, + value.filter((id) => id !== entity.id) + ) + }) + + delete window[entity.id] + } + + hasDependency(variable) { + return this.variables.has(variable) || this.entityVariables.has(variable) + } + + addVariable(variable, entityID) { + const variables = this.variables.get(variable) || [] + this.variables.set(variable, [...new Set(variables), entityID]) + } + + addEntityVariable(parentEntityID, variable, entityID) { + const key = `${parentEntityID}.${variable}` + const variables = this.entityVariables.get(key) || [] + this.entityVariables.set(key, [...new Set(variables), entityID]) + } + + create(object, entityID = null) { + const ctx = this + + return new Proxy(object, { + set: function (target, property, value) { + if (entityID) ctx.setEntityState(target, property, value, entityID) + else if (State.isLocalState(property)) + ctx.setLocalState(target, property, value) + else ctx.setState(target, property, value) + + return true + }, + get: function (target, property) { + return target[property] + }, + }) + } + + setLocalState(target, property, value) { + localStorage.setItem(property, JSON.stringify(value)) + this.setState(target, property, value) + } + + setState(target, property, value) { + target[property] = value + + if (!this.hasDependency(property)) return + this.evaluateDependencies(property) + this.attachVariableHelpers([property]) + } + + setEntityState(target, property, value, entityID) { + target[property] = value + + if (!this.hasDependency(entityID)) return + + const variable = `${entityID}.${property}` + this.evaluateDependencies(variable) + this.attachVariableHelpers([entityID]) + this.attachVariableHelpers([property], entityID) + } + + evaluate() { + Array.from(this.entities.values()).forEach((entity) => { + entity.attributes.evaluate() + }) + + this.attachVariableHelpers(Array.from(this.variables.keys())) + } + + evaluateDependencies(variable) { + const variables = this.variables.get(variable) || [] + + variables.forEach((entityID) => { + const entity = this.entities.get(entityID) + // TODO: Only update relevant attributes that uses those variables + entity?.attributes.evaluate() + }) + + const entityVariables = this.entityVariables.get(variable) || [] + + entityVariables.forEach((entityID) => { + const entity = this.entities.get(entityID) + // TODO: Only update relevant attributes that uses those variables + entity?.attributes.evaluate() + }) + + this.attachVariableHelpers([variable]) + } + + attachVariableHelpers(variables, entityID = null) { + variables.forEach((variable) => { + const value = + entityID != null + ? this.window[entityID][variable] + : this.window[variable] + + if (Array.isArray(value) && !(value instanceof MiniArray)) { + if (entityID != null) + this.window[entityID][variable] = new MiniArray(...value) + else this.window[variable] = new MiniArray(...value) + } + }) + } + + disposeVariables(entityID, variables) { + variables.forEach((variable) => { + if (State.isElState(variable)) { + delete window[entityID] + MiniJS.state.disposeVariable(entityID) + + if (variable !== 'el') { + const varName = variable.replace('el.', '') + MiniJS.state.disposeEntityVariable(entityID, varName) + } + } else { + delete window[variable] + MiniJS.state.disposeVariable(variable) + + if (State.isLocalState(variable)) localStorage.removeItem(variable) + } + }) + } + + disposeVariable(variable) { + this.variables.delete(variable) + } + + disposeEntityVariable(parentEntityID, variable) { + const key = `${parentEntityID}.${variable}` + this.entityVariables.delete(key) + } +} diff --git a/readme.md b/readme.md index 23f8ef8..edfe422 100644 --- a/readme.md +++ b/readme.md @@ -23,6 +23,8 @@ To setup MiniJS in your local machine, you can do the following: `State` are variables that changes the UI or the DOM that uses it when they get updated. +Note: Only non-nested objects are supported for reactive state. + ### Setting Initial State You can set the initial state of the variables using vanilla JS: @@ -178,12 +180,29 @@ These are the events added in by MiniJS: ## Statements -- `:each` - loop through an array and render a template for each item +- `:each` (experimental!) - loop through an array and render a template for each item + - do not use in production + +## Variables + +### Variables saved in Local Storage + +Appending `$` to the variable name will save the variable in the local storage: + +```html + + + +``` -## Variable +Note: Currently, this is only available for globally declared variables. ### Variable Scoping +#### Global Variables + Whenever you create a variable, it will automatically be added to the global scope. This means that you can access it anywhere in your code. ```html @@ -194,23 +213,108 @@ Whenever you create a variable, it will automatically be added to the global sco ``` -If you want to create a local variable, instead of using `const`, `var`, and `let` variable declarations, you need use `el.`: +#### Local Variables + +To use variables only in a current event, you can create a local variable using `const`, and `let`: + +```html + +``` + +### Element Variables + +If you want to use the variable across an element's attributes and events, you can use `el.`: ```html ``` +Like the example above, `:load` can be used to set the initial value of the variable. + +### Parent Element Variables + +Adding a `:parent` attribute to an element will allow you to access its variables from its children using `parent.` variables. + +A children's `parent.` variable is the same as the parent's `el.` variable. + +```html +
    + + + + +
    + + +
    + +
    + +
    + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy + eirmod. +
    +
    + +
    + +
    + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy + eirmod. +
    +
    +
    +``` + ### Variable Methods MiniJS added some commonly-used custom methods to variables.