diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..9d75303 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,82 @@ +import { Attributes, Events, Func, Init, Prop, States, Subscribers } from "./tsimp.types"; +declare class TSimp { + /**Initialize the element by defining its type, parent(query-selector), classes and id*/ + init: Init; + /**Define the text, html or value(attribute) of the element */ + prop: Prop; + /**Add some events that the element will listen to like click, input etc */ + events: Events; + /**Define the attributes of the element */ + attr: Attributes; + /**The actual physical element present in the dom */ + domElement: HTMLElement | undefined; + /**The state object, contains all the states and their current values */ + states: States; + pseudoStates: States; + /**List of all the subscribers and the states they are subscribed to */ + subscribers: Subscribers; + private onmount; + private onunmount; + private onsubscribed; + private onnewsubscriber; + private effects; + private renderCondition; + constructor(init: Init, prop?: Prop, events?: Events, attr?: Attributes); + /**Converts the virtual element into a physical element */ + render(): void; + /**Append the element to the DOM */ + mount(): this; + /**Remove the element from the DOM */ + unMount(): void; + private _directMount; + /**Check if this element is in the DOM */ + isMount(): boolean; + /**Combines render and mount methods and returns the element */ + make(): this; + /**Make an element subscribe to the other so that it can access its states as pseudo-states. + * @param subscriber - the element which will access the states by subscribing to other. + * @param main - the element that'll share its states. + * @param forStates - States of the `main` element to be shared, leave the array empty to trigger all. + */ + static subscribe(subscriber: TSimp, main: TSimp, forStates: string[]): void; + /**States are internal variables that when change automatically update their special references in some specific properties, i.e., `html, text, css, value, class, id` + * @param stateName - name of the state + * @param initialValue - initial value of the state + * @returns Two functions in an array, one to get state (non reactive) another to set state + */ + state(stateName: string, initialValue: T): [(() => T), ((newVal: T | Func) => void)]; + /** + * Effects are functions that get called when some states or pseudoStates (dependencies) change + * @param func - this function will get called when the dependencies change + * @param dependencyArray - add states that will affect the effect, examples: + * - `['$count$', '%color%']` + * - `['f']` + * - `['e']` + * @param onFirst - `default: true`, by default every effect runs on its first render whether the deps change or not. + * */ + effect(func: CallableFunction, dependencyArray: string[], onFirst?: boolean): void; + /**Define a condition for when an element should be in the DOM + * @param condition - function or a text condition that'll return boolean signifying mount or not, eg: + * - Function - `putIf(() => state() > 2)` + * - Text - `putIf('$state$ > 2')` + * - Conditions can include pseudo-states also + * @param stick - if true, the element will be in its position after remounting. Bydefault: `false` + * @returns a [getter and setter] (just like `.state` does) for the "sticky" state + */ + putIf(condition: ((() => boolean) | string), stick?: boolean): [() => boolean, (newVal: boolean | Func) => void]; + /**Called when the element is added to the dom */ + onMount(func: ((didMount?: boolean) => void)): void; + /**Called when the element is removed from the dom */ + onUnmount(func: CallableFunction): void; + /**Called on the element to which the subscriber is subscribing when subscription is added */ + onNewSubscriber(func: CallableFunction): void; + /**Called on the subscriber element when subscription is added */ + onSubscribed(func: CallableFunction): void; + private getState; + private getPState; + private formatString; + private stateExtracter; +} +declare const subscribe: typeof TSimp.subscribe; +export { subscribe }; +export default TSimp; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..308e96b --- /dev/null +++ b/dist/index.js @@ -0,0 +1,334 @@ +const regex = { + stateOperateExp: /{{[a-zA-Z0-9$%+\-*/()\[\]<>?:="'^.! ]+}}/g, + stateExp: /\$[a-zA-Z0-9-]+\$/g, + pStateExp: /%[a-zA-Z0-9-]+%/g, + both: /\$[a-zA-Z0-9-]+\$ | %[a-zA-Z0-9-]+%/g +}; +class TSimp { + constructor(init, prop, events, attr) { + this.init = init; // { type, parent, class("class1 class2"), id } + this.prop = prop; // { text, html, css, value } + this.events = events; // { event: e => e.target.value } + this.attr = attr; // { attributes (eg. title, type) } + this.states = {}; // { "nameOfState": valueOfState } + this.pseudoStates = {}; // --dito-- + this.subscribers = []; // [ { subscriber: El, states: [] } ] + this.effects = []; // [ { func(), deps[], ranOnce:false, onFirst:boolean, currentStates[] } ] + this.renderCondition = () => true; // by default no condition + } + // dom methods: + /**Converts the virtual element into a physical element */ + render() { + this.domElement = this.domElement ? this.domElement : document.createElement(this.init.type); + if (this.init.class) { + const classes = this.init.class.split(' '); + for (let _class of classes) { + if (this.domElement.classList.contains(_class)) + continue; + this.domElement.classList.add(this.formatString(_class)); + } + } + if (this.init.id) { + this.domElement.id = this.formatString(this.init.id); + } + if (this.events) { + Object.keys(this.events).forEach(event => { + // @ts-ignore + this.domElement.addEventListener(event, this.events[event]); + }); + } + if (this.attr != undefined) { + Object.keys(this.attr).forEach(attr => { + if (this.domElement && this.attr) + this.domElement.setAttribute(attr, this.formatString(this.attr[attr])); + }); + } + if (this.prop) { + const text = this.prop.text; + const html = this.prop.html; + const value = this.prop.value; + const css = this.prop.css; + if (this.prop.css) { + for (let property of Object.keys(this.prop.css)) { + //@ts-ignore + this.domElement.style[property] = this.formatString(css[property]); + } + } + if (text) + this.domElement.innerText = this.formatString(text); + if (html) + this.domElement.innerHTML = this.formatString(html); + if (value) + this.domElement.setAttribute('value', this.formatString(value)); + } + if (this.effects) { + const effects = this.effects; + effects.forEach((eff, i) => { + if (eff.deps[0] == 'f') { + if (!eff.ranOnce) { + eff.ranOnce = true; + delete effects[i]; + eff.func(); + } + return; + } + if (eff.deps[0] == 'e') { + let ranOnce = eff.ranOnce; + eff.ranOnce = true; + if (!eff.onFirst && !ranOnce) { + eff.ranOnce = true; + return; + } + eff.func(); + return; + } + const anyChange = eff.deps.filter((dep, i) => { + let currentStateValue = eff.currentStates[i]; + if (typeof currentStateValue == 'object') + currentStateValue = JSON.stringify(currentStateValue); + eff.currentStates[i] = this.formatString(dep); + return this.formatString(dep) != currentStateValue; + }).length != 0; + if (anyChange) { + if (!eff.onFirst && !eff.ranOnce) { + eff.ranOnce = true; + return; + } + eff.func(); + } + else { + if (eff.onFirst && !eff.ranOnce) { + eff.ranOnce = true; + eff.func(); + } + } + }); + } + } + /**Append the element to the DOM */ + mount() { + if (this.renderCondition() && this.isMount()) + return this; + if (this.renderCondition()) + if (this.onmount) + this.onmount(); + let parent = document.querySelector(this.init.parent); + if (!parent) + throw Error(`DOMElement of query: ${this.init.parent} doesn't exist :(`); + if (!this.domElement) + throw Error('No DOMElement attached :('); + if (typeof this.getState('__position__') !== 'number') + this.state('__position__', parent.childNodes.length); + if (!this.renderCondition()) { + if (this.isMount()) { + this.state('__position__', Array.from(parent.children).indexOf(this.domElement)); + this.unMount(); + } + return this; + } + if (this.getState('__stick__')) { + let position = +this.getState('__position__'); + if (position >= parent.childNodes.length) + this._directMount(parent); + else + parent.insertBefore(this.domElement, parent.children.item(position)); + } + else + this._directMount(parent); + return this; + } + /**Remove the element from the DOM */ + unMount() { + if (this.onunmount) + this.onunmount(); + let parent = document.querySelector(this.init.parent); + if (!parent) + throw Error(`DOMElement of query: ${this.init.parent} doesn't exist`); + if (this.domElement) + parent.removeChild(this.domElement); + } + _directMount(parent) { + if (this.domElement) + parent.appendChild(this.domElement); + } + /**Check if this element is in the DOM */ + isMount() { + let parent = document.querySelector(this.init.parent); + if (!parent) + throw Error(`DOMElement of query: ${this.init.parent} doesn't exist`); + if (!this.domElement) + throw Error('No DOMElement attached :('); + return Array.from(parent.children).indexOf(this.domElement) > -1; + } + /**Combines render and mount methods and returns the element */ + make() { + this.render(); + return this.mount(); + } + // out-of-the-box-feature methods + /**Make an element subscribe to the other so that it can access its states as pseudo-states. + * @param subscriber - the element which will access the states by subscribing to other. + * @param main - the element that'll share its states. + * @param forStates - States of the `main` element to be shared, leave the array empty to trigger all. + */ + static subscribe(subscriber, main, forStates) { + forStates = forStates.length == 0 + ? Object.keys(main.states) + : forStates.filter(state => main.states[state] != undefined); + for (let state of forStates) { + subscriber.pseudoStates[state] = main.states[state]; + } + main.subscribers.push({ subscriber: subscriber, states: forStates }); + if (main.onnewsubscriber) + main.onnewsubscriber(); + if (subscriber.onsubscribed) + subscriber.onsubscribed(); + } + /**States are internal variables that when change automatically update their special references in some specific properties, i.e., `html, text, css, value, class, id` + * @param stateName - name of the state + * @param initialValue - initial value of the state + * @returns Two functions in an array, one to get state (non reactive) another to set state + */ + state(stateName, initialValue) { + this.states[stateName] = initialValue; + const setState = (newVal) => { + let stateValue; + //@ts-ignore + if (typeof newVal == 'function') + stateValue = newVal(this.getState(stateName)); + else + stateValue = newVal; + this.state(stateName, stateValue); + this.make(); + const validSubs = this.subscribers.filter(sub => sub.states.includes(stateName)); + validSubs.forEach(sub => { + sub.subscriber.pseudoStates[stateName] = stateValue; + sub.subscriber.render(); + }); + }; + const stateGetter = () => this.getState(stateName); + return [stateGetter, setState]; + } + /** + * Effects are functions that get called when some states or pseudoStates (dependencies) change + * @param func - this function will get called when the dependencies change + * @param dependencyArray - add states that will affect the effect, examples: + * - `['$count$', '%color%']` + * - `['f']` + * - `['e']` + * @param onFirst - `default: true`, by default every effect runs on its first render whether the deps change or not. + * */ + effect(func, dependencyArray, onFirst = true) { + if (dependencyArray[0] == 'f' || dependencyArray[0] == 'e') { + this.effects.push({ + func, deps: dependencyArray, ranOnce: false, onFirst, currentStates: [] + }); + return; + } + dependencyArray = dependencyArray.filter(dep => { + dep = dep.replace(/\$|\%/g, ''); + return this.getState(dep) != undefined || this.getPState(dep) != undefined; + }); + const currentStates = dependencyArray.map(dep => this.formatString(dep)); + this.effects.push({ + func, deps: dependencyArray, + ranOnce: false, onFirst, currentStates + }); + } + /**Define a condition for when an element should be in the DOM + * @param condition - function or a text condition that'll return boolean signifying mount or not, eg: + * - Function - `putIf(() => state() > 2)` + * - Text - `putIf('$state$ > 2')` + * - Conditions can include pseudo-states also + * @param stick - if true, the element will be in its position after remounting. Bydefault: `false` + * @returns a [getter and setter] (just like `.state` does) for the "sticky" state + */ + putIf(condition, stick = false) { + if (typeof condition == 'string') { + this.renderCondition = () => eval(this.formatString(condition)); + } + else + this.renderCondition = condition; + return this.state('__stick__', stick); + } + // inbuilt events + /**Called when the element is added to the dom */ + onMount(func) { + this.onmount = func; + } + /**Called when the element is removed from the dom */ + onUnmount(func) { + this.onunmount = func; + } + /**Called on the element to which the subscriber is subscribing when subscription is added */ + onNewSubscriber(func) { + this.onnewsubscriber = func; + } + /**Called on the subscriber element when subscription is added */ + onSubscribed(func) { + this.onsubscribed = func; + } + // util methods + getState(stateName) { + return this.states[stateName]; + } + getPState(stateName) { + return this.pseudoStates[stateName]; + } + formatString(text) { + var _a; + if (!checkForOperation(text)) + return this.stateExtracter(text); + const operations = text.match(regex.stateOperateExp); + //@ts-ignore - operations is not null, cause we are already checking for it (look up) + for (let rawOperation of operations) { + let operation = rawOperation.replace(/{{|}}/g, ''); + operation = this.stateExtracter(operation); + let afterOperation; + try { + afterOperation = eval(operation); + } + catch (e) { + console.error(`[err] Invalid State Operation:\n\n${rawOperation}\n\n${e}\n\nHint: ` + + `The state(s) in use << ${(_a = operation.match(regex.both)) === null || _a === void 0 ? void 0 : _a.map(s => s.trim())} >> might not exist`); + } + if (typeof afterOperation == 'undefined') + return text; + text = text.replace(rawOperation, afterOperation); + } + return this.stateExtracter(text); + } + stateExtracter(text) { + const stateNames = text.match(regex.stateExp); + const pseudoStateNames = text.match(regex.pStateExp); + if (stateNames) { + for (let stateRaw of stateNames) { + const state = stateRaw.replace(/\$/g, ''); + if (typeof this.states[state] == null) + continue; + let stateVal = this.getState(state); + if (typeof stateVal == 'object') + stateVal = JSON.stringify(stateVal); + text = text.replace(stateRaw, `${stateVal}`); + } + } + if (pseudoStateNames) { + for (let stateRaw of pseudoStateNames) { + const state = stateRaw.replace(/\%/g, ''); + if (this.pseudoStates[state] == undefined) + continue; + let stateVal = this.getPState(state); + if (typeof stateVal == 'object') + stateVal = JSON.stringify(stateVal); + text = text.replace(stateRaw, `${stateVal}`); + } + } + return text; + } +} +const subscribe = TSimp.subscribe; +export { subscribe }; +export default TSimp; +function checkForOperation(text) { + return regex.stateOperateExp.test(text); +} diff --git a/package.json b/package.json index 363329e..8631edc 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "tsimp", - "version": "1.3.5", + "version": "1.3.6", "description": "", - "main": "tsimp.ts", + "main": "./dist/index.js", "repository": { "type": "git", "url": "git+https://github.com/spuckhafte/TSimp.git" diff --git a/tsimp.ts b/src/index.ts similarity index 100% rename from tsimp.ts rename to src/index.ts diff --git a/tsimp.types.d.ts b/src/tsimp.types.d.ts similarity index 100% rename from tsimp.types.d.ts rename to src/tsimp.types.d.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a7743a1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,107 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ES6", /* Specify what module code is generated. */ + "rootDir": "./src", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "exclude": [ + "dist", + "example" + ] +}