├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── compile.js ├── editor.config ├── eslint.json ├── package.json ├── src ├── core │ ├── component-factory.ts │ ├── i-inst-config.ts │ ├── i-inst-watched.ts │ ├── index.ts │ ├── nodes-action.ts │ ├── nodes-watched.ts │ └── platform.ts └── incremental-dom.ts ├── test ├── app │ ├── main-app.html │ ├── main-app.html.js │ └── main-app.js └── index.html └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | package-lock.json 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | #dist dir 41 | dist 42 | test/node_modules 43 | 44 | #.npmrc 45 | .npmrc 46 | 47 | change-commiter.sh 48 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | jspm_packages 3 | src 4 | assets 5 | tasks 6 | package-lock.json 7 | tslint.json 8 | tsconfig.json 9 | editor.config 10 | Gulpfile.js 11 | 12 | #dist dir 13 | dist/example 14 | dist/incremental-dom.js 15 | # dist/index.js 16 | 17 | test 18 | test/node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 alex happy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ***FerrugemJS*** 2 | 3 | ![Ferrugem Logo](/assets/img/new-logo-home.png) 4 | 5 | **A simple, reactive, conventional and non-intrusive library!** 6 | ***FerrugemJS*** is inspired by [Aurelia](http://aurelia.io/) and [React](https://facebook.github.io/react/) using [Incremental DOM](http://google.github.io/incremental-dom/) with a html template engine. 7 | 8 | ## Documentation/Site project 9 | https://ferrugemjs.github.io/home-page/ -------------------------------------------------------------------------------- /compile.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | var fjsparse = require("@ferrugemjs/compile/parse/parse"); 3 | 4 | const filePath = "test/app/main-app.html"; 5 | 6 | // console.log(fjsparse.default('', {})); 7 | 8 | fs.readFile(filePath, function (err, buf) { 9 | const compiledStr = fjsparse.default(buf.toString(), { 10 | templateExtension: ".html", 11 | viewModel: "main-app", 12 | env: "production" // default is "development" 13 | }) 14 | 15 | fs.writeFile(`${filePath}.js`, compiledStr, (err) => { 16 | if (err) console.log(err); 17 | console.log("Successfully Written to File."); 18 | }); 19 | }); 20 | 21 | -------------------------------------------------------------------------------- /editor.config: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [], 3 | "rules": { 4 | "max-line-length": { 5 | "options": [220] 6 | }, 7 | "member-access": true, 8 | "no-any": false, 9 | "triple-equals": true, 10 | "class-name": true, 11 | "prefer-template": true, 12 | "indent": [true, "spaces", 2], 13 | "quotemark": [true, "single"], 14 | "no-unnecessary-callback-wrapper": true, 15 | "semicolon": [true, "always"], 16 | "space-before-function-paren": [true, {"anonymous": "always", "named": "always", "asyncArrow": "always","method":"never","constructor":"never"}], 17 | "curly": true, 18 | "one-variable-per-declaration": [true, "ignore-for-loop"], 19 | "interface-name": [true, "always-prefix"], 20 | "new-parens": true, 21 | "no-arg": true, 22 | "no-bitwise": true, 23 | "no-conditional-assignment": true, 24 | "no-consecutive-blank-lines": true, 25 | "no-console": { 26 | "options": [ 27 | "debug", 28 | "info", 29 | "time", 30 | "timeEnd", 31 | "trace" 32 | ] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ferrugemjs/library", 3 | "version": "3.0.0", 4 | "description": "A reactive library.", 5 | "main": "dist/core/index.js", 6 | "scripts": { 7 | "build": "node node_modules/.bin/tsc --p tsconfig.json", 8 | "eslint": "eslint -c eslint.json ./src/**/*.ts", 9 | "copy:dist": "copyfiles -a ./dist/**/*.js test/node_modules/@ferrugemjs/library", 10 | "copy:requirejs": "copyfiles ./node_modules/requirejs/require.js test/", 11 | "copy:idom": "copyfiles ./node_modules/incremental-dom/dist/incremental-dom.js test/", 12 | "copy:tslib": "copyfiles ./node_modules/tslib/tslib.js test/", 13 | "compile": "node ./compile.js", 14 | "preview": "npm run compile && npm run copy:dist && npm run copy:requirejs && npm run copy:idom && http-server -p 8114 test/" 15 | }, 16 | "author": "ferrugemjs", 17 | "license": "ISC", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/ferrugemjs/library.git" 21 | }, 22 | "devDependencies": { 23 | "@ferrugemjs/compile": "3.0.2", 24 | "copyfiles": "2.4.1", 25 | "eslint": "8.34.0", 26 | "http-server": "14.1.1", 27 | "requirejs": "2.3.6", 28 | "typescript": "4.9.5" 29 | }, 30 | "dependencies": { 31 | "incremental-dom": "0.7.0", 32 | "tslib": "2.5.0" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/ferrugemjs/library/issues" 36 | }, 37 | "homepage": "https://github.com/ferrugemjs/library#readme" 38 | } -------------------------------------------------------------------------------- /src/core/component-factory.ts: -------------------------------------------------------------------------------- 1 | import { patch, notifications, attributes } from 'incremental-dom'; 2 | import inst_watched from './nodes-watched'; 3 | import { detacheNode, attacheNode } from './nodes-action'; 4 | 5 | notifications.nodesDeleted = function (nodes: HTMLDivElement[]) { 6 | nodes.forEach(detacheNode); 7 | }; 8 | 9 | notifications.nodesCreated = function (nodes: HTMLDivElement[]) { 10 | nodes.forEach(attacheNode); 11 | }; 12 | attributes.value = function (el: any, name: string, value: any) { 13 | el.value = value === null || typeof (value) === 'undefined' ? '' : value; 14 | }; 15 | attributes.checked = function (el: any, name: string, value: any) { 16 | el.checked = !!value; 17 | }; 18 | 19 | export default (p_module:any , props_inst:{[key:string]:any}, {key_id,is}:{is:string, key_id:string}) => { 20 | 21 | return { 22 | content : (cont?: Function) => { 23 | 24 | delete props_inst['key_id']; 25 | delete props_inst['is']; 26 | delete props_inst['key:id']; 27 | if(props_inst['prop:values']){ 28 | props_inst = {...props_inst, ...props_inst['prop:values']}; 29 | delete props_inst['prop:values']; 30 | } 31 | let propsAfterAttached = {}; 32 | for (let propOrign in props_inst) { 33 | let prop: string = propOrign; 34 | 35 | if(prop.indexOf('-') > -1){ 36 | prop = propOrign.toLowerCase().replace(/-(.)/g, function (match, group1) { 37 | return group1.toUpperCase(); 38 | }); 39 | props_inst[prop] = props_inst[propOrign]; 40 | delete props_inst[propOrign]; 41 | }else if(prop.indexOf('.') > -1){ 42 | propsAfterAttached[propOrign] = props_inst[propOrign]; 43 | delete props_inst[propOrign]; 44 | } 45 | } 46 | 47 | if(key_id && !inst_watched[key_id]){ 48 | const inst = new p_module(props_inst); 49 | const proxy_inst = new Proxy(inst, { 50 | set: function (target, prop, value) { 51 | //console.log(`'${String(prop)}' change from '${target[prop]}' to '${value}'`); 52 | target[prop] = value; 53 | 54 | setTimeout( 55 | proxy_inst.$draw.bind(proxy_inst, {key_id, is}), 56 | 0 57 | ); 58 | 59 | return true; 60 | } 61 | }); 62 | 63 | proxy_inst.$draw = function({key_id,is}:any){ 64 | //console.log(key_id,is,this); 65 | patch( 66 | document.getElementById(key_id), 67 | this.$render.bind(this,{key_id, is, loaded:true}), 68 | this 69 | ) 70 | } 71 | inst_watched[key_id] = {$loaded: false, inst: proxy_inst, $is:is, $propsAfterAttached:propsAfterAttached}; 72 | } 73 | 74 | if(cont){ 75 | inst_watched[key_id].inst.$content = cont; 76 | } 77 | if(inst_watched[key_id].$loaded && Object.keys(props_inst).length && inst_watched[key_id].inst && inst_watched[key_id].inst.propsChanged){ 78 | inst_watched[key_id].inst.propsChanged(props_inst); 79 | } 80 | return inst_watched[key_id].inst; 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/core/i-inst-config.ts: -------------------------------------------------------------------------------- 1 | export interface IInstConfig { 2 | classFactory: any; 3 | hostVars: {}; 4 | staticVars: {}; 5 | tag: string; 6 | alias?: string; 7 | target?: string; 8 | } -------------------------------------------------------------------------------- /src/core/i-inst-watched.ts: -------------------------------------------------------------------------------- 1 | export interface IInstWatched { 2 | inst?: { 3 | attached?: Function; 4 | detached?: Function; 5 | afterDetached?: Function; 6 | propsChanged?: (props: {}) => void; 7 | $content?: Function; 8 | $render?: Function; 9 | _$unsubs$_: {unsubscribeAll:Function}[]; 10 | }; 11 | $loaded?: boolean; 12 | $is?: string; 13 | $propsAfterAttached: {}; 14 | } 15 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import platform from './platform'; 3 | 4 | export { platform }; -------------------------------------------------------------------------------- /src/core/nodes-action.ts: -------------------------------------------------------------------------------- 1 | import inst_watched from './nodes-watched'; 2 | 3 | export const detacheNode = (node: HTMLDivElement) => { 4 | //detacheNode(node.children); 5 | if (node.children && node.children.length) { 6 | //detacheNode(node.children); 7 | let node_array = Array.prototype.slice.call(node.children); 8 | node_array.forEach( detacheNode ); 9 | } 10 | let key_id: string = node.getAttribute ? node.getAttribute('id') : ''; 11 | //console.log(node); 12 | let inst_captured = inst_watched[key_id]; 13 | if (key_id && inst_captured) { 14 | if(inst_captured.inst.detached){ 15 | inst_captured.inst.detached(); 16 | } 17 | if(inst_captured.inst.afterDetached){ 18 | inst_captured.inst.afterDetached(); 19 | } 20 | } 21 | //ajudando o guarbage collector do javascript 22 | if (key_id && inst_captured) { 23 | //evitando usar o refresh em um no morto 24 | inst_captured.$loaded = false; 25 | inst_captured.inst = null; 26 | inst_captured = null; 27 | delete inst_watched[key_id]; 28 | } 29 | }; 30 | 31 | export const attacheNode = (node: HTMLDivElement) => { 32 | let key_id: string = node.getAttribute ? node.getAttribute('id') : ''; 33 | let inst_captured = inst_watched[key_id]; 34 | if (key_id && inst_captured) { 35 | if(!inst_captured.$loaded){ 36 | const $inst = inst_captured.inst; 37 | for (let propOrign in inst_captured.$propsAfterAttached) { 38 | let prop_splited: string[] = propOrign.split('.'); 39 | if($inst[prop_splited[0]] && typeof $inst[prop_splited[0]][prop_splited[1]] === 'function'){ 40 | $inst[prop_splited[0]][prop_splited[1]]($inst[propOrign]); 41 | if(typeof $inst[prop_splited[0]]['unsubscribeAll'] === 'function'){ 42 | if(!$inst._$unsubs$_){ 43 | $inst._$unsubs$_ = []; 44 | } 45 | $inst._$unsubs$_.push($inst[prop_splited[0]]); 46 | 47 | if(typeof $inst.afterDetached !== 'function'){ 48 | $inst.afterDetached = function () { 49 | $inst._$unsubs$_.forEach(insSub => { 50 | insSub.unsubscribeAll(); 51 | }); 52 | $inst._$unsubs$_.length = 0; 53 | delete $inst._$unsubs$_; 54 | }; 55 | } 56 | } 57 | }else{ 58 | let {$is: compName} = inst_captured; 59 | console.warn(`There is no method '${propOrign}' in component '${compName}'!`); 60 | } 61 | } 62 | delete inst_captured.$propsAfterAttached; 63 | if($inst.attached){ 64 | $inst.attached(node); 65 | } 66 | } 67 | inst_captured.$loaded = true; 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/core/nodes-watched.ts: -------------------------------------------------------------------------------- 1 | import {IInstWatched} from './i-inst-watched'; 2 | let inst_watched:{[key:string]:IInstWatched}={}; 3 | export default inst_watched; -------------------------------------------------------------------------------- /src/core/platform.ts: -------------------------------------------------------------------------------- 1 | import componentFactory from './component-factory'; 2 | import { patch } from 'incremental-dom'; 3 | 4 | export class PlatformBootstrap { 5 | public bootstrap(pmodule: any, option?: { templateExtension: string }): any { 6 | return { at:(domRender: HTMLElement) => this.at(domRender,pmodule) }; 7 | } 8 | public at(domRender: HTMLElement, pmodule?: any): void { 9 | const key_id = `uid_${new Date().getTime()}`; 10 | const is = 'init-app-tag'; 11 | const proxyInst = componentFactory(pmodule,{z: 1000},{is, key_id}).content(function(){}); 12 | patch(domRender, proxyInst.$render.bind(proxyInst,{is, key_id}), proxyInst); 13 | } 14 | } 15 | 16 | export default new PlatformBootstrap(); -------------------------------------------------------------------------------- /src/incremental-dom.ts: -------------------------------------------------------------------------------- 1 | declare module 'incremental-dom' { 2 | export var patch: any; 3 | export var elementClose: any; 4 | export var currentElement: any; 5 | export var text: any; 6 | export var elementOpen: any; 7 | export var elementVoid: any; 8 | export var attr: any; 9 | export var port: any; 10 | export var notifications: { nodesDeleted: Function, nodesCreated: Function }; 11 | export var attributes: any; 12 | } 13 | -------------------------------------------------------------------------------- /test/app/main-app.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/app/main-app.html.js: -------------------------------------------------------------------------------- 1 | define(["exports","incremental-dom","@ferrugemjs/library/dist/core/component-factory","./main-app"], function (exports,_idom,_libfjs_factory,_main_app){ 2 | var __main_app_tmp = Object.keys(_main_app)[0]; 3 | exports.default = (function(super_clazz){ 4 | function _clazz_sub_f_d60b5d62_2c5c_4e78_9889_f75a30aa5f89_tmp(props){ 5 | if(super_clazz.call){ 6 | super_clazz.call(this, props); 7 | } 8 | }; 9 | _clazz_sub_f_d60b5d62_2c5c_4e78_9889_f75a30aa5f89_tmp.prototype = Object.create(super_clazz.prototype || super_clazz); 10 | _clazz_sub_f_d60b5d62_2c5c_4e78_9889_f75a30aa5f89_tmp.prototype.constructor = _clazz_sub_f_d60b5d62_2c5c_4e78_9889_f75a30aa5f89_tmp; 11 | _clazz_sub_f_d60b5d62_2c5c_4e78_9889_f75a30aa5f89_tmp.prototype.$render = function(config_props){if(!config_props.loaded){ _idom.elementOpen("div",""+(config_props.key_id)+"",[""],"is",(config_props.is),"id",(config_props.key_id)); 12 | 13 | 14 | }; 15 | _idom.elementOpen("h1"); 16 | 17 | _idom.text("Ola"); 18 | 19 | _idom.elementClose("h1"); 20 | 21 | _idom.elementOpen("span"); 22 | 23 | _idom.text(""+(this.msg)+""); 24 | 25 | _idom.elementClose("span"); 26 | if(!config_props.loaded){_idom.elementClose("div");}; }; 27 | return _clazz_sub_f_d60b5d62_2c5c_4e78_9889_f75a30aa5f89_tmp; 28 | })(_main_app[__main_app_tmp] || _main_app); 29 | }); -------------------------------------------------------------------------------- /test/app/main-app.js: -------------------------------------------------------------------------------- 1 | define(["exports"], (exports) => { 2 | class MainApp { 3 | // this.msg = "agora vai?"; 4 | // this.attached = () => { 5 | // console.log('im in DOM'); 6 | // setInterval(() => { 7 | // this.msg = "agooooo"; 8 | // // this.refresh(); 9 | // }, 2000); 10 | // } 11 | attached() { 12 | console.log('im in DOM3', this.msg); 13 | setInterval(() => { 14 | this.msg = "agooooo" + new Date().getTime(); 15 | // this.refresh(); 16 | }, 2000); 17 | } 18 | } 19 | 20 | // MainApp.prototype.msg = "gora avi???"; 21 | // MainApp.prototype.attached = () => { 22 | // console.log('im in DOM2', MainApp.prototype.msg); 23 | // setInterval(() => { 24 | // MainApp.prototype.msg = "agooooo"; 25 | // // this.refresh(); 26 | // }, 2000); 27 | // } 28 | 29 | exports.default = new MainApp; 30 | }) 31 | 32 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Test Project 9 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6" 4 | ,"module": "amd" 5 | ,"moduleResolution": "classic" 6 | ,"outDir": "./dist" 7 | ,"experimentalDecorators": true 8 | ,"noImplicitAny":true 9 | ,"sourceMap": false 10 | ,"declaration": true 11 | ,"removeComments":true 12 | ,"noEmitHelpers":true 13 | ,"importHelpers":true 14 | ,"suppressImplicitAnyIndexErrors": true 15 | ,"baseUrl": "./" 16 | ,"lib":["ESNext", "dom","es6","scripthost"] 17 | ,"paths": { 18 | "tslib" : ["node_modules/tslib/tslib.d.ts"] 19 | } 20 | }, 21 | "exclude": [ 22 | "node_modules" 23 | ,"dist" 24 | ], 25 | "compileOnSave": false 26 | } 27 | --------------------------------------------------------------------------------