From 1d44da8fa8a20edceb584bcf04c1ae49bbbc1de0 Mon Sep 17 00:00:00 2001 From: smtdfc Date: Sat, 9 Mar 2024 01:50:29 +0700 Subject: [PATCH] update --- src/app.js | 19 -- src/app/index.js | 31 ++++ .../component.js => component/index.js} | 72 +++++--- src/component/state.js | 24 +++ src/components/base.js | 17 -- src/components/processing.js | 75 -------- src/components/render.js | 52 ------ src/components/state.js | 17 -- src/dom/attributes.js | 27 +++ src/dom/memorize.js | 7 + src/dom/parser.js | 7 + src/dom/ref.js | 0 src/dom/render.js | 87 +++++++++ src/modules/router/base.js | 168 ------------------ src/modules/router/index.js | 153 +++++++++++++++- src/modules/router/parser.js | 28 --- src/turtle.js | 11 +- 17 files changed, 391 insertions(+), 404 deletions(-) delete mode 100644 src/app.js create mode 100644 src/app/index.js rename src/{components/component.js => component/index.js} (55%) create mode 100644 src/component/state.js delete mode 100644 src/components/base.js delete mode 100644 src/components/processing.js delete mode 100644 src/components/render.js delete mode 100644 src/components/state.js create mode 100644 src/dom/attributes.js create mode 100644 src/dom/memorize.js create mode 100644 src/dom/parser.js create mode 100644 src/dom/ref.js create mode 100644 src/dom/render.js delete mode 100644 src/modules/router/base.js delete mode 100644 src/modules/router/parser.js diff --git a/src/app.js b/src/app.js deleted file mode 100644 index 2813c93..0000000 --- a/src/app.js +++ /dev/null @@ -1,19 +0,0 @@ - -export class TurtleApp{ - constructor(root){ - this.root = root - this.data = {} - this.components = {} - this.modules = [] - } - - use(module,configs ={}){ - this.modules.push(module.init(this,configs)) - } - -} - -export function createApp(root) { - return new TurtleApp(root) -} - diff --git a/src/app/index.js b/src/app/index.js new file mode 100644 index 0000000..e5f400b --- /dev/null +++ b/src/app/index.js @@ -0,0 +1,31 @@ +import {render} from '../dom/render.js'; + +export class TurtleApp { + constructor(root) { + this.root = root + this.data = {} + this.modules = [] + this.html = render.bind(this) + } + + use(module,configs) { + this.modules.forEach(m => { + if (m === module) throw `Module already exists in this app !` + }) + + if (!module.init) { + throw `Cannot find init function of module ` + } else { + module.init(this,configs) + this.modules.push(module) + } + } + + render(content) { + this.root.appendChild(content.root) + } +} + +export function createApp(rootElement) { + return new TurtleApp(rootElement) +} \ No newline at end of file diff --git a/src/components/component.js b/src/component/index.js similarity index 55% rename from src/components/component.js rename to src/component/index.js index bfe27d2..bdb3355 100644 --- a/src/components/component.js +++ b/src/component/index.js @@ -1,32 +1,51 @@ -import { render } from './render.js'; -import {TurtleComponentState} from './state.js'; +import { render } from '../dom/render.js'; +import { createState } from './state.js'; + function evalInScope(js, contextAsScope) { return new Function(`return ${js}`).call(contextAsScope); } -export class TurtleComponent { +class TurtleComponentElement extends HTMLElement { + constructor() { + super() + this._controller = null + } + + connectedCallback() { + this._controller.start(this) + } +} + +window.customElements.define("turtle-component", TurtleComponentElement) + +export class TurtleComponentInstance { constructor(fn, props) { - this._fn = fn.bind(this) - this._props = props - this._base = null + this.fn = fn + this.props = props + this.data = {} + this._memories = {} this._refs = {} + this._element = null this._reactive = true - this._memories = [] this.html = render.bind(this) this.onCreate = new Function() this.onRender = new Function() this.onUpdate = new Function() this.onDestroy = new Function() } - - createState(value){ - return new TurtleComponentState(this,value) + + initState(value) { + let state = createState(value) + state.component = this + return state } - get refs(){ - return this._refs + addUpdateDependents(dependents) { + dependents.forEach(dependent => { + dependent.component = this + }) } - + forceUpdate() { for (let i = 0; i < this._memories.length; i++) { let d = this._memories[i] @@ -45,14 +64,18 @@ export class TurtleComponent { d.node.innerHTML = evalInScope(d.expr, this) } } - + this.onUpdate() } - start() { - this.onCreate() - this._base._c = this - this._base.appendChild(this._fn(this,...this._props)) + start(element) { + this._element = element + let { root, context } = this.requestRender() + + this._element.appendChild(root) + this._memories = context._memories + this._refs = context._refs + for (let i = 0; i < this._memories.length; i++) { let d = this._memories[i] if (d.type == "attr") { @@ -70,15 +93,22 @@ export class TurtleComponent { d.node.innerHTML = evalInScope(d.expr, this) } } + this.onRender() + + } + + requestRender() { + return this.fn.bind(this)(this, ...this.props) } - + } export function createComponent(fn) { let fn_component = function(...props) { - return new TurtleComponent(fn, props) + return new TurtleComponentInstance(fn, props) } - fn_component.instance = TurtleComponent + + fn_component.instance = TurtleComponentInstance return fn_component } \ No newline at end of file diff --git a/src/component/state.js b/src/component/state.js new file mode 100644 index 0000000..31ca5b1 --- /dev/null +++ b/src/component/state.js @@ -0,0 +1,24 @@ +import { TurtleComponentInstance } from './index.js'; + +class TurtleComponentState { + constructor(component, value) { + this.component = component + this._value = value + } + + get value() { + return this._value + } + + set value(val) { + this._value = val + if (this.component instanceof TurtleComponentInstance) { + if (!this.component._reactive) return + this.component.forceUpdate() + } + } +} + +export function createState(value) { + return new TurtleComponentState(null, value) +} \ No newline at end of file diff --git a/src/components/base.js b/src/components/base.js deleted file mode 100644 index 844c9d6..0000000 --- a/src/components/base.js +++ /dev/null @@ -1,17 +0,0 @@ -export class TurtleBaseComponent extends HTMLElement { - constructor() { - super() - this._controller = null - this._c = null - } - - connectedCallback(){ - this._controller(this) - } - - disconnectedCallback(){ - this._c.onDestroy() - } -} - -window.customElements.define("turtle-component",TurtleBaseComponent) \ No newline at end of file diff --git a/src/components/processing.js b/src/components/processing.js deleted file mode 100644 index 58cc4c1..0000000 --- a/src/components/processing.js +++ /dev/null @@ -1,75 +0,0 @@ -import {TurtleComponent} from './component.js'; - -function checkAndExtractId(inputString) { - const regex = /^TurtleComponent_([a-zA-Z0-9]+)$/; - const match = inputString.match(regex); - - if (match) { - const id = match[1]; - return id; - } else { - return null; - } -} - -export function processAttributes(node, root, context = {}) { - if (node && root) { - for (var i = 0; i < node.attributes.length; i++) { - let attribute = node.attributes[i]; - if (attribute.localName.indexOf('bind-') === 0) { - let attrName = attribute.localName.substring('bind-'.length); - if (context.root instanceof TurtleComponent) { - context.root._memories.push({ type: "attr", attr: attrName, node: root, expr: attribute.value }) - root.setAttribute(attrName, attribute.value); - } - } else if (attribute.localName.indexOf('on-') === 0) { - let eventName = attribute.localName.substring('on-'.length); - root.addEventListener(eventName, evalInScope(attribute.value, context)) - } else if (attribute.localName == "t-text") { - if (context.root instanceof TurtleComponent) { - context.root._memories.push({ type: "text", node: root, expr: attribute.value }) - } - } else if (attribute.localName == "t-ref") { - context.root._refs[attribute.value] = root - } else if (attribute.localName == "t-html") { - if (context.root instanceof TurtleComponent) { - context.root._memories.push({ type: "html", node: root, expr: attribute.value }) - } - } else { - root.setAttribute(attribute.name, attribute.value); - } - } - } -} - - -export function processing(dom, contents, context) { - for (let i = 0; i < contents.childNodes.length; i++) { - let node = contents.childNodes[i] - - if (node.nodeType === Node.ELEMENT_NODE) { - let name = node.tagName - let element = document.createElement(node.tagName) - if (element instanceof HTMLUnknownElement) { - if (name.indexOf("TurtleComponent_") != -1) { - let id = checkAndExtractId(name) - if (id) { - if (context.data.components[id]) { - element = document.createElement("turtle-component") - element.setAttribute("id", id) - element._controller = context.data.components[id] - dom.appendChild(element) - } - } - } - } else { - processAttributes(node, element, context) - processing(element, node, context) - dom.appendChild(element) - } - } else if (node.nodeType === Node.TEXT_NODE) { - dom.appendChild(document.createTextNode(node.textContent)) - } - } - -} \ No newline at end of file diff --git a/src/components/render.js b/src/components/render.js deleted file mode 100644 index c28b4c5..0000000 --- a/src/components/render.js +++ /dev/null @@ -1,52 +0,0 @@ -import { TurtleComponent } from './component.js'; -import { processing } from './processing.js'; - - -export function render(str, ...values) { - let data = { - components: {} - } - - for (let i = 0; i < values.length; i++) { - let value = values[i] - if (value instanceof TurtleComponent) { - let key = (Math.floor(Math.random() * 9999) * Date.now()).toString(16) - data.components[key] = function(base) { - value._base = base - return value.start() - } - - values[i] = `TurtleComponent_${key}` - } - - if (value) { - if (value.instance === TurtleComponent) { - let key = (Math.floor(Math.random() * 9999) * Date.now()).toString(16) - value = value() - data.components[key] = function(base) { - value._base = base - return value.start() - } - values[i] = `TurtleComponent_${key}` - - } - } - } - - let content = new DOMParser().parseFromString( - `${String.raw(str,...values)}`, - "text/xml" - ).querySelector("root") - - let fragment = document.createDocumentFragment() - processing( - fragment, - content, - { - data: data, - root: this - } - ) - - return fragment -} \ No newline at end of file diff --git a/src/components/state.js b/src/components/state.js deleted file mode 100644 index 7025268..0000000 --- a/src/components/state.js +++ /dev/null @@ -1,17 +0,0 @@ -export class TurtleComponentState{ - constructor(component,value){ - this._component = component - this._value = value - } - - set val(value){ - this._value = value - if(this._component._reactive){ - this._component.forceUpdate() - } - } - - get val(){ - return this._value - } -} \ No newline at end of file diff --git a/src/dom/attributes.js b/src/dom/attributes.js new file mode 100644 index 0000000..34cd79c --- /dev/null +++ b/src/dom/attributes.js @@ -0,0 +1,27 @@ +function evalInScope(js, contextAsScope) { + return new Function(`return ${js}`).call(contextAsScope); +} + +export function processAttributes(node, root, context = {}, scope) { + if (node && root) { + for (var i = 0; i < node.attributes.length; i++) { + let attribute = node.attributes[i]; + if (attribute.localName.indexOf('bind-') === 0) { + let attrName = attribute.localName.substring('bind-'.length); + context._memories.push({ type: "attr", attr: attrName, node: root, expr: attribute.value }) + root.setAttribute(attrName, attribute.value); + } else if (attribute.localName.indexOf('on-') === 0) { + let eventName = attribute.localName.substring('on-'.length); + root.addEventListener(eventName, evalInScope(attribute.value, scope)) + } else if (attribute.localName == "t-text") { + context._memories.push({ type: "text", node: root, expr: attribute.value }) + } else if (attribute.localName == "t-ref") { + context._refs[attribute.value] = root + } else if (attribute.localName == "t-html") { + context._memories.push({ type: "html", node: root, expr: attribute.value }) + } else { + root.setAttribute(attribute.name, attribute.value); + } + } + } +} diff --git a/src/dom/memorize.js b/src/dom/memorize.js new file mode 100644 index 0000000..09debb3 --- /dev/null +++ b/src/dom/memorize.js @@ -0,0 +1,7 @@ +export class TurtleMemorizeContext{ + constructor(component){ + this.component = component + this._memories = [] + this._refs = {} + } +} \ No newline at end of file diff --git a/src/dom/parser.js b/src/dom/parser.js new file mode 100644 index 0000000..b36d547 --- /dev/null +++ b/src/dom/parser.js @@ -0,0 +1,7 @@ +export function parserContent(content) { + content = new DOMParser().parseFromString( + `${content}`, + "text/xml" + ).querySelector("root") + return content +} \ No newline at end of file diff --git a/src/dom/ref.js b/src/dom/ref.js new file mode 100644 index 0000000..e69de29 diff --git a/src/dom/render.js b/src/dom/render.js new file mode 100644 index 0000000..fb6d0bd --- /dev/null +++ b/src/dom/render.js @@ -0,0 +1,87 @@ +import { TurtleComponentInstance } from '../component/index.js'; +import { TurtleMemorizeContext } from './memorize.js'; +import { parserContent } from './parser.js'; +import {processAttributes} from './attributes.js'; +function checkAndExtractId(inputString) { + const regex = /^TurtleComponent_([a-zA-Z0-9]+)$/; + const match = inputString.match(regex); + + if (match) { + const id = match[1]; + return id; + } else { + return null; + } +} + + + +function processing(root, contents, context, data, scope) { + for (let i = 0; i < contents.childNodes.length; i++) { + let node = contents.childNodes[i] + + if (node.nodeType === Node.ELEMENT_NODE) { + let name = node.tagName + + let element = document.createElement(node.tagName) + if (element instanceof HTMLUnknownElement) { + + if (name.indexOf("TurtleComponent_") != -1) { + let id = checkAndExtractId(name) + + if (id) { + if (data.components[id]) { + element = document.createElement("turtle-component") + element.setAttribute("id", id) + element._controller = data.components[id] + root.appendChild(element) + } + } + } + } else { + processAttributes(node, element, context, scope) + processing(element, node, context) + root.appendChild(element) + } + } else if (node.nodeType === Node.TEXT_NODE) { + root.appendChild(document.createTextNode(node.textContent)) + } + } + +} + +export function render(str, ...values) { + let renderData = { + components: {}, + } + + for (let i = 0; i < values.length; i++) { + let value = values[i] + let key = (Math.floor(Math.random() * 99999) * Date.now()).toString(16) + if (value instanceof Function && value.instance === TurtleComponentInstance) { + renderData.components[key] = value([]) + values[i] = `TurtleComponent_${key}` + } + + if (value instanceof TurtleComponentInstance) { + renderData.components[key] = value + values[i] = `TurtleComponent_${key}` + } + } + + let root = document.createDocumentFragment() + let context = new TurtleMemorizeContext(this) + let content = parserContent(String.raw(str, ...values)) + processing(root, content, context, renderData,this) + return { + root, + context, + content, + refs:context._refs + } +} + +export function attach(element, content){ + element.textContent = null + element.appendChild(content.root) +} \ No newline at end of file diff --git a/src/modules/router/base.js b/src/modules/router/base.js deleted file mode 100644 index 3863fc9..0000000 --- a/src/modules/router/base.js +++ /dev/null @@ -1,168 +0,0 @@ -import { matches } from './parser.js'; -import { render } from '../../components/render.js'; -export class TurtleRouterModule { - constructor(app, configs) { - this.element = configs.element ?? document.createElement("div") - this.routes = {} - this.params = {} - this.events = { - pagenotfound: [], - notallow: [], - routechanged: [], - loadcontentfailed: [] - } - this.query = new URLSearchParams() - this.app = app - this.matched = null - app.router = this - this.currentContext = null - this.render = render - this.render = this.render.bind(this) - } - - static init(app, configs) { - return new TurtleRouterModule(app, configs) - } - - async matches(path) { - if (path.length == 0) path = "/" - let matched = false - let url = new URL(path, window.location.origin) - path = url.pathname - if (path.length == 0) path = "/" - for (let i = 0; i < Object.keys(this.routes).length; i++) { - let pattern = Object.keys(this.routes)[i] - let result = matches(pattern, path) - if (result.matched) { - if(this.currentContext){ - if(this.currentContext.onRouteChange) this.currentContext.onRouteChange() - } - matched = true - let content_fn = {} - let route_info = this.routes[pattern] - if (route_info.beforeLoadContent) route_info.beforeLoadContent.bind(this)() - this.matched = pattern - this.params = result.params - this.url = path - if (route_info.protect) { - let res = await route_info.protect() - if (!res) { - this.triggerEvent("notallow") - return - } - } - this.query = url.searchParams - if (route_info.loader) content_fn = await route_info.loader.bind(this)() - if (route_info.content) content_fn = route_info.content - if (route_info.onContentLoaded) { - try { - route_info.onContentLoaded.bind(this)() - } catch (err) { - this.triggerEvent("loadcontentfailed", err) - } - } - - if (content_fn.template) { - const context = { - app: this.app, - _memories: [], - _refs: {}, - get refs() { - return this._refs - }, - - } - - context.html=render.bind(context) - this.currentContext = context - let template = content_fn.template.bind(context)(context, result) - this.element.textContent = "" - this.element.appendChild(template) - if (context.onRender) context.onRender() - } - - if (route_info.callback) { route_info.callback.bind(this)() } - if (content_fn.configs) { - let configs = content_fn.configs - document.title = configs.title ?? document.title - } - this.triggerEvent("routechanged") - break - } - } - - if (!matched) { - this.triggerEvent("pagenotfound") - } - } - - on(name, callback) { - this.events[name].push(callback) - } - - async redirect(path, replace = false) { - if (!replace) { - window.location.hash = path - } else { - window.history.replaceState(null, null, `#${path}`) - path = window.location.hash.slice(1) - await this.matches(path) - } - } - - forceDisplayPage(content_fn){ - if (content_fn.template) { - const context = { - app: this.app, - _memories: [], - _refs: {}, - get refs() { - return this._refs - }, - - } - - context.html = render.bind(context) - this.currentContext = context - let template = content_fn.template.bind(context)(context, result) - this.element.textContent = "" - this.element.appendChild(template) - if (context.onRender) context.onRender() - } - } - - triggerEvent(name) { - this.events[name].forEach(callback => { - callback() - }) - } - - - start() { - let ctx = this - - window.addEventListener("hashchange", function(e) { - let path = window.location.hash.slice(1) - ctx.matches(path) - }) - - window.addEventListener("click", function(event) { - let target = event.target - if (target.getAttribute("t-redirect")) { - event.preventDefault() - let path = target.getAttribute("t-redirect") - window.location =`#${path}` - } - }) - - let path = window.location.hash.slice(1) - this.matches(path) - } - - createPage(fn, configs) { - return { - template: fn, - configs: configs - } - } -} \ No newline at end of file diff --git a/src/modules/router/index.js b/src/modules/router/index.js index b4208ef..160dbc6 100644 --- a/src/modules/router/index.js +++ b/src/modules/router/index.js @@ -1 +1,152 @@ -export * from "./base.js" +import { attach, render } from '../../dom/render.js'; + +export class TurtleRouterModule { + constructor(app, configs) { + this.root = configs.root ?? document.createElement("div") + this.app = app + this.app.router = this + this.routes = {} + this.matched = null + this.url = null + this.params = {} + this.query = new URLSearchParams() + this.events = { + notallow: [], + notfound: [] + } + } + + on(event, callback) { + this.events[event].push(callback) + } + + off(event, callback) { + this.events[event].forEach((fn, idx) => { + if (fn === callback) { + this.events[event].splice(idx, 1) + } + }) + } + + static init(app, configs) { + return new TurtleRouterModule(app, configs) + } + + async matches(url) { + let u = new URL(url, window.location.origin) + url = u.pathname + for (let j = 0; j < Object.keys(this.routes).length; j++) { + let route = Object.keys(this.routes)[j] + let configs = this.routes[route] + let routeSplited = route.split("/") + let urlSplited = url.split("/") + let passed = true + let params = {} + + for (let i = 0; i < routeSplited.length; i++) { + + if (urlSplited[i] === undefined) { + passed = false + break + } + + if (routeSplited[i] == "*") { + continue + } + + if (routeSplited[i][0] == ":") { + let name = routeSplited[i].substring(1, routeSplited[i].length) + params[name] = urlSplited[i] + continue + } + + if (routeSplited[i] != urlSplited[i]) { + passed = false + break + } + } + + if (passed) { + this.params = params + this.query = u.searchParams + this.matched = route + this.url = url + let component = new Function() + if (configs.callback) { await configs.callback() } + if (configs.protect) { + let result = await configs.protect() + if (!result) { + this.triggerError("not_allow") + return + } + } + + if (configs.loader) { + component = await configs.loader() + } + + if (configs.component) { + component = configs.component + } + + attach(this.root, render` + <${component(this)}/> + `) + return + } + } + + this.triggerError("not_found") + } + + start() { + window.addEventListener("hashchange", function() { + let path = window.location.hash + if (path.length == 0) { + window.location.hash = "#!/" + path = "/" + } else { + path = path.slice(2) + } + this.matches(path) + }.bind(this)) + + let path = window.location.hash + + if (path.length == 0) { + window.location.hash = "#!/" + path = "/" + } else { + path = path.slice(2) + } + + this.matches(path) + } + + redirect(path, replace = false) { + if (!replace) { + window.location.hash = `#!${path}` + } else { + window.history.replaceState(null, null, `./#!${path}`) + this.matches(path) + } + } + + emitEvent(name, data) { + this.events[name].forEach(fn => { + fn(data) + }) + } + + triggerError(name) { + switch (name) { + case 'not_allow': + this.emitEvent("notallow", this) + break; + + case 'not_found': + this.emitEvent("notfound", this) + break; + } + } +} \ No newline at end of file diff --git a/src/modules/router/parser.js b/src/modules/router/parser.js deleted file mode 100644 index 0a9067d..0000000 --- a/src/modules/router/parser.js +++ /dev/null @@ -1,28 +0,0 @@ - -export function matches(pattern, urlToMatch) { - const patternParts = pattern.split('/'); - const urlParts = urlToMatch.split('/'); - - if (patternParts.length !== urlParts.length) { - return { matched: false }; - } - - const params = {}; - - for (let i = 0; i < patternParts.length; i++) { - const patternPart = patternParts[i]; - const urlPart = urlParts[i]; - - if (patternPart.startsWith(':')) { - const paramName = patternPart.slice(1); - params[paramName] = urlPart; - } else if (patternPart === '*') { - continue - } else if (patternPart !== urlPart) { - return { matched: false }; - } - } - - return { matched: true, params }; -} - diff --git a/src/turtle.js b/src/turtle.js index 30de559..977c213 100644 --- a/src/turtle.js +++ b/src/turtle.js @@ -1,10 +1,9 @@ -import {} from './components/base.js'; -export * from "./components/component.js" -export { render } from "./components/render.js" +export {render as fragment} from './dom/render.js'; +export * from "./component/index.js" +export * from "./component/state.js" +export * from "./app/index.js" export * from "./modules/router/index.js" -export * from "./app.js" - -export function addScript(src, asyncLoad=false,script) { +export function addScript(src, asyncLoad = false, script) { let d = document return new Promise((resolve, reject) => { script = d.createElement('script');