@fruit-ui/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +78 -0
  2. package/dist/index.js +1 -0
  3. package/package.json +28 -0
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ ## Functional Reactivity UI Toolkit (FRUIT)
2
+
3
+ FRUIT is a lightweight, zero-dependency UI framework written in JS for JS apps. It uses nested JavaScript objects to represent DOM elements, i.e.,
4
+
5
+ ```javascript
6
+ import { appendChild } from "@fruit-ui/core";
7
+
8
+ const Paragraph = {
9
+ tag: 'p',
10
+ children: [
11
+ 'Writing in ',
12
+ {tag: 'strong', style: {color: 'blue'}, children: 'FRUIT'},
13
+ ' is fun!'
14
+ ]
15
+ };
16
+
17
+ // to append an element to the DOM
18
+ appendChild(document.body, Paragraph);
19
+ ```
20
+
21
+ FRUIT is powerful, efficient, and feature-packed. In addition to objects representing static elements, users can write stateful, reactive components, i.e.,
22
+
23
+ ```javascript
24
+ import { appendChild } from "@fruit-ui/core";
25
+
26
+ const Counter = {
27
+ state() {
28
+ return {i: 0};
29
+ },
30
+ render() {
31
+ return {
32
+ tag: 'button',
33
+ children: `I've been clicked ${this.state.i} times!`,
34
+ on: {
35
+ click() {
36
+ this.setState.i(this.state.i + 1);
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ appendChild(document.body, Counter);
44
+ ```
45
+
46
+ FRUIT's features include:
47
+ - Intuitive element and component syntax
48
+ - Implicit props-passing between components
49
+ - Preserved, optionally reactive state
50
+ - Smooth, efficient rerendering with support for transitions and animations
51
+ - Keys to preserve state among re-ordered siblings
52
+ - An on-mount listener and handler methods
53
+ - Bindings to elements within components
54
+
55
+ with all special functional features (state, controlled rerendering, bindings) accessed through the `this` argument.
56
+
57
+ Documentation is available for FRUIT [here](https://asantagata.github.io/fruit-ui/).
58
+
59
+ ## Why FRUIT over other front-end frameworks?
60
+
61
+ Smaller apps don't always warrant heavyweight frameworks, but interfacing with the DOM directly is a hassle. The ability to declare and mutate state reactively is crucial in web apps with any amount of interactivity. Working in FRUIT and vanilla JS means no complex hidden logic to keep track of, no build step, and no separation of languages for your UI and your internal logic.
62
+
63
+ ## Getting started
64
+
65
+ There are three ways to use FRUIT in your projects:
66
+ - Download and copy the [Terser-compressed JS file](https://github.com/asantagata/fruit-ui/blob/main/dist/index.js) file into your project. (This is a compressed version built with Terser; you can just as well use the [non-compressed version](https://github.com/asantagata/fruit-ui/blob/main/src/index.js).) Then you can use `import { create, replaceWith, appendChild, insertBefore } from "./modules/fruit.js"` or `<script type="module" src="./modules/fruit.js">` to access FRUIT in your JS apps.
67
+ - Access via browser loading, i.e., `import { create, replaceWith, appendChild, insertBefore } from "https://cdn.jsdelivr.net/npm/@fruit-ui/core@latest/index.js"`.
68
+ - With NPM installed, run `npm install @fruit-ui/core`. Then use `import { create, replaceWith, appendChild, insertBefore } from "@fruit-ui/core"`.
69
+
70
+ ## Contributing
71
+
72
+ Ongoing development on FRUIT focuses on:
73
+ - Thorough, interactive, user-facing documentation (available [here](https://asantagata.github.io/fruit-ui/))
74
+ - Benchmarks to compare against other JS frameworks
75
+ - A "useMemo" equivalent
76
+ - Basic built-in components
77
+
78
+ This project's initial release is currently not yet finished, so contributions aren't currently being sought out.
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ let globalComponentCount=0;const thisRecord={};const rerenderQueue=new Set;function rerenderEnqueuedComponents(){Array.from(rerenderQueue).forEach(cId=>thisRecord[cId]?.rerender());rerenderQueue.clear()}function enqueueToRerender(componentId){const enqueueRerenderTask=rerenderQueue.size===0;rerenderQueue.add(componentId);if(enqueueRerenderTask)queueMicrotask(rerenderEnqueuedComponents)}function createThis(){return{state:{},setState:{},bindings:{}}}function initializeThis(element,producer){this.element=element;this.producer=producer;this.rerender=rerender.bind(this);this.setState=new Proxy({},{get:(o,p,r)=>x=>{this.state[p]=x;enqueueToRerender(producer.componentId)}})}function createElementFromTemplate(template,onMounts,producer=null){if(template.cloneFrom){const element=template.cloneFrom.cloneNode(true);if(this&&!this.element){initializeThis.call(this,element,producer)}return element}const{tag:tag,class:c,style:style,on:on,componentId:componentId,children:children,cloneFrom:cloneFrom,dataset:dataset,key:key,binding:binding,innerHTML:innerHTML,...rest}=template;const element=document.createElement(template.tag||"div");if(template.class){switch(typeof template.class){case"string":element.className=template.class;break;case"object":if(Array.isArray(template)){element.className=template.class.join(" ")}else{element.className=Object.keys(template.class).filter(c=>template.class[c]).join(" ")}}}if(template.style){for(const k in template.style){element.style[k]=template.style[k]}}if(template.on){const{mount:onMount,...listeners}=template.on;for(const type in listeners){if(!listeners[type])continue;element.addEventListener(type,event=>listeners[type].call({...this??{},target:element},event))}if(onMount){onMounts.push(()=>onMount.call({...this??{},target:element}))}}for(const attribute in rest){if(!template[attribute])continue;element.setAttribute(attribute,template[attribute])}if(template.dataset){for(const k in template.dataset){element.dataset[k]=template.dataset[k]}}if(template.componentId){element.dataset.componentId=template.componentId}if(template.key){element.dataset.key=template.key}if(template.binding){element.dataset.binding=template.binding}if(this&&!this.element){initializeThis.call(this,element,producer)}if(template.innerHTML){element.innerHTML=template.innerHTML}else if("children"in template){if(Array.isArray(template.children)){element.replaceChildren(...template.children.map(ct=>createElementFromElementable.call(this,ct,onMounts)));for(let i=0;i<template.children.length;i++){const childTm=template.children[i];if(childTm.binding&&this){setBinding.call(this,childTm.binding,element.childNodes[i])}}}else{element.replaceChildren(createElementFromElementable.call(this,template.children,onMounts));if(template.children.binding&&this){setBinding.call(this,template.children.binding,element.childNodes[0])}}}return element}function bindTemplateProducer(producer){const newThis=createThis();const boundProducer=producer.bind(newThis);boundProducer.this=newThis;boundProducer.componentId=`component-${globalComponentCount++}`;return boundProducer}function elementableIsComponent(elementable){return!!elementable.render}function createElementFromElementable(elementable,onMounts){if(typeof elementable==="object"){if(elementableIsComponent(elementable)){return createElementFromComponent(elementable,onMounts)}else{return createElementFromTemplate.call(this,elementable,onMounts)}}else{return elementable.toString()}}function createElementFromComponent(component,onMounts){const boundProducer=bindTemplateProducer(component.render);thisRecord[boundProducer.componentId]=boundProducer.this;boundProducer.this.state=component.state?component.state():{};const template=boundProducer();return createElementFromTemplate.call(boundProducer.this,giveTemplateComponentMetadata(template,boundProducer.componentId,component.key,component.binding),onMounts,boundProducer)}function giveTemplateComponentMetadata(template,componentId,key,bindingName){return{...template,componentId:componentId,key:key??template.key,binding:bindingName??template.binding}}function doOnMountHandling(func){const onMounts=[];func(onMounts);onMounts.forEach(om=>om())}function rerender(){doOnMountHandling(onMounts=>{const template=this.producer();rerenderElementFromTemplate.call(this,this.element,giveTemplateComponentMetadata(template,this.producer.componentId),onMounts)})}function rerenderByBinding(bindingName){const template=this.producer();const subTemplate=findSubtemplate(template,bindingName);const subElement=findSubelement(this.element,bindingName);if(subTemplate&&subElement){doOnMountHandling(onMounts=>{if(elementableIsComponent(subTemplate)){if("componentId"in subElement.dataset)rerenderChildComponent(subElement,subTemplate,onMounts);else subElement.replaceWith(createElementFromComponent(subTemplate,onMounts))}else{if("componentId"in subElement.dataset)delete thisRecord[subElement.dataset.componentId];rerenderElementFromTemplate.call(this,subElement,subTemplate,onMounts)}})}}function findSubelement(element,bindingName,atRoot=true){if(element.dataset.binding===bindingName&&!atRoot){return element}else if(!atRoot&&"componentId"in element.dataset){return undefined}return Array.from(element.children).find(c=>!!findSubelement(c,bindingName,false))}function findSubtemplate(template,bindingName,atRoot=true){if(template.binding===bindingName&&!atRoot){return template}if(template.children){if(Array.isArray(template.children)){return template.children.find(c=>findSubtemplate(c,bindingName,false))}else{return findSubtemplate(template.children,bindingName,false)}}return undefined}function rerenderElementFromTemplate(element,template,onMounts){if(typeof template!=="object"){return element.replaceWith(template.toString())}if(template.cloneFrom&&!element.isEqualNode(template.cloneFrom)){return element.replaceWith(template.cloneFrom.cloneNode(true))}if((template.tag?.toUpperCase()||"DIV")!==element.tagName){return element.replaceWith(createElementFromElementable.call(this,template,onMounts))}if(template.class){if(typeof template.class==="string"){element.className=template.class}else if(Array.isArray(template)){element.className=template.class.join(" ")}else{for(const key in template.class){if(template.class[key]){element.classList.add(key)}else{element.classList.remove(key)}}}}if(template.style){for(let key in template.style){element.style[key]=template.style[key]}}if(template.dataset){for(let key in template.dataset){element.dataset[key]=template.dataset[key]}}const{tag:tag,cloneFrom:cloneFrom,class:_,style:style,on:on,key:key,dataset:dataset,componentId:componentId,children:children,innerHTML:innerHTML,binding:binding,...rest}=template;for(const attribute in rest){element.setAttribute(attribute,template[attribute])}if(template.innerHTML){element.innerHTML=template.innerHTML}else if(template.children===undefined||template.children.length===0){element.innerHTML=""}else{rerenderChildren.call(this,element,template,onMounts)}}function rerenderChildComponent(element,component,onMounts){const cmpThis=thisRecord[element.dataset.componentId];cmpThis.producer=component.render.bind(cmpThis);rerenderElementFromTemplate.call(cmpThis,element,cmpThis.producer(),onMounts)}function recreateKeyedChildComponent(component,componentId,onMounts){const cmpThis=thisRecord[componentId];cmpThis.producer=component.render.bind(cmpThis);const template=cmpThis.producer();const element=createElementFromTemplate.call(cmpThis,giveTemplateComponentMetadata(template,componentId,component.key,component.binding),onMounts,cmpThis.producer);cmpThis.element=element;return element}function rerenderChildren(element,template,onMounts){const elChildrenArray=Array.from(element.children);const tmChildNodeArray=Array.isArray(template.children)?template.children:[template.children];const tmChildrenArray=tmChildNodeArray.filter(c=>typeof c==="object");if(elChildrenArray.length>0&&tmChildrenArray.length>0&&elChildrenArray.every(elChild=>"key"in elChild.dataset)&&tmChildrenArray.every(tmChild=>"key"in tmChild)){const keyIndexInEl={};for(let i=0;i<elChildrenArray.length;i++){keyIndexInEl[elChildrenArray[i].dataset.key]=i}const keyIndexInTm={};for(let i=0;i<tmChildrenArray.length;i++){keyIndexInTm[tmChildrenArray[i].key]=i}const tmKeyIndexInEl=tmChildrenArray.map(k=>keyIndexInEl[k.key]??-1);const lis=LIS(tmKeyIndexInEl.filter(i=>i>-1));for(let i=0;i<lis.length;i++){const elIndex=lis[i];const childEl=elChildrenArray[elIndex];const childTm=tmChildrenArray[keyIndexInTm[childEl.dataset.key]];if(elementableIsComponent(childTm)){if("componentId"in childEl.dataset)rerenderChildComponent(childEl,childTm,onMounts);else childEl.replaceWith(createElementFromComponent(childTm,onMounts))}else{if("componentId"in childEl.dataset)delete thisRecord[childEl.dataset.componentId];rerenderElementFromTemplate.call(this,childEl,childTm,onMounts)}}const setLis=new Set(lis);const movedElKeyToComponentId={};for(let i=elChildrenArray.length-1;i>=0;i--){if(!setLis.has(i)){const childEl=elChildrenArray[i];if("componentId"in childEl.dataset){if(childEl.dataset.key in keyIndexInTm){movedElKeyToComponentId[childEl.dataset.key]=childEl.dataset.componentId}else{delete thisRecord[childEl.dataset.componentId]}}if("binding"in childEl.dataset)delete this.bindings[childEl.dataset.binding];childEl.remove()}}for(let i=0;i<tmChildrenArray.length;i++){const childTm=tmChildrenArray[i],childEl=element.children[i];if(!childEl){const componentId=movedElKeyToComponentId[childTm.key];if(componentId){element.appendChild(recreateKeyedChildComponent(childTm,componentId,onMounts))}else{element.appendChild(createElementFromElementable.call(this,childTm,onMounts))}}else{if(childEl.dataset.key!==childTm.key){const componentId=movedElKeyToComponentId[childTm.key];if(componentId){element.insertBefore(recreateKeyedChildComponent(childTm,componentId,onMounts),childEl)}else{element.insertBefore(createElementFromElementable.call(this,childTm,onMounts),childEl)}}}}}else if(tmChildrenArray.length>0&&elChildrenArray.length>0){for(let i=0;i<Math.min(elChildrenArray.length,tmChildrenArray.length);i++){let childEl=elChildrenArray[i],childTm=tmChildrenArray[i];if("binding"in childEl.dataset&&childEl.dataset.binding!==childTm.binding)delete this.bindings[childEl.dataset.binding];if(elementableIsComponent(childTm)){if("componentId"in childEl.dataset)rerenderChildComponent(childEl,childTm,onMounts);else childEl.replaceWith(createElementFromComponent(childTm,onMounts))}else{if("componentId"in childEl.dataset)delete thisRecord[childEl.dataset.componentId];rerenderElementFromTemplate.call(this,childEl,childTm,onMounts)}}if(elChildrenArray.length<tmChildrenArray.length){for(let i=elChildrenArray.length;i<tmChildrenArray.length;i++){let childTm=tmChildrenArray[i];const newChild=createElementFromElementable.call(this,childTm,onMounts);element.appendChild(newChild)}}else if(elChildrenArray.length>tmChildrenArray.length){for(let i=elChildrenArray.length-1;i>=tmChildrenArray.length;i--){let childEl=elChildrenArray[i];if("componentId"in childEl.dataset)delete thisRecord[childEl.dataset.componentId];if("binding"in childEl.dataset)delete this.bindings[childEl.dataset.binding];childEl.remove()}}}for(let i=0;i<tmChildrenArray.length;i++){const childTm=tmChildrenArray[i];if(childTm.binding){setBinding.call(this,childTm.binding,element.children[i])}}for(let i=0;i<tmChildNodeArray.length;i++){if(i>=element.childNodes.length){element.appendChild(document.createTextNode(tmChildNodeArray[i]))}else{if(element.childNodes[i].nodeType===Node.TEXT_NODE){if(typeof tmChildNodeArray[i]!=="object"){element.childNodes[i].textContent=tmChildNodeArray[i]}else{while(element.childNodes[i].nodeType!==Node.ELEMENT_NODE){element.childNodes[i].remove()}}}else{if(typeof tmChildNodeArray[i]!=="object"){element.insertBefore(document.createTextNode(tmChildNodeArray[i]),element.childNodes[i])}}}}while(element.childNodes.length>tmChildNodeArray.length){element.lastChild.remove()}}function LIS(arr){let N=arr.length;let P=new Array(N).fill(0);let M=new Array(N+1).fill(0);M[0]=-1;let L=0;for(let i=0;i<N;i++){let low=1,high=L+1;while(low<high){let mid=low+(high-low>>1);if(arr[M[mid]]>=arr[i])high=mid;else low=mid+1}let newL=low;P[i]=M[newL-1];M[newL]=i;if(newL>L){L=newL}}let S=new Array(L);let k=M[L];for(let j=L-1;j>=0;j--){S[j]=arr[k];k=P[k]}return S}function setBinding(bindingName,element){this.bindings[bindingName]={element:element,rerender:()=>rerenderByBinding.call(this,bindingName)}}function create(elementable,includeOnMounts=false){const onMounts=[];let element=createElementFromElementable(elementable,onMounts);if(typeof element==="string")element=document.createTextNode(element);return includeOnMounts?{element:element,onMounts:onMounts}:element}function replaceWith(element,elementable){const{element:newElement,onMounts:onMounts}=create(elementable,true);element.replaceWith(newElement);onMounts.forEach(om=>om())}function appendChild(element,elementable){const{element:newElement,onMounts:onMounts}=create(elementable,true);element.appendChild(newElement);onMounts.forEach(om=>om())}function insertBefore(parentElement,nextSiblingElement,elementable){const{element:newElement,onMounts:onMounts}=create(elementable,true);parentElement.insertBefore(newElement,nextSiblingElement);onMounts.forEach(om=>om())}export{create,replaceWith,appendChild,insertBefore};
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@fruit-ui/core",
3
+ "version": "1.0.0",
4
+ "description": "A vanilla JS toolkit for reactive UI",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1",
8
+ "build": "terser .\\src\\index.js -o .\\dist\\index.js"
9
+ },
10
+ "keywords": [
11
+ "fruit",
12
+ "core",
13
+ "js",
14
+ "json",
15
+ "vanilla",
16
+ "ui",
17
+ "dom",
18
+ "react",
19
+ "reactive"
20
+ ],
21
+ "author": "asantagata",
22
+ "license": "ISC",
23
+ "type": "module",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/asantagata/fruit-ui.git"
27
+ }
28
+ }