diff --git a/demo/observer.html b/demo/observer.html new file mode 100644 index 0000000..5fd50e1 --- /dev/null +++ b/demo/observer.html @@ -0,0 +1,80 @@ + + + + + + Mini Demo Observer + + + + + + +
    +
  1. + +
  2. +
  3. + +
  4. +
  5. + +
  6. +
  7. + +
  8. +
  9. …More will be added after 3 seconds…
  10. +
+ + + + diff --git a/index.html b/index.html index 81ccd37..64233f1 100644 --- a/index.html +++ b/index.html @@ -1,245 +1,329 @@ + + + + MiniJS + + + + - - - - MiniJS - - - - - - - - - - -
-

MiniJS

-
- -
-
-
-

AirBnB Search Bar:

+ + + + + +
+
+

MiniJS

+ +
+
-
- +
+
+
+

AirBnB Search Bar:

+
+ +
+ + +
+ +
-
- -
- -
-
-
-

Active State Example:

- -
- -
- -
+ +
+
+
+

Active State Example:

+ +
+ +
+ + -
-
-
- -
-
-
-

On Change Example:

- -
- -
-
-
-
-

Input:

-
-

Hello World

- +
+ +
+
+
+

On Change Example:

+ +
+ +
+
+
+
+

Input:

+ +
+

+ Hello World +

+
+ -
- -
-
-
-

Checkbox:

-
- -

OFF

-
-
+ +
+
+
+

Storing to Local Storage:

+ +
+ +
+
+ +

+ Update and Reload +

+
-
-
-
-
- -
-
-
-

Multi Select:

- -
+
-
- +
+
+
+

Multi Select:

+ +
- - + Flavor + + - - -

Selected Tags:

-
-
- - Selected Tags:

+
+
+ + - x - + > + x + +
-
- - -
- -
+ + +
+ +
+
+ About Us +
+
+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod. +
+
+
+
+ Contact Us +
+
+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod. +
+
+
-
- -
-
- About Us -
-
- Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod. -
-
-
- -
- Contact Us -
-
- Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod. -
-
-
- -
- Team 3 -
-
- Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod. -
-
-
-
-
- - -
-
-
-

Alert Dialog:

- + Team 3 +
+
+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod. +
+
+
+
-
- - - +
+
+
+

Alert Dialog:

+ +
-

- Your account has been deleted. +

+ -

- -
+ Your account has been deleted. + + +

+ + + -

- Are you sure you want to delete your account? -

- -

- This action cannot be undone. This will permanently delete your account and remove all your data. -

- -
- - -
- -
-
+ > +

+ Are you sure you want to delete your account? +

- + +
-
- - -
-
-
-

Dialog:

- -
+
-
- - -
-
-

- - -
- -
-
Your Name:
-
-
Username:
-
-
+ + + Show Code +
- -
-

- Edit Profile -

- -
-
- - -
- -
- - -
+
+ -
- - -
-
+
+
+

-
-
- -
-
- + + +
+

Edit Profile

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
+
+ + - - - + -
-
-
-

Tonic Dialog:

- -
+
+
+
+

Tonic Dialog:

