From 412d3791a4a0e07d5e896c5570b9376f646caf4f Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Mon, 12 Feb 2024 14:06:05 +0800 Subject: [PATCH 01/29] refactor: relocate state related logic to state.js --- lib/main.js | 77 ++++++++-------------------------------------------- lib/state.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 65 deletions(-) create mode 100644 lib/state.js diff --git a/lib/main.js b/lib/main.js index 508f769..9377453 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,39 +1,15 @@ 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,13 +17,13 @@ const MiniJS = (() => { let startTime = performance.now() _setDebugMode() - _setProxyWindow() + state.setProxyWindow() Events.initValidEvents() _findElements() _initializeGlobalVariables() - _addMethodsToVariables() + state.attachVariableHelpers() Events.applyEvents(_elements) - updateStates() + state.triggerDOMUpdate() _listenToDOMChanges() // Temporarily commented out - to be reviewed // _evaluateLoadEvents(); @@ -85,26 +61,10 @@ 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() { @@ -134,19 +94,6 @@ const MiniJS = (() => { }) } - 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('*') @@ -188,13 +135,13 @@ const MiniJS = (() => { _elements = newElements }, get variables() { - return _variables + return state.variables }, - set variables(newVarList) { - _variables = newVarList + set variables(variables) { + state.variables = variables }, get window() { - return proxyWindow + return state.window }, tryFromLocal, } diff --git a/lib/state.js b/lib/state.js new file mode 100644 index 0000000..4fcc0bb --- /dev/null +++ b/lib/state.js @@ -0,0 +1,69 @@ +import MiniArray from './helpers/array' + +export class State { + static isLocalState(variable) { + return variable[0] === '$' + } + + constructor() { + this.window = null + this.variables = [] + } + + setProxyWindow() { + this.window = this.create(window) + } + + create(object) { + const ctx = this + + return new Proxy(object, { + set: function (target, property, value) { + 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.variables.includes(property)) return + this.triggerDOMUpdate(property) + this.attachVariableHelpers() + } + + triggerDOMUpdate(state) { + for (const entity of MiniJS.elements) { + const shouldUpdate = + entity.variables.includes(state) || + state == null || + entity.uuid == state || + entity.parent?.uuid == state + + if (shouldUpdate) entity.attributes.evaluate() + } + } + + attachVariableHelpers(variables = this.variables) { + variables.forEach((variable) => { + if ( + Array.isArray(this.window[variable]) && + !(this.window[variable] instanceof MiniArray) + ) { + this.window[variable] = new MiniArray(...this.window[variable]) + } + }) + } +} From 666430810fc82a5ab1982af46c5a076eb632c049 Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:55:58 +0800 Subject: [PATCH 02/29] chore: add note to update states --- lib/state.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/state.js b/lib/state.js index 4fcc0bb..c546e32 100644 --- a/lib/state.js +++ b/lib/state.js @@ -52,6 +52,7 @@ export class State { entity.uuid == state || entity.parent?.uuid == state + // TODO: Only update relevant attributes that uses those variables if (shouldUpdate) entity.attributes.evaluate() } } From fcf16e5d651797a35745902a0e1116c5b9d0ab6d Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:56:09 +0800 Subject: [PATCH 03/29] refactor: update initial text of stored valeu --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index ebe09e5..fdff595 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... From a996de7ed68c13a0452418c808b88bc285d76ce6 Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:05:46 +0800 Subject: [PATCH 04/29] fix: exclude object properties in tracked variables --- lib/entity.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/entity.js b/lib/entity.js index ff82aaa..00cdb73 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -86,33 +86,31 @@ export default class Entity { _initVariables() { this.variables = [...new Set(this.variables)] - MiniJS.variables = [...new Set(MiniJS.variables.concat(this.variables))] + + const trackedVariables = [] this.variables.forEach((variable) => { - if (variable.startsWith('el.')) { + if (variable.startsWith('el.') || variable === 'el') { this.setAsParent() if (!this.parent) this.parent = this.getParent() - const varName = variable.replace('el.', '') - if (!window[this.uuid]) window[this.uuid] = {} - - // ! FIXME: Any changes to el.varName isn't being watched - window[this.uuid][varName] = MiniJS.tryFromLocal( - variable.replace('el.', this.uuid + '.') - ) - - if (!this.variables.includes(this.uuid)) this.variables.push(this.uuid) } 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] + + trackedVariables.push(identifier) } }) + + MiniJS.variables = [...new Set(MiniJS.variables.concat(trackedVariables))] } async _interpret(expr, options = {}) { From a4e4cd70f14cb6e61ac04a8e70989e5dadf3edff Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:12:58 +0800 Subject: [PATCH 05/29] feat: setup proxy object for el variables --- lib/entity.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/entity.js b/lib/entity.js index 00cdb73..3cfb76b 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -91,11 +91,15 @@ export default class Entity { this.variables.forEach((variable) => { if (variable.startsWith('el.') || variable === 'el') { - this.setAsParent() + if (this.uuid == null) { + this.setAsParent() - if (!this.parent) this.parent = this.getParent() + if (!this.parent) this.parent = this.getParent() - if (!window[this.uuid]) window[this.uuid] = {} + window[this.uuid] = MiniJS.state.create({}, this.uuid) + } + + trackedVariables.push(this.uuid) } else if (typeof window[variable] === 'function') { this.variables.splice(this.variables.indexOf(variable), 1) MiniJS.variables.splice(MiniJS.variables.indexOf(variable), 1) From 53e65aa5fb5cc82764f47d18d379bd351ea38115 Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:13:23 +0800 Subject: [PATCH 06/29] refactor: use local variables instead of el variables for modal --- index.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.html b/index.html index fdff595..8e87b61 100644 --- a/index.html +++ b/index.html @@ -1996,11 +1996,11 @@

Tonic Dialog:

Open Modal From c926d9b1500dddf3e16e48d84ed0f5d9442e8320 Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Mon, 12 Feb 2024 19:20:44 +0800 Subject: [PATCH 07/29] feat: add dependency-based state update and add reactive entity variables --- demo/observer.html | 33 +++++++------ lib/entity.js | 30 +++++++----- lib/main.js | 11 ++--- lib/state.js | 118 +++++++++++++++++++++++++++++++++++++++------ 4 files changed, 146 insertions(+), 46 deletions(-) diff --git a/demo/observer.html b/demo/observer.html index 69254ae..d7bc15a 100644 --- a/demo/observer.html +++ b/demo/observer.html @@ -13,51 +13,56 @@
  1. diff --git a/lib/entity.js b/lib/entity.js index 3cfb76b..24411c0 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -12,6 +12,7 @@ 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 } @@ -86,7 +87,6 @@ export default class Entity { _initVariables() { this.variables = [...new Set(this.variables)] - const trackedVariables = [] this.variables.forEach((variable) => { @@ -96,13 +96,17 @@ export default class Entity { if (!this.parent) this.parent = this.getParent() - window[this.uuid] = MiniJS.state.create({}, this.uuid) + window[this.id] = MiniJS.state.create({}, this.id) } - trackedVariables.push(this.uuid) + MiniJS.state.addDependency(this.id, this.id) + + if (variable !== 'el') { + const [_, varName] = variable.split('.') + MiniJS.state.addEntityDependency(this.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 { const [identifier] = variable.split('.') @@ -110,11 +114,14 @@ export default class Entity { ? MiniJS.tryFromLocal(identifier) : window[identifier] + MiniJS.state.addDependency(identifier, this.id) trackedVariables.push(identifier) } }) - MiniJS.variables = [...new Set(MiniJS.variables.concat(trackedVariables))] + MiniJS.state.variables = [ + ...new Set(MiniJS.state.variables.concat(trackedVariables)), + ] } async _interpret(expr, options = {}) { @@ -135,8 +142,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 @@ -155,7 +160,6 @@ export default class Entity { } generateEntityUUID() { - // Suggestion: we can use crypto.randomUUID(). Tho crypto only works in secure contexts return 'Entity' + Date.now() + Math.floor(Math.random() * 10000) } @@ -197,6 +201,7 @@ export default class Entity { if (!entity) continue variables.push(...entity.variables) + MiniJS.state.removeEntity(entity) entity.events.dispose() } @@ -215,16 +220,19 @@ export default class Entity { (variable) => !usedVariables.includes(variable) ) - MiniJS.variables = MiniJS.variables.filter( + MiniJS.state.variables = MiniJS.state.variables.filter( (variable) => !unusedVariables.includes(variable) ) unusedVariables.forEach((variable) => { - if (variable.startsWith('el.')) { + if (variable.startsWith('el.') || variable === 'el') { + delete window[this.id] const varName = variable.replace('el.', '') - if (window[this.uuid]?.[varName]) delete window[this.uuid] + MiniJS.state.disposeDependency(this.id) + MiniJS.state.disposeEntityDependency(this.id, varName) } else { delete window[variable] + MiniJS.state.disposeDependency(variable) } }) } diff --git a/lib/main.js b/lib/main.js index 9377453..30de2ab 100644 --- a/lib/main.js +++ b/lib/main.js @@ -23,7 +23,7 @@ const MiniJS = (() => { _initializeGlobalVariables() state.attachVariableHelpers() Events.applyEvents(_elements) - state.triggerDOMUpdate() + state.evaluate() _listenToDOMChanges() // Temporarily commented out - to be reviewed // _evaluateLoadEvents(); @@ -134,15 +134,12 @@ const MiniJS = (() => { set elements(newElements) { _elements = newElements }, - get variables() { - return state.variables - }, - set variables(variables) { - state.variables = variables - }, get window() { return state.window }, + get state() { + return state + }, tryFromLocal, } })() diff --git a/lib/state.js b/lib/state.js index c546e32..736b3a0 100644 --- a/lib/state.js +++ b/lib/state.js @@ -8,18 +8,83 @@ export class State { constructor() { this.window = null this.variables = [] + + this.entities = new Map() // key: entityID, value: entity + this.dependencies = new Map() // key: variable, value: entityID + this.entityDependencies = new Map() // key: entityID.variable, value: entityID } setProxyWindow() { this.window = this.create(window) } - create(object) { + addEntity(entity) { + this.entities.set(entity.id, entity) + } + + removeEntity(entity) { + this.entities.delete(entity.id) + + this.dependencies.forEach((_, entityID) => { + if (entityID === entity.id) this.dependencies.delete(entity.id) + }) + + this.entityDependencies.forEach((_, variable) => { + const [entityID] = variable.split('.') + if (entityID === entity.id) this.entityDependencies.delete(variable) + }) + } + + hasDependency(variable) { + return ( + this.dependencies.has(variable) || this.entityDependencies.has(variable) + ) + } + + addDependency(variable, entityID) { + const dependencies = this.dependencies.get(variable) || [] + this.dependencies.set(variable, [...new Set(dependencies), entityID]) + } + + removeDependency(variable, entityID) { + const dependencies = (this.dependencies.get(variable) || []).filter( + (dep) => dep === entityID + ) + + this.dependencies.set(variable, dependencies) + } + + disposeDependency(variable) { + this.dependencies.delete(variable) + } + + addEntityDependency(parentEntityID, variable, entityID) { + const key = `${parentEntityID}.${variable}` + const dependencies = this.entityDependencies.get(key) || [] + this.entityDependencies.set(key, [...new Set(dependencies), entityID]) + } + + removeEntityDependency(parentEntityID, variable, entityID = null) { + const key = `${parentEntityID}.${variable}` + const dependencies = (this.entityDependencies.get(key) || []).filter( + (dep) => dep === entityID + ) + + this.entityDependencies.set(key, dependencies) + } + + disposeEntityDependency(parentEntityID, variable) { + const key = `${parentEntityID}.${variable}` + this.entityDependencies.delete(key) + } + + create(object, entityID = null) { const ctx = this return new Proxy(object, { set: function (target, property, value) { - if (State.isLocalState(property)) + if (entityID) ctx.setEntityState(target, property, value, entityID) + else if (State.isLocalState(property)) ctx.setLocalState(target, property, value) else ctx.setState(target, property, value) @@ -39,22 +104,47 @@ export class State { setState(target, property, value) { target[property] = value - if (!this.variables.includes(property)) return - this.triggerDOMUpdate(property) - this.attachVariableHelpers() + 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]) } - triggerDOMUpdate(state) { - for (const entity of MiniJS.elements) { - const shouldUpdate = - entity.variables.includes(state) || - state == null || - entity.uuid == state || - entity.parent?.uuid == state + evaluate() { + Array.from(this.entities.values()).forEach((entity) => { + entity.attributes.evaluate() + }) + + this.attachVariableHelpers(Array.from(this.dependencies.keys())) + } + + evaluateDependencies(variable) { + const dependencies = this.dependencies.get(variable) || [] + + dependencies.forEach((entityID) => { + const entity = this.entities.get(entityID) + // TODO: Only update relevant attributes that uses those variables + entity?.attributes.evaluate() + }) + + const entityDependencies = this.entityDependencies.get(variable) || [] + entityDependencies.forEach((entityID) => { + const entity = this.entities.get(entityID) // TODO: Only update relevant attributes that uses those variables - if (shouldUpdate) entity.attributes.evaluate() - } + entity?.attributes.evaluate() + }) + + this.attachVariableHelpers([variable]) } attachVariableHelpers(variables = this.variables) { From d2a02d108b47582b5129e44e4ef2c7b109d7b28b Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Mon, 12 Feb 2024 19:23:02 +0800 Subject: [PATCH 08/29] fix: catch eval error for non-existing elements --- index.html | 9 +++-- lib/entity.js | 29 ++++++++------- lib/entity/attributes.js | 78 +++++++++++++++++++++++++--------------- 3 files changed, 72 insertions(+), 44 deletions(-) diff --git a/index.html b/index.html index 8e87b61..85e6775 100644 --- a/index.html +++ b/index.html @@ -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. diff --git a/lib/entity.js b/lib/entity.js index 24411c0..849651c 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -26,6 +26,10 @@ export default class Entity { return !!this.uuid } + isExists() { + return document.documentElement.contains(this.element) + } + getVariables() { this._getVariablesFromAttributes() this._getVariablesFromEvents() @@ -91,19 +95,19 @@ export default class Entity { this.variables.forEach((variable) => { if (variable.startsWith('el.') || variable === 'el') { - if (this.uuid == null) { - this.setAsParent() + this.setAsParent() - if (!this.parent) this.parent = this.getParent() + if (!this.parent) this.parent = this.getParent() - window[this.id] = MiniJS.state.create({}, this.id) + if (window[this.uuid] == null) { + window[this.uuid] = MiniJS.state.create({}, this.id) } - MiniJS.state.addDependency(this.id, this.id) + MiniJS.state.addDependency(this.uuid, this.id) if (variable !== 'el') { const [_, varName] = variable.split('.') - MiniJS.state.addEntityDependency(this.id, varName, this.id) + MiniJS.state.addEntityDependency(this.uuid, varName, this.id) } } else if (typeof window[variable] === 'function') { this.variables.splice(this.variables.indexOf(variable), 1) @@ -127,9 +131,10 @@ export default class Entity { async _interpret(expr, options = {}) { const Engine = options.isClass ? ClassInterpreter : Interpreter const engine = new Engine(expr, options) - const ids = { $: 'document.querySelector' } - - if (this.parent?.uuid) ids.el = `proxyWindow['${this.parent.uuid}']` + const ids = { + $: 'document.querySelector', + el: `proxyWindow['${this.uuid}']`, + } this.variables.forEach((variable) => { if (variable.startsWith('el.') || variable === 'el') return @@ -226,10 +231,10 @@ export default class Entity { unusedVariables.forEach((variable) => { if (variable.startsWith('el.') || variable === 'el') { - delete window[this.id] + delete window[this.uuid] const varName = variable.replace('el.', '') - MiniJS.state.disposeDependency(this.id) - MiniJS.state.disposeEntityDependency(this.id, varName) + MiniJS.state.disposeDependency(this.uuid) + MiniJS.state.disposeEntityDependency(this.uuid, varName) } else { delete window[variable] MiniJS.state.disposeDependency(variable) diff --git a/lib/entity/attributes.js b/lib/entity/attributes.js index 6c8ead4..3d4c426 100644 --- a/lib/entity/attributes.js +++ b/lib/entity/attributes.js @@ -65,54 +65,74 @@ export class Attributes { const expr = this.base.element.getAttribute(':class') if (!expr) return - const updatedClassNames = await this.base._interpret(expr, { - base: this.initialState.classList, - isClass: true, - }) - - this.base.element.setAttribute('class', updatedClassNames) + try { + const updatedClassNames = await this.base._interpret(expr, { + base: this.initialState.classList, + isClass: true, + }) + + 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 } } From 5c5a6b0fb6af1a2f470ad498e98c9f95a38f0975 Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Tue, 13 Feb 2024 00:02:29 +0800 Subject: [PATCH 09/29] refactor: prevent usage of :innerHTML and :innerText --- lib/entity/attributes.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/entity/attributes.js b/lib/entity/attributes.js index 3d4c426..9ec104a 100644 --- a/lib/entity/attributes.js +++ b/lib/entity/attributes.js @@ -3,9 +3,11 @@ import { escapeHTML } from '../helpers/sanitize' export class Attributes { static CUSTOM_ATTRIBUTES = [':class', ':text', ':value', ':checked', ':each'] + 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 From bd5df017ddd33c28201877ae0786bc9c7ebe305c Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Tue, 13 Feb 2024 00:04:19 +0800 Subject: [PATCH 10/29] style: missing button class --- demo/observer.html | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/observer.html b/demo/observer.html index d7bc15a..e324950 100644 --- a/demo/observer.html +++ b/demo/observer.html @@ -39,6 +39,7 @@ :class="el.isHovered ? 'bg-red-200' : ''" > ``` -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 + +``` + +If you want to use the variable across the 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. + ### Variable Methods MiniJS added some commonly-used custom methods to variables. From e35f0cfbcc6623910adccdf7e0e7e914936b5c2d Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Tue, 13 Feb 2024 02:18:44 +0800 Subject: [PATCH 25/29] feat: add :parent directive --- index.html | 17 +++++++----- lib/entity.js | 24 ++++++++++++++--- lib/state.js | 4 +++ readme.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 108 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index 85e6775..4bac795 100644 --- a/index.html +++ b/index.html @@ -1505,13 +1505,13 @@

    Multi Select:

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

    Multi Select:

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

    Multi Select:

    - + Team 3
    diff --git a/lib/entity.js b/lib/entity.js index e72b48b..b9d0f4b 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) { @@ -15,6 +16,8 @@ export default class Entity { MiniJS.state.addEntity(this) if (MiniJS.debug) this.element.dataset.entityId = this.id + + if (this.element.hasAttribute(':parent')) this.setAsParent() } setAsParent() { @@ -93,11 +96,9 @@ export default class Entity { this.variables = [...new Set(this.variables)] this.variables.forEach((variable) => { - if (variable.startsWith('el.') || variable === '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) } @@ -108,6 +109,19 @@ export default class Entity { const [_, varName] = variable.split('.') MiniJS.state.addEntityVariable(this.id, varName, this.id) } + } else if (State.isParentState(variable)) { + if (!this.parent) this.parent = this.getParent() + + 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 (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) } else { @@ -130,8 +144,10 @@ export default class Entity { el: `proxyWindow['${this.id}']`, } + 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}` }) diff --git a/lib/state.js b/lib/state.js index 87ed511..e671d75 100644 --- a/lib/state.js +++ b/lib/state.js @@ -9,6 +9,10 @@ export class State { return variable.startsWith('el.') || variable === 'el' } + static isParentState(variable) { + return variable.startsWith('parent.') || variable === 'parent' + } + constructor() { this.window = null diff --git a/readme.md b/readme.md index 40511e0..84bfaf3 100644 --- a/readme.md +++ b/readme.md @@ -226,7 +226,9 @@ To use variables only in a current event, you can create a local variable using ``` -If you want to use the variable across the element's attributes and events, you can use `el.`: +### Element Variables + +If you want to use the variable across an element's attributes and events, you can use `el.`: ```html