+ +
-
- - - - Open Modal - - -
- - - \ No newline at end of file +
+ + diff --git a/lib/entity.js b/lib/entity.js index 5faba54..89416c4 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -52,7 +52,9 @@ export default class Entity { const RESERVED_KEYWORDS = ['$', 'window', 'document', 'console'] const CUSTOM_ATTRIBUTES = [':each', ':class', ':text', ':value', ':checked'] - ;[...this.dynamicAttributes, ...CUSTOM_ATTRIBUTES].forEach((name) => { + const attributes = [...this.dynamicAttributes, ...CUSTOM_ATTRIBUTES] + + attributes.forEach((name) => { const attr = this.element.attributes[name] if (!attr) return @@ -115,8 +117,10 @@ export default class Entity { 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) + variable.replace('el.', this.uuid + '.') ) if (!this.variables.includes(this.uuid)) this.variables.push(this.uuid) @@ -158,14 +162,12 @@ export default class Entity { const engine = new Engine(expr, options) const ids = { $: 'document.querySelector' } + if (this.parent?.uuid) ids.el = `proxyWindow['${this.parent.uuid}']` + this.variables.forEach((variable) => { - if (variable.startsWith('el.')) { - const members = variable.split('.') - ids[variable] = - `proxyWindow['${this.parent.uuid}'].` + members.slice(1).join('.') - } else { - ids[variable] = `proxyWindow-${variable}` - } + if (variable.startsWith('el.') || variable === 'el') return + + ids[variable] = `proxyWindow-${variable}` }) engine.replace(ids, ['declared']) @@ -194,6 +196,37 @@ export default class Entity { return 'Entity' + Date.now() + Math.floor(Math.random() * 10000) } + async init(shouldAdd = false) { + this.getVariables() + this.applyEventBindings() + await this.evaluateAll() + + if (shouldAdd || !this.isInsideEachElement()) MiniJS.elements.push(this) + } + + initChildren() { + const elements = this.element.querySelectorAll('*') + + for (let i = 0; i < elements.length; i++) { + const element = elements[i] + if (element.nodeType !== 1) continue + + const entity = new Entity(element) + entity.init(true) + } + } + + isInsideEachElement() { + let value = this.element.parentElement + + while (value) { + if (value.hasAttribute && value.hasAttribute(':each')) return true + value = value.parentElement + } + + return false + } + async evaluateEventAction(attrName) { const attrVal = this.element.getAttribute(attrName) await this._interpret(attrVal) @@ -234,17 +267,7 @@ export default class Entity { }) this.element.innerHTML = newHTML - - const elements = this.element.querySelectorAll('*') - const entities = [] - - for (let i = 0; i < elements.length; i++) { - const entity = new Entity(elements[i]) - entity.getVariables() - entity.applyEventBindings() - await entity.evaluateAll() - MiniJS.elements.push(entity) - } + this.initChildren() } } @@ -377,4 +400,37 @@ export default class Entity { hasAttribute(attr) { return !!this.element.getAttribute(attr) } + + dispose() { + const elements = [ + this.element, + ...Array.from(this.element.querySelectorAll('*')), + ] + + // 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 unusedVariables = MiniJS.variables.filter( + (variable) => !usedVariables.includes(variable) + ) + + MiniJS.variables = [...usedVariables] + + unusedVariables.forEach((variable) => { + if (variable.startsWith('el.')) { + const varName = variable.replace('el.', '') + delete window[this.uuid][varName] + } else { + delete window[variable] + } + }) + } } diff --git a/lib/generators/interpreter.js b/lib/generators/interpreter.js index dc5a835..36a72f9 100644 --- a/lib/generators/interpreter.js +++ b/lib/generators/interpreter.js @@ -35,7 +35,7 @@ export class Interpreter extends Lexer { super(code, options) } - async interpret(context, ids) { + async interpret(context) { const code = super.output() // Needed for proxy variables, should be nice to pass only used variables instead of the proxyWindow const scope = { ...DEFAULT_SCOPE, proxyWindow: MiniJS.window } diff --git a/lib/generators/observer.js b/lib/generators/observer.js new file mode 100644 index 0000000..d936815 --- /dev/null +++ b/lib/generators/observer.js @@ -0,0 +1,25 @@ +const MutationObserver = + window.MutationObserver || window.WebKitMutationObserver + +export function observeDOM(obj, callback) { + if (obj == null || obj.nodeType !== 1) return + + if (MutationObserver) { + // define a new observer + const mutationObserver = new MutationObserver(callback) + + // have the observer observe for changes in children + mutationObserver.observe(obj, { childList: true, subtree: true }) + + return mutationObserver + // browser support fallback + } else if (window.addEventListener) { + obj.addEventListener('DOMNodeInserted', callback, false) + obj.addEventListener('DOMNodeRemoved', callback, false) + + return () => { + obj.removeEventListener('DOMNodeInserted', callback, false) + obj.removeEventListener('DOMNodeRemoved', callback, false) + } + } +} diff --git a/lib/main.js b/lib/main.js index a80b1fd..71438b9 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,6 +1,7 @@ import Entity from './entity' import MiniArray from './helpers/array' import { Lexer } from './generators/lexer' +import { observeDOM } from './generators/observer' let nativeProps = Object.getOwnPropertyNames(window) @@ -97,11 +98,12 @@ const MiniJS = (() => { _applyBindings() _addHelpers() updateStates() + _listenToDOMChanges() // Temporarily commented out - to be reviewed // _evaluateLoadEvents(); const endTime = performance.now() const executionTime = endTime - startTime - console.log(`myFunction took ${executionTime}ms to run.`) + console.log(`MiniJS took ${executionTime}ms to run.`) } function _addHelpers() { @@ -114,6 +116,26 @@ const MiniJS = (() => { }) } + function _listenToDOMChanges() { + observeDOM(document.body, (mutation) => { + mutation.forEach((record) => { + record.removedNodes.forEach((node) => { + const entity = _elements.find((entity) => entity.element === node) + entity?.dispose() + }) + + if (record.addedNodes.length) { + record.addedNodes.forEach((node) => { + if (node.nodeType !== 1) return + const entity = new Entity(node) + entity.init() + entity.initChildren() + }) + } + }) + }) + } + function _addMethodsToVariables(variables = _variables) { variables.forEach((variable) => { if ( @@ -184,25 +206,16 @@ const MiniJS = (() => { } function _findElements() { - var elems = document.body.getElementsByTagName('*') + const elems = document.body.getElementsByTagName('*') for (let i = 0; i < elems.length; i++) { const elem = elems[i] + if (elem.nodeType !== 1) continue + const entity = new Entity(elem) - if (!isInsideEachElement(entity.element.parentElement)) { - _elements.push(entity) - } - } - } - function isInsideEachElement(element) { - while (element) { - if (element.hasAttribute && element.hasAttribute(':each')) { - return true - } - element = element.parentElement + if (!entity.isInsideEachElement()) _elements.push(entity) } - return false } function _domReady() { @@ -230,7 +243,7 @@ const MiniJS = (() => { return _elements }, set elements(newElements) { - return newElements + _elements = newElements }, get variables() { return _variables