Cle.js
(Clever Js) is a declarative
Javascript Framework, with Pure Objects
in mind (for ui, data model & logic), no virtual dom
, a "caged" environment, a True reactive
nature, a powerful context system, native state management utils and fast rt "static" analysis (used to build dependencies between components and achieve true reactivity).
TLDR "HTML String" (XML) vs "Pure JS Objects" (JS):
Html | Cle |
---|---|
<div>
<h1 class="title">
Hello World
</h1>
<button onclick="console.log(...)">
Log
</button>
</div> |
{ div: { '=>': [
{ h1: { class:'title',
text:'Hello World'
}},
{ button: { onclick: $ => console.log(...),
text:'Log'
}},
]
}} |
Cle.js is not a "all-or-nothing"
framework, it can be mixed up with other frameworks, this way applications can be gradually migrated to cle or take the best from different worlds with mashups
(eg. simple status management with props, scope, signal and dbus, or "zero imports" for components declaration). See the React mashup
example in src/demo/mashups/react.
Cle.js can be installed from NPM
, for structured projects, or it can be also imported directly from CDN
, for fast prototyping or small, simple pure-js-based projects (it doesn't require to be compiled, as it leverage on es6 modules with benefits for DX, eg. start dev server for big projects). Cle promotes open-source and suggest to avoid "security-by-obfuscation" pratiques and just minimize the code for production.
Cle.js is fast
. For his Pure Objects nature one of it's major improve w.r.t other frameworks is that reactivity is not achieved by using virtual dom and diffing algorithms, instead it leverage the "caged" system and very a fast "static analysis" to determinate the dependencies between components, props, attrs etc, in order to re-render parts only in reaction to changes. Lazy loading and rendering does the rest. This makes cle really fast
.
Another remarkable improve is that components are just object
, still editable, customizable and extensible, also if taken from NPM (in other frameworks everything a component can do should be "prepared" from developers). This lead for eg. UI library developers to create and handle less code
, because components are not sealed, or handle re-hydration in different ways.
Cle.js it's also a "meta-language" first
. In other words something like JSON: a syntax readable both by Humans & Computers that does not require compilation. This means that it's easy to build other syntax/frameworks
with Cle. In this framework you will find different
styles & techniques, each with pro and cons, but ALL styles can be used together and mixed as you prefer.
Inspired mostly by:
QML
(id, scoping & naming, true "reactive" properties, events as signals & slot, mangling for declarations, coding by convention, everything has a signal, component editing from external, assignment system, elements/ref by ID, auto context/scope)Angular
(pure declarative & templating, ng-for, ng-if, 2 way data binding, "auto-update", Hooks & life-cycle)React
(non-verbose, render library first, lightweight, fast and dynamic, with very limited size & memory footprint, easy to learn, css approach)Vue
(html like files for components declaration with script/style/template)Python
(read-first, mangling, function declaration must have "self" as first parameter, natural language syntax like)- and many more
Install from npm
npm install cle.js
or clone the project template starter [for html components]
git clone https://github.com/vcamelblue2/create-cle-js-app--use-html.git
or the mashup project template starter [for Cle - React mashup components]
git clone https://github.com/vcamelblue2/create-cle-js-app--use-react-mashup
Quick Start - From React
React | Cle |
---|---|
const App = () => {
// States
const [user, setUser] = useState('Username')
const [counter, setCounter] = useState(0)
const computed = user + ' - ' + counter
// Functions
const increment = () => {
setCounter( x => x + 1 )
}
// Childs
return (<>
<h1>Hello {user}</h1>
<Counter count={counter} increment={increment}/>
</>)
}
const Counter = ({count, increment}) => (
<div>
<h3>The counter is: {count}</h3>
<button onclick={increment}>+1</button>
<div/>
) |
const App = () => ({ app: {
// States, just "variables"
let: {
user: 'Username'
count: 0,
computed: $ => $.user + ' - ' + $.count
},
// Functions
def: {
increment: $ => { $.count += 1 }
},
// Childs
'=>': [
{ h1: ['Hello ', $ => $.user] },
Counter(), /* no manual bind, just "scope" visibility. */
]
}})
const Counter = () => Div({
deps: ['count', 'increment']
},
H3({}, 'The Counter is (', $ => $.count, ')'),
Button({ onclick: $ => $.increment()}, '+1')
) |
"Scope..what?". Don't worry, you will understand it later. To calm you: in/out can always be passed down as in other frameworks
...
Counter({count: $=>$.count, increment: $=>$.increment})
...
const Counter = ({count, increment}) => Div({
'@input': {count, increment}
},
H3({}, 'The Counter is (', $ => $.count, ')'),
Button({ onclick: $ => $.increment()}, '+1')
)
Quick Example
const { RenderApp, cle, f, Bind, Use, pass, html, defineHtmlComponent, remoteHtmlComponent } = await import('https://cdn.jsdelivr.net/npm/cle.js/lib/caged-le.js');
const { App, Div, Hr, H4 } = await import('https://cdn.jsdelivr.net/npm/cle.js/extra/smart-alias.js');
const SimpleComponent = { div: {
style: "margin-bottom: 1rem",
text: "Author: Vincenzo Villani"
}}
// App Definition
const app = async () => RenderApp(document.body, cle.root({
css: [`body { padding: 20px }`]
},
{ h1: "Hello World!" },
{ div: ["This is the body", "!"] },
{ div: {
myVariable: "123", // shortcut of let: { myVariable } for NON reserved names
style: { color: "green"},
text: $ => "And a component with some definition, myVariable:" + $.myVariable }
},
{ hr: {}},
SimpleComponent,
// Use components, for "advanced usage"
Use(MyReusableInputBar, {
// setup variable
val: "ABC",
// handle signals & variable changes
on_valChanged: ($, txt, oldtxt) => console.log("new text: ", txt, " - old: ", oldtxt),
on_hiLogged: $ => console.log("hi has been logged!"),
}),
cle.hr({}),
{ div: {
id: "hiddenDiv", // unique id in app!
let: {
isHidden: true
},
// define depens on computed attr
style: $ => ({
display: $.isHidden ? 'none' : null
}),
// hooks & lifecycle
onInit: $ => { console.log("I'm Hidden?", $.isHidden) },
text: "Secret Message!"
}},
{ h4: {
// handle html events
handle: {
onclick: $ => {
$.le.hiddenDiv.isHidden = !$.le.hiddenDiv.isHidden
}
},
text: ["The div is ", $ => $.le.hiddenDiv.isHidden ? 'hidden' : 'visible', " (click to toggle)"],
}},
// same using smart definition:
cle.h4({
handle_onclick: $ => {
$.le.hiddenDiv.isHidden = !$.le.hiddenDiv.isHidden
}
}, "The div is ", f`$.le.hiddenDiv.isHidden ? 'hidden' : 'visible'`, " (click to toggle)"),
// Or Smart Alias
H4({
handle_onclick: $ => {
$.le.hiddenDiv.isHidden = !$.le.hiddenDiv.isHidden
}
}, "The div is ", f`$.le.hiddenDiv.isHidden ? 'hidden' : 'visible'`, " (click to toggle)"),
Hr(),
// Html Components (dynamics)
html`
<div>
<h1 style="font-weight: 600">Hi from simple HTML</h1>
<span>(with a twist!)</span>
<div [meta-foreach]="item of [1,2,3]">
- {{ @item }}
</div>
</div>
`,
Hr(),
// Remote Html Components [svelte/vue style]
await remoteHtmlComponent("/remote-components.html", { component: "MyToolbar", params: {hello: "world"}, state: {statevar: "private state namespace.."}, DepsInj: {CircleImage}} ),
cle.hr(),
await CircleImage(),
))
// A reusable component
const MyReusableInputBar = cle.div({
// definition
let: { // variables
val: "",
doubleVal: $ => 'x2: ' + $.val + ' - ' + $.val, // computed
tripleVal: f`'x3: ' + @val + ' - ' + @val + ' - ' + @val`
},
def: { // functions
sayHi: $ => {
console.log("hi!");
$.this.hiLogged.emit(new Date())
}
},
signals: { // signals
hiLogged: "stream => (date: Date)"
}
},
// childs
cle.h5({}, "Insert a value"),
{ input: { // 2 way data binding using 'Bind'
attrs: { value: Bind(f`@val`), placeholder: "Insert a value..", style: "margin-right: 10px" }
}},
{ span: { text: f`'You have inserted: ' + @val + ' -> ' + @doubleVal`} },
cle.button({ // mix modes as you prefer!
handle_onclick: $ => $.sayHi(),
style: { marginLeft: "10px" }
}, "Say Hi!" )
);
// Define Html Components [also remote]
const CircleImage = defineHtmlComponent(/*html*/`
<script>({
let: {
src: "https://parceljs.org/logo.49e8bbc1.svg",
custom_style: {}
},
style: $ => ({
borderRadius: '50%',
...$.custom_style
})
})</script>
<view>
<img [src]="$.src" class="pad-img">
</view>
<style>
.pad-img{
padding: 5px;
}
</style>
`, { isRemote: false })
await app();
Real World Example:
Un elemento Cle è un qualsiasi Object
(oggetto standard javascript) la cui unica e prima chiave è un tag html. Il valore associato a questo tag può essere invece:
- un Object contenente la
definizione
delle caratteristiche dell'elemento HTML che si vuole renderizzare, nonchè dati, metodi etc. - una stringa
- un Valore-Funzione (Evaluable)
Per esempio, un elemento HTML h1
può essere scritto come:
// Standard Definition
{ h1: {
...definition...
}}
// String Constants (shortcut)
{ h1: "some text" }
// Evaluable (shortcut)
{ h1: $ => ...something that return text... }
i sottoelementi possono essere:
- un elemento Cle
- { span: ... }
- una stringa
- un Valore-Funzione (Evaluable)
- funzione lambda il cui primo argomento è sempre $ => ...
e possono essere definiti come valori di un array all'interno della definition
attraverso una qualsiasi di queste chiavi:
'childs'
'text'
'view'
'=>'
''
{ div: {
childs: [
...
]
}}
Nei casi di un singolo sottoelemento l'array può essere omesso.
Per avere un elemento html di tipo h1
contenente un "Hello World!"
(ovvero il cui unico sottoelemento è un testo) basta scrivere:
{ h1: {
text: "Hello World!"
}}
Nel caso di più sottoelementi basta semplicemente utilizzare un Array:
{ h1: {
text: [ "Hello ", "World!" ]
}}
Esistono poi delle shortcuts:
- Se la
definition
dichiara un solo sottoelemento e nulla più, si può passare il sottoelemento direttamente come valore al tag. - Nel caso di più sottoelementi (definiti come array), è possibile passare direttamente come valore al tag.
// Shortcuts
{ h1: "Hello World!" } // 1
{ h1: [ "Hello ", "World!" ]} // 2
L'unità minima che è possibile rendirizzare in Cle è un singolo elemento Cle (ovvero un unico elemento HTML), che si può immaginare come un "componente" per come è inteso in altri framework.
Per renderizzare un Componente possiamo utilizzare la funzione RenderApp(htmlContainer, cleComponent)
importandola da lib
.
import {RederApp} from "cle.js/lib"
RenderApp(document.body, { h1: "Hello World!" })
Generalmente, per convenzione, le app iniziano con un cle.root
:
import {RederApp, cle} from "cle.js/lib"
RenderApp(document.body, cle.root({},
{ h1: "Hello World!" },
{ div: "This is the body" }
))
Il lifecycle di un componente Cle prevede di fatto che prima venga renderizzato l'albero "spoglio" di elementi HTML, dopodichè vengono inizializzati e lanciati gli Hooks che si possono definire con le seguenti keywords:
constructor
: async? ($, {...args}) => { }- Constructor, called at component creations, useful to setup properties from external with the
Use
declaration. use onInit in other cases. Only called in Use components.
- Constructor, called at component creations, useful to setup properties from external with the
onInit
: async? $ => { }- on Init, after constructor, but before childs creation. This is the standard initializer.
afterChildsInit
: async? $ => { }- After every childs "onInit". Useful to setup special things into childs after creation.
afterInit
: $ => { }- After 1ms of the afterChildsInit. a convenient
auto-lazy
method for pseudo-async init
- After 1ms of the afterChildsInit. a convenient
Infine, sotto determinate condizioni l'elemento viene distrutto
onDestroy
: $ => { }- After Childs Destroy, Before Destroing "this" element. Usefull to clean imperative action and so on.
La standard definition
di un componente prevede che all'interno della definizione possano essere esplicitati, oltre ai sottoelementi tramite una delle chiavi di cui sopra, molte altre cose.
E' ad esempio possibile definire variabili, funzioni, segnali, attributi html, classi css, nonchè gestire eventi html, segnali locali, di properità o globali. Per una lista approfondita si rimanda alla sezione #Full Reference
.
Vediamo ora com'è possibile creare una variabile in un componente.
// Component is just an Object and can be assigned to any variable
const TheCounter = { div: {
id: "myCounter",
props: {
counter: 0
},
onInit: $ => {
console.log($.counter)
}
}}
In questo esempio stiamo definendo un componente "TheCounter", la cui rappresentazione grafica è un div. Possiamo definire una variabile counter
con valore iniziale 0
grazie alla keyword "props
".
Abbiamo inoltre assegnato al nostro componente un identificativo (myCounter
), tramite la keyword "id
", che sarà univoco in tutta l'app.
Vedremo meglio com'è possibile utilizzare gli id in seguito.
Abbiamo anche definito l'hook onInit
che verrà chiamato all'inizializzazione del componente.
Per referenziare e mostrare all'utente il contenuto della variabile è possibile utilizzare:
const TheCounter = { div: {
id: "myCounter",
props: {
counter: 0
},
text: $ => "The Counter Is: " + $.this.counter
}}
// Rendered as <div>The Counter Is: 0</div>
Per la definizione degli Evaluable (funzioni il cui primo argomento è sempre '$
') ci si rifà alla definizione di funzione in python, per cui il primo argomento è sempre "self
". Nel caso di Cle, invece, è sempre $
, anche se l'argomento $
è qualcosa di più complesso di un mero riferimento al 'this
' del Componente.
$
here is a special variable that contains some reference to certain scope selector
. It contains:
-
'
this
' scope selector, wich is a refernce tothis
Cle element. like the 'self' in python- here we can find variables, methods, signals, a reference to the parent element ("parent"), and a reference to the renderized html element via "
el
" property
- here we can find variables, methods, signals, a reference to the parent element ("parent"), and a reference to the renderized html element via "
-
'
parent
' scope selector, wich contain a refernce to the parent -
'
scope
' scope selector, wich contain all the variables and methods of this element, it's meta and of all the parents in his parent chain (equivalent to:this
&meta
&parent
&parent.parent
&parent.parent.parent
+ ...). With normal shadowing rule. -
'
meta
' scope selector, wich contain all the variables and methods of all the parents 'meta' in his parent chain. mostly used in the list-like components, usingleFor
-
'
le
' scope selector, wich is a container of the element by id (global). -
'
ctx
' scope selector, wich is a container of the element by id (local of the so called 'component', that are created with the 'Use' key) -
'
ref
' scope selector, wich is a container of the element by id (local of the so called 'component', where the 'name' is registered in the childsRef, and a 'name' is assigned) -
'
dbus
' scope selector, where you can find the global dbus channels. useful to connetc different application part. -
'
u
' scope selector, that contains some metaprogramming utils, like signal creation and subscribe, dynamic child retrive, creation and destruction -
'
oos
' out of scope selector, a free (untracked and non bindable) per-object space where can be manually defined (imperative code) anything, for example for data where binding is not required. This space will be "shared" with all the components instance, also in "Use". To have a per-instance space declare it as a function that return an object (with $ arg) and will be generated at run-time before init. -
#Final Tip
: is always possible to use directly$.xxx
wich will be resolved as$.scope.xxx
( scope selector 'scope
' is the default used in resolution).
Visto che abbiamo definito un id univoco per questo componente (myCounter
) è dunque possibile referenziare la variabile counter come:
$.le.myCounter.counter
Questo implica che è possibile utilizzare anche una proprietà di un componente NON nella propria parent chain, semplicemente riferendosi al componente che la contiene e chiamandola per nome:
const TheCounter = { div: {
id: "myCounter",
props: {
counter: 0
},
...
}}
const TheCounterStats = { div: {
props: {
isEven: $ => $.le.myCounter.counter % 2 === 0,
isBig: $ => $.le.myCounter.counter > 100,
},
text: [
"Is Counter Even? ", $ => $.isEven, { br: {}},
"Is Counter Big? ", $ => $.isBig
]
}}
RenderApp(dcoment.body, cle.root({},
{ h2: "Welcome to Cle.js" },
TheCounter,
TheCounterStats
))
Inoltre, grazie a $.parent
e $.scope
possiamo utilizzare le proprietà nella nostra parent chain. Questo di fatto permette di creare dei veri e propri "Componenti
" riutilizzabili
const TheCounter = { div: {
id: "myCounter",
props: {
counter: 0
},
// Childs
'=>': [
{ h3: "Hello Dev!" },
{ div: $ => "The Counter Is: " + $.scope.counter } // Explicit
{ div: $ => "The Counter Is: " + $.counter } // Implicit $.scope.counter
{ div: $ => "The Counter Is: " + $.parent.counter } // Referencing the direct parent only. Not suitable for frequent changing component.
]
}}
/* Rendered as:
<div>
<h3>Hello Dev!<h3>
<div>The Counter Is: 0</div>
<div>The Counter Is: 0</div>
</div>
*/
Per concludere sulle variabili andiamo a valutare il meccanismo di change detection adottato da Cle, più simile a quello di React che a quello di Angular.
Data infatti una variabile contenente un oggetto:
props: {
calendarEvent: { id: 0, dueDate: "2022-01-01", title: "Go to grocery store" }
}
Il meccanismo di change detection, che scatena un rerendering dei componenti che seguono
tale proprietà, è quello dell'uguaglianza semplice (===). Nel caso dell'oggetto calendarEvent, la modifica del title
, ad esempio, NON verrà identificata (il riferimento non è cambiato!), mentre per una property string questo succederà (è una nuova stringa!). Ciò ovviamente vale, rispettivamente, per tutti i tipi riferimento e valore.
E' necessario dunque trattare le property come costanti (come in React): per modificare il title è necessario riassegnare un nuovo oggetto, ad esempio usando lo spread operator
... $ => {
$.calendarEvent = { ...$.calendarEvent, title: "Go to the mall" }
}
Esiste però un'altra possibilità, ovvero utilizzare un metodo che viene autogenerato per ogni property, nello stesso scope della property, e che viene generato come '_mark_XXX_as_changed()
'.
In questo caso potremmo modificare il codice precedente in:
... $ => {
$.calendarEvent.title = "Go to the mall"
$._mark_calendarEvent_as_changed()
}
Come vedremo in seguito, per ogni property viene anche generato un Signal
, nella forma 'xxxChanged
', che i componenti (incluso se stesso) possono decidere di ascoltare e reagire al fine di disaccoppiare azioni/responsabilità e ridurre gli orchestratori.
Nell'esempio precedente per la proprietà calendarEvent
verrà generato un segnale calendarEventChanged
.
Una shortcut al patter edit "ref-prop and mark as changed" è utilizzare la funzionalità built-in nel this editRefVal
, il quale permette di effetuare modifiche interne a un ref e segnalare il cambiamento automaticamente.
$ => $.this.editRefVal.calendarEvent(cv => { cv.title = "Go to the mall" })
La keyword "props" non è l'unica che si può utilizzare per dichiarare delle variabili. Le altre sono:
- let
- data
e sono determinate solo dalla semantica che lo sviluppatore vuole utilizzare.
Inoltre, come vedremo anche per le altre "keyword", è possibile evitare di scriverele in un oggetto assegnato alla keyword, utilizzando una definizione compatta "inline" con dash-case. E' quindi possibile dichiarare una variabile come:
- "let_" + "myPropName"
es:
{ div: {
let_property1: "a value", // preferred way!
let_property2: 123
}}
In ultima analisi, nel caso in cui si inserisca una "keyword" nella definizione che NON è tra le reserved e/o non viene riconosciuta, questa diventerà per Cle una definizione di una variabile. Dunque per dichiarare una variabile è sufficiente scrivere:
{ div: {
property1: "a value", // preferred way!
property2: 123
}}
Le funzioni vengono definite tramite la key "def
" e devono avere come primo argomento un riferimento allo scope $
, come in Python, seguito dagli altri argomenti.
Riprendendo il nostro esempio del counter:
const TheCounter = { div: {
id: "myCounter",
props: {
counter: 0
},
def: {
increment: ($, incAmount=1) => {
$.counter += incAmount
}
},
'=>': [
...
{ button: {
text: "Counter +1",
handle: { onclick: $ => $.inctrement(1) }
}},
{ button: {
text: "Counter +5",
handle: { onclick: $ => $.inctrement(5) }
}},
]
}}
Per le sole funzioni è possibile definire dei namespace
in cui inserire funzioni (massimo 1 livello di profondità). Ad esempio:
{
def: {
increment: ($, incAmount=1) => {
$.counter += incAmount
},
appUtils: {
getRandomNum: $ => Math.random()
capitalize: ($, str) => str[0].toUpperCase() + str.slice(1)
}
},
}
...
$ => $.appUtils.capitalize("hello world!")
E' anche possibile definire delle funzioni come costanti in una props utilizzando l'utils asFunc
import {asFunc} from "cle.js/lib"
{
myFunc: asFunc(($, ...args)=>{ ... }) // const/prop function
}
// using standard usage: $ => $.scope.myFunc()
Come per le variabili, esiste poi la possibilità di collassare la definizione keyword: object
tramite:
- "def_" + "myFunction"
{ div: {
def_myFunc: $ => { ... }
}}
I Segnali in Cle sono un meccanismo PUB/SUB like di tipo stream che i componenti possono definire e lanciare affinchè altri componenti (o essi stessi) possano decidere di ascoltare per reagire ad eventi ad esso collegati.
I segnali, come gli Event in Angular, possono trasmettere diversi tipi di dato, al fine di poter essere utilizzati non solo come notifica ma come meccanismo di Input/Outut, disaccoppiando prodicer dal consumer, e dunque codice.
Esistono due tipi di segnali:
Segnali "Puri
"- definiti tramite keyword "
signal
: { [name
]: "stream => (myData: string)
" }" // definition as descriptive string
- definiti tramite keyword "
Segnali connessi a variabili
- autogenerati da Cle per ogni variabile definita, nella forma "
xxxChanged
"
- autogenerati da Cle per ogni variabile definita, nella forma "
I primi trasportano solo i valori "emessi", mentre i secondi si differenziano dai primi in quanto trasportano il nuovo e il vecchio valore della property puntata, al fine di fare opportuni ragionamenti.
I segnali "puri" possono anche essere lanciati in modo imperativo utilizzando $.yyy.signal_name.emit(...args)
, mentre i secondi non possono essere lanciati direttamente ma vengono emessi da Cle al change del dato, o dopo una "mark as changed
" esplicita.
I segnali sono dunque locali al componente e vivono nel namespace dello stesso. Per ascoltarli sarà necessario poter puntare a quel namespace, (es via
E' possibile definire un segnale con:
const TheCounter = { div: {
...
signals: {
counterReset: "stream => (void)",
counterEditedFromUser: "stream => (newValue: string)"
}
...
}}
Per emettere un segnale possiamo riferirci ad esso con $.xxxx.signalname
e usare .emit(...args)
(con xxxx scopeSelector)
const TheCounter = { div: {
...
def_reset: ($, newVal) => {
...
$.this.counterReset.emit()
}
def_userManualEdit: ($, newVal) => {
...
$.this.counterEditedFromUser.emit(newVal)
}
...
}}
E' possibile collassare la definizione inline keyword: object
tramite:
- "signal_" + "signalName"
- "s_" + "signalName"
const TheCounter = { div: {
...
signal_counterReset: "stream => (void)",
signal_counterEditedFromUser: "stream => (newValue: string)"
// or
s_counterReset: "stream => (void)",
s_counterEditedFromUser: "stream => (newValue: string)"
...
}}
Per ascoltare un segnale è sufficiente dichiararne la gestione tramite keyword "on_s
" oppure "on
". Ad esempio, per agganciarsi ai due segnali precedenti:
{
...
on: {
// scope selector
this: {
// signal
counterReset: $ => { // action
console.log("counter has ben reset")
},
counterEditedFromUser: ($, newVal) => {
console.log("counter edited from user, new value:", newVal)
}
},
// scope: {
// ...signalInScope...
// },
// exception for: le, ctx and ref scopes:
// le: {
// ...leComponentById... : {
// signal
// }
// }
}
...
}
Esiste poi una shortcut per dichiarare la gestione inline:
{
...
// "on"_"scope selector"_"signal": action
on_this_counterReset: $ => { ... },
on_this_counterEditedFromUser: ($, newVal) => { ... }
// exception for le and ctx:
// "on"_"le"_"id"_"signal": $ => { ... }
...
}
In case of "scope" selector it can be omitted as it's the default:
{
...
on_counterReset: $ => { ... }
on_counterEditedFromUser: ($, newVal) => { ... }
...
}
Allo stesso modo è possibile registrarsi a una propertyChanges (tramite scope selector e mangling "propertyName" + "Changed"):
const myComponent = { div: {
props: {
status: "active"
}
on_this_statusChanged: ($, newStatus, oldStatus) => { // action
if (newStatus !== oldStatus){
console.log("status changed!")
}
}
...
}}
E' sempre possibile utilizzare codice imperativo per creare segnali e fare subscribe/unsubscribe dinamicamente. All'interno di $.this
troviamo infatti i metodi:
subscribe
:- subscribe to signal and return the unsubscriber
subscribeAsync
- same as subscribe but async
unsubscribe
Per generare nuovi segnali è invece possibile utilizzare lo scope "u
" (utils), in cui si trova $.u.newSignal
.
E' possibile inviare un segnale a un tempo stabilito con:
- emitLazy(t=1, ...args)
E' anche possibile aspettare la gestione del segnale e ricevere le risposte tramite:
- emitWaitResp - wait for all handlersa and capture the response data (if any). To send back a response in a on_xxx_signalName simply return a value in the handler function
- emitWaitFirstToResp - wait only the first response not "undefined" and STOP subsequent handlers. Signal delivered at most once.
- emitWaitRespCondition - same as first to resp but with a specific condition (func as first argument).
// on the emitter:
const responses = $.scope.newElement.emitWaitResp($.element)
...
// on the handlers
on_newElement: ($, el) => {
return el.prop1 === "type1" ? $.someData : undefined
}
Dbus è un meccanismo di eventi globale, del tutto simile a Signals ma a livello globale.
Per dichiarare un segnale DBUS è necessario utilizzare la "keyword" dbus_signals
:
{ div: {
...
dbus_signals: {
globalCounterReset: "stream => (void)",
}
}}
Per ascoltare un segnale DBUS si utilizza lo scope selector "dbus" in una "on" / "on_s"
{ div: {
...
on: {
dbus: {
globalCounterReset: $ => { ... }
}
}
}}
Per lanciare un segnale DBUS basta utilizzare lo scope speciale $.dbus
in cui si possono trovare tutti i segnali globali ed emetterli con $.dbus.dbus_sginal_name.emit(...args)
L'utilità di un sistema come DBUS è quello di avere un meccanismo di eventi dipendente da un concetto / contratto. Basta infatti conoscere il nome del segnale per poter far sì che diverse parti dell'applicazione possano seguirlo.
Anche per i segnali DBUS è possibilie collassare la definizione inline tramite:
- "dbus_signal_" + "signalName"
{ div: {
dbus_signal_globalCounterReset: "stream => (void)"
}}
Anche per dbus troviamo le varianti della emit definite per i Signals.
Gli attributi HTML in Cle vengono dichiarati dalla keyword "attrs
" e la sua shortcut "a
". Gli attributi permessi in questa definizione sono solo quelli testuali
(quindi non le funzioni come onclick etc, che vanno gestiti in "handle").
const coloredDiv = { div: {
attrs: {
class: "colored-div full-height",
style: "color: red",
mycustomattr: "abc",
...
}
// shortcut inline:
// "a_" + "attributeName":
// a_class: "colored-div full-height"
// a_style: "color: red"
}}
Per i soli attributi style e class è possibile anche omettere la shortcuts "a_" e scriverli direttamente come:
const coloredDiv = { div: {
class: "colored-div full-height",
style: "color: red"
}}
Per il solo attributo "style" è possibile scrivere lo stile inline come object (camel case, automaticamente convertito in kebab-case ad eccezione delle proprietà che iniziano per "::" passate come stringhe)
const coloredDiv = { div: {
attrs: {
style: {
color: "red",
fontSize: "15px",
// start with :: to keep key untouched
"::-webkit-box-shadow": "0px 6px 32px -1px rgb(0 0 0 / 42%)"
}
}
}}
Gli attributi possono essere dichiarati anche come Evaluable, e dunque seguire cambiamenti nei dati come per le property
const coloredDiv = { div: {
props: {
status: 'active'
},
attrs: {
class: $=>"colored-div full-height " + ($.status === 'active' ? 'actv-sts' : '')
style: $=>({
color: $.status === 'active' ? "red" : "black"
}),
mycustomattr: "abc",
}
}}
Grazie a questo meccanismo è possibile realizzare 2 way data binding
utilizzando la funzione "Bind
" (vista meglio nella sezione dedicata).
Per esempio, su un campo di input:
import { Bind } from "cle.js/lib"
const inputBar = { input: {
props: {
text: 'Initial Text'
},
attrs: {
value: Bind($=>$.text)
}
}}
o per esempio con una select
import { Bind } from "cle.js/lib"
const mySelect = { select: {
props: {
selected: "val1"
},
attrs: {
value: Bind($ => $.selected)
},
'=>': [
{ option: { a_value: "val1", text: "Value 1" }},
{ option: { a_value: "val2", text: "Value 2" }},
]
}}
E' inoltre possibile definire gli attributi / proprietà NON inline dell'elemento html associato via hattrs
e la sua shortcut "ha_
".
a differenza degli attributi attrs, che vengono impostati in modo safe tramite setAttribute, questi NON sono attributi safe, e dunque potenzialmente pericolosi se non si sa cosa si sta facendo. Vengono comunque messi a disposizione in quanto per la maggior parte utili (tutti quelli che non sono funzioni o che possono essere valutati come funzioni, come gli onclick etc) e sicuri. Inoltre rappresentano, in certi casi, l'unico modo per impostare alcuni attributi in modo dichiarativo con l'aggiunta della detect changes.
Possono essere utilizzati per definire proprietà nested, oppure proprietà non presenti nella dichiarazione HTML, come scrollTop
const scrollableDiv = { div: {
props: {
scrollPosition: 0
},
hattrs: {
"scrollTop": Bind($ => $.scrollPosition)
// possibile anche dichiarare la detectChanges come lazy:
// "@lazy:scrollTop": Bind($ => $.scrollPosition)
// nested prop:
'myAttr.nested.prop': "some-value",
// ad esempio in una form:
// "@lazy:form.elements.formElement.value": Bind(...) // lazy necessario per accedere a "formElement"
}
// shortcut:
// "ha_" + "objectAttributeName":
// ha_scrollTop: ...
}}
un suo utilizzo molto particolare come nested prop è per esempio poter definire un evaluable per un singolo componente dello style:
const coloredDiv = { div: {
props: {
status: 'active'
},
"ha.style.fontSize": "15px"
"ha.style.color": $ => $.status === 'active' ? "red" : "black"
}}
Per gestire gli eventi HTML come ad esempio onclick è necessario usare la keyword "handle
" o la sua shortcut "h_
".
L'evento HTML originale viene passato come secondo parametro (il primo è e deve sempre essere "$
")
const myButton = { button: {
text: "Do Some Action",
handle: {
onclick: ($, event) => ...,
onmouseenter: $ => ...,
onmouseleave: $ => ...
}
// shortcuts inline:
// "handle_" + "oneventname"
// "h_" + "oneventname"
// "ev_" + "oneventname"
// handle_onclick: ($, event) => ...,
// h_onclick: ($, event) => ...,
// ev_onclick: ($, event) => ...,
// ma anche:
// "oneventname" + "_event"
// onclick_event: ($, event) => ...,
}}
Unico caso speciale per "onclick" che può essere definito senza shortcut come per gli attr "style" e "class".
{
onclick: ($, evt) => ...
}
Esistono però casi in cui bisogna personalizzare in modo più importante gli eventi. in questo caso si può utilizzare la keyword "when
" e la sua shortcut "w_
"
si può utilizzare per catturare eventi particolari, oppure per cambiare la stategia capture. Questo perchè di fatto gestisce gli html event come addEventListener (che è configurabile!).
Possiamo passare un evaluable/handler direttamente come per "handle" oppure un oggetto
contenete l'handler e le options della addEventListener
{
...
when: {
focusin: ($, e) => ...standardAction...
focusout: {
options: {capture: true, useCapture: true}, // addEventListener options!
handler: ($, e) => ...standardAction...
}
}
// shortcuts:
// "w_" + "eventName"
// when_focusin: ($, e) => ...,
}
per lo styling conigliamo l'utilizzo di una librearia di css-in-js come csz (https://github.com/lukejacksonn/csz). Con questa libreria è possibile definire classi css direttamente nella definizione delle classi usate dall'elemento
const css = (await import('https://unpkg.com/csz')).default;
// declare global style
const globalStyle = css`
:global(body){
padding: 10px; margin: 0px; width: 100%
}
:global(*){
box-sizing: border-box !important;
}`
// reusable class
const highlightOnHover = css`&:hover{ transform: scale(1.05)}`
{ div: {
class: [
css`display: flex; justify-content: space-between;`,
highlightOnHover
].join(" "),
}}
Esistono però due keywords dedicate al css per scrivere classi css senza l'uso di librerie esterne
la keyword "css
" accetta una lista di stringhe, contenenti le definizini css da appendere al foglio.
{ div: {
let_transformScale: 1.05,
css: [
`
div { padding: 5rem }
.flex-item {
display: flex;
justify-content: space-between;
}`,
$=>`.flex-item:hover{
transform: scale(${$.transformScale})
}`,
]
}}
Il problema di questo approccio sta nella separazione delle classi nei vari componenti, in quanto agisce come style globale.
come miglioramento troviamo il complesso sistema di “s_css", che permette di rendere le classi css dichiarate solo nei sottofigli, (incapsulando all'indietro). Con questo meccanismo è inoltre anche possibile modificare dall'esterno le regole css definite quando veine riutilizzato un componente tramite Use/Extended.
{ div: {
s_css: {
".myClass": [{
display: "inline"
}]
".myClassDynRule": [$=>({
display: $.scope.condition ? "inline" : "none"
})]
} // Use/Extended(component, {s_css: {".myClass": [ExtendSCSS, {opacity: 0.8}]}})
}}
visita la demo demo/misc-example.js/ -> appDemoSCSS per altre info
Tra le varie shortcuts linguistiche presenti in cle, in grado di migliorare la sintassi e al tempo stesso la velocità di scrittura del codice troviamo gli Smart Component
e le Smart Function
(Evaluable).
Per utilizzare gli Smart Component
è sufficiente importare cle
.
import { cle } from "cle.js/lib"
cle è una shortcuts linguistica che permette di scrivere componenti Cle come :
cle.xxxHTMLTAGxxx(standardDefinition, ...childs)
convertendoli in:
{ xxxHTMLTAGxxx: {
...standardDefinition,
"=>": [
...childs
]
}}
Con l'eccezione per ciu standardDefinition
può essere anche una Stringa o un Evaluable, e in quel caso viene convertito in:
{ xxxHTMLTAGxxx: [ ...childs ]}
Possiamo riscrivere il componente TheCounter
di prima come:
import { cle } from "cle.js/lib"
const TheCounter = cle.div(
// Definition
{
id: "myCounter",
props: {
counter: 0
},
},
// Childs
cle.h3("Hello Dev!"),
cle.div($ => "The Counter Is: " + $.scope.counter)
cle.div($ => "The Counter Is: " + $.counter)
cle.div($ => "The Counter Is: " + $.parent.counter)
)
Allegerendone la sintassi.
Nel caso in cui i figli siano tutti stringhe o evaluable e non ci sia una definition è possibile anche scrivere:
cle.div("Text ", $=>$.evaluable, ...)
La seconda shortcuts riguarda invece la possibilità di scrivere Funzioni (Evaluable) inline tramite stringhe di testo, inserendo dei riferimenti a variabili dei componenti Cle tramite "@
". Ricorda, anche se scritte come testo sono a tutti gli effetti Funzioni, dunque NON provare a fare metaprogrammazione combinando o manipolando le Smart Function come faresti con le stringhe ed eviterai spiacevoli attacchi XSS!
Per utilizzarla basta importare le funzioni f
o fArgs
import { f, fArgs } from "cle.js/lib"
Il funzionamento di f è quello di un Template literals
(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) pr cui viene chiamata non con delle parentesi ma scrivendo prima di una stringa con "backtick
".
- Nota: viene utilizzata tale sintassi solo per allegerire visivamente la definizione, non per dei veri e propri templating!
Potremo dunque referenziare le variabili xxx nello scope
con f`@xxx` oppure quelle del parent come f`@p.xxx`.
Possiamo riscrivere il componente TheCounter
di prima come:
import { cle, f } from "cle.js/lib"
const TheCounter = cle.div(
// Definition
{
id: "myCounter",
props: {
counter: 0
},
},
// Childs
cle.h3("Hello Dev!"),
cle.div(f`"The Counter Is: " + @counter`)
cle.div(f`"The Counter Is: " + @p.counter`)
)
Full shortcuts Reference:
- "
@xxx
" -> $.scope.xxx - "
@s.xxx
" -> $.scope.xxx - "
@p.xxx
" -> $.parent.xxx - "
@t.xxx
" -> $.this.xxx - "
@l.xxx
" -> $.le.xxx - "
@c.xxx
" -> $.ctx.xxx - "
@m.xxx
" -> $.meta.xxx - "
@d.xxx
" -> $.dbus.xxx
Nel caso di Evaluable più complessi è possibile utilizzare le parentesi graffe per indicare che il codice è su più righe e gestiremo da noi la return (non è inline):
...
cle.div(f`{
const txt = "The Counter Is: ";
return txt + (@counter * 2);
}`)
...
Le fArgs entrano in gioco quando vogliamo invece definire funzioni che però accettano più parametri, come ad esempio la gestione di un evento HTML, che può essere definito come:
...
cle.div({
handle_onclick: fArgs("$event")`{
console.log("The html target is:", $event.targt);
console.log("Counter incremented! New: ", @counter);
}`
})
...
Per la definizione di componenti che devono essere presenti o meno in base ad una condizione (es *ngIf in Angular) utilizziamo la keyword "if
" nel "meta".
la keyword meta
di fatto è un contenitore di meta-programmazione, per cui si stabilisce il funzionamento di ogni componente.
Per realizzare un componente "opzionale" basta scrivere:
{ div: {
meta: { if: $ => $.scope.condition === true },
...
}}
Per realizzare componenti ripetuti, come una lista di elementi, si utilizza la definzione "forEach
": "meta-var-definition-name", "of
": evaluable/const nella keyword "meta
"
{ div: {
meta: { forEach: "item", of: $ => $.scope.items },
...
}}
// tips: 'f' is encuraged here:
// meta: { forEach: "item", of: f`@items` },
grazie a questa sintassi il componente verrà ripetuto N volte, e verrà generata la variabile descritta in "forEach" nello scope selector "meta" (e ovviamente nello scope, essendo il meta incluso), al cui interno si potrà trovare il valore dell'elemento corrispondente alla propria posizione.
Qui a a differenza di altri framework come React, in cui la generazione di componenti ripetuti è realizzata tramite codice imperativo (es .map, o in generale cicli), si utilizza un approccio più descrittivo, alla Angular. L'idea è che basta dichiarare nel meta che è da ripetere per avere N elementi simili. Ciò implica che NON è possibile fare
// Error!
{ div: {
'': $ => $.scope.items.map(item=>(
{ h2: { text: item }}
))
}}
questa sintssi non farebbe altro che generare un evaluable che Cle proverebbe a renderizzare come testo alla sua risoluzione, e dunque il risultato a video sarebbe un [Object, Object..].
Questo perchè Cle è un framework prettamente dichiarativo, non avrebbe senso mischiare codice imperativo/funzionale in un contesto del genere.
// Correct!
{ div: {
'':
{ h2: { meta: { forEach: "item", of: $ => $.scope.items },
text: $ => $.meta.item // also $.scope.item or $.item
}}
}}
a differenza di framework come react in cui è impossibile modificare un elemento di una lista in una map senza passare dalla useState in Cle è possibile modificare direttamente un singolo item via semplice assegnazione con $.item = ...
questo perchè il meccanismo interno di Cle possied il vero riferimento e fa in modo che l'utente modifichi realmente quello.
Come già detto però modificare un singolo elemento di una lista non porterebbe a una detect changes, quindi come in react bisognerebbe comunque riassegnare l'intero array.
E' però possibile abilitare nel meta l'opzione metaPushbackAutomark: true
. Questo farà si che il changes venga identificato correttamente per le assegnazioni dirette: $.item
= 123
Al fine di ottenre un boost delle performance nel repaint è consigliato l'utilizzo del flag optimized: true
nel meta. Il repaint in questo caso sarà solo dal primo elemento che cambia in poi.
Questo farà si che sia possibile definire anche un nuovo comparer
per la detect dei changes dell'intero array e/o dei singoli elementi con idComparer
Per ridurre al minimo il repaint è consigliato abilitare la modalità full_optimized: true
che migliora la optimize e il comparer con un algoritmo più lento ma che garantisce di non effettuare repaint se non per gli elementi realmente modificati. In questa modalità l'idComparer con id stabili è altamente consigliato.
{ meta: {
optimized: true, // enable optimization
full_optimized: true, // full optimizatione (slower). disable ALL repaint not required by near-full comparison
// Array Comparer, per identificare i changes dell'array, to change the "edit ref by new val" pattern
comparer: (newArr, oldArr) => newArr !== oldArr
// Comparer of each elements of the Array, to boost performance
idComparer: (newEl, oldEl) => newEl !== oldEl
}}
Tramite la keyword "define
" in "meta" è possibile definire delle variabili "helpers" che verranno inserite nel meta. Le 5 variabili standard sono: index, first, last, length, iterable, che possono essere utilizzate per avere info sull'elemento dell'array ripetuto.
{ meta: {
forEach: "item", of: $ => $.scope.items,
define:{
// helpers: "set-local-name-in-meta"
index: "idx",
first: "isFirst",
last: "isLast",
length: "len",
iterable: "myArr",
}
}}
Pur essendo sostituibile con degli leIf esiste una definizione specifica per gli switch-case:
import {Switch, Case} from "cle.js/lib"
{ div: {
...
'': [
...Switch(
Case(
value | evaluable,
component // { h2: { ... }}
),
Case(
value | evaluable,
component // { h2: { ... }}
),
...
)
]
}}
Scope Options, inside meta:
meta: {
newScope: bool (false) // block scope visibility
noThisInScope: bool (false) //remove this from the scope
noMetaInScope: bool (false) // remove meta variable in scope
hasViewChilds: bool (false) // materialize Cle Object
}
Componentizzare in Cle significa principalmente 'splittare' il codice in diversi oggetti riutilizzabili (variabili/costanti). A differenza di altri framwork non esiste un vero e proprio concetto di "Componente" da dover definire obbligatoriamente.
Un vero componente Cle è di fatto un singolo elemento HTML. Grazie però allo scope
e al ctx
, nonchè alle due funzioni Use
ed Extend
è possibile definire un 'componente' in modo abbastanza simile a ciò che siamo abituati a usare.
Questa scelta di fatto aiuta a pensare alla separazione e componentizzazione del codice come un operazione facile e veloce come in React (ctrl+c, ctr+v), piuttosto che onerosa come in Angular (ng-generate, ctrl+c, ctr+v).
Inoltre, rispetto a framework come React, non è necessario passare in avanti esplicitamente (o con i context manager..) le i getter e setter dello stato, in quanto grazie allo scope
queset sono esposte e visibili automaticamente, a meno di non essere splicitamente disabilitato. In questo modo si evita di passare in avanti infinite props e in particolare l'uso di id
e name
(aka reference) permette di evitare l'effetto "move state into parent components" per permettere la comunicazione, e dunque si ritorna ad avere uno "stato" vicino all'owner naturale.
Pur sembrando un antipattern, rilassare questo vincolo, ovvero che i child possono vedono proprietà del padre, se ben utilizzato tramite i Cle Object (Model/Controller/Service..) permette un incredibile modularità, spostando il concetto di dipendenza come accoppiamento di codice (che tende a generare boilerplate code) a dipendenza come "convenzione" / "concetto atteso" (Coding By Convention, DI like). Per non cadere nel loop del "what is used?" consigliamo di esplicitare sempre le dipendenze tramite "deps".
Resta però sempre possibile decidere di bloccare lo scope automatico e passare in modo imperativo proprietà, metodi etc tramite costruttore.
Dal momento che un componente Cle è un Object, per definire dei componenti riutilizzabili basta semplicemente assegnare questi oggeti ad una variabile e dunque per utilizzarli basterà usare tali variabili come childs.
const ReusableH1 = { h1: { text: "I'm reusable!" }}
const ReusableH3 = { h2: { text: "Whenever you want" }}
const App = { root: {
'': [
ReusableH1,
ReusableH3,
{ br: {}},
ReusableH1,
ReusableH3,
{ div: "other definitions.." }
]
}}
Grazie alla sua natura "POJO" (Plain Old Javascript
Object), allo scoping veso il basso e agli id/ctx_id, la modalità predefinita per "passare" valori
ad un componente è quella della "ridefinizione". Questo concetto, molto familiare agli utilizzatori di QML, è quello di fare "override" di una qualsiasi definizione/keyword del componente, e dunque impostare i valori, le properità da seguire, le reazioni ai segnali. E' possibile addirittura cambiare il comportamento originale. Possiamo infatti utilizzare la funzione "Use"
da lib per dichiarare l'utilizzo di un componente e allo stesso tempo passare delle "redefinitions"
.
Use(
component,
redefinitions = { [keyword]: value } | undefined,
// options
{
strategy = "merge" | "override", // merge is default
init = { [arg]: value } | undefined,
inject = { [placeholderName]: cle component } | undefined
}
)
possiamo pensare a redefinitios come una shortcuts per:
{...definition, ...redefinitions}
ma molto più evoluta, in quanto effettua, è possibile infatti decidere la strategia della redefinitions:
- "override" assume che la redefinitions è a cura dell'utente e dunque è in tutto e per tutto identico al codice precedente
- "merge" effettua una merge delle nuove chiavi in cascata (senza cancellare le vecchie)
Ad esemprio la merge strategy (default) in questa redefinition:
const Component = { div: {
props: {
var1: "",
}
}}
Use( Component, {
props: { var2: "content" }
})
produrrebbe di fatto:
{ div: {
props: { var1: "", var2: "content" }
}}
Inoltre l'utilizzo di Use su un cle element farà si che questo diventi il root di un nuovo context (ctx), valido per lui stesso e tutti i suoi sottofigli che NON sono componenti Use, ovvero un vero e proprio componente.
import {RederApp, Use, Bind, cle} from "cle.js/lib"
const InsertPercentageComponent = cle.div({ // ctx id is by defult "root" for component in Use
// component's public scope (via ctx.root...)
let_initialText: "",
def: {
getInitialInputText: $ => $.initialText,
getOriginalInputText: $ => $.ctx.inputBar.text,
getCorrectedInputText: $ => $.ctx.output.correctedText,
setText: ($, txt) => { $.ctx.inputBar.text = txt },
resetText: $ => { $.ctx.inputBar.text = "" },
}
},
// Component's private scope can be created in this way (accessed in this component via ctx.private...)
{ Model: { ctx_id: "private",
let_privateVar: "hello i'm private",
}},
// Ordinary childs, their scope are not visible from outside
cle.input({ ctx_id: "inputBar",
let_text: $ => $.initialText, // Because of Bind it's copied once, then overwritten!
a_value: Bind(f`@text`)
}),
cle.div({ ctx_id: "output",
let_correctedText: $ => $.ctx.inputBar.text.length > 0 ? ($.ctx.inputBar.text.toFixed(1) + "%") : ""
},
$ => $.correctedText
),
cle.div($ => $.ctx.private.privateVar)
)
// Usage
RenderApp(document.body, cle.root({},
Use( InsertPercentageComponent, { ctx_ref_id: "nameInParentContext", // or define a "name" and use childsRef in root
let_initalText: "0.0"
}),
{ Controller: { // refer to used component by name in this context
onInit: $ => console.log($.ctx.nameInParentContext.getCorrectedInputText())
}}
))
Con l'utilizzo della Use è anche possibile utilizzare l'opzione "init"
per passare valori al "constructor". Il suo funzionamento è molto semplice, basta passare un dizionario contenete le variabile del constructor e il loro valore, tenendo in considerazione che il punto di vista degli evaluable sarà quello del parent. Questa modalità è da preferire per i casi di componenti per cui si vuole creare un nuovo scope (bloccando in sostanza la visibilità dello scope parent), e si vuole passare una variabile "bindata"
Visita la demo in demo/misc-example.js/ -> appDemoConstructor per altre info sui constructor.
Una delle possibilità della Use è quella di andare a definire in maniera dichiarativa quali sono i sottocomponenti che è possibile "sostituire", ovvero quelli che si possono "iniettare" all'interno di un componente che verrà utilizzato tramite Use. In particolare basterà definire dei "Placeholder" nel componente che prevede l'injection, e utilizzare il parametro "inject" della Use.
// Signature
Placeholder(name, {default_component=undefined | cle element, must_be_redefined=false, forced_id=undefined, forced_ctx_id=undefined, replace_if_different=true})
Per dichiarare un titolo e un body di deafult all'interno del nostro componente che però potrebbe essere sostituito in altre parti ci basta definire dei Placeholder:
const EditableComponent = cle.div({
title: "This is the title"
},
Placeholder("titleEl", { defualt_component: { h2: $ => $.title } }),
{ h5: "Sub-title NOT EDITABLE" },
Placeholder("bodyEl", { defualt_component: { p: "This is the body" }, must_be_redefined: true }),
cle.div({},
cle.subelement({},
Placeholder("nestedEl") // no default defined. will be "evicted" if not injected.
))
...
)
Use(EditableComponent, pass, { inject: {
titleEl: { h1: "A Simple Title" },
bodyEl: cle.div({}, cle.b("A Simple Bold Body")),
}})
è anche possibile specificare che un elemento deve essere necessariamente specificato, e inoltre è possibile devinire dei Placeholder in tutti i sottochilds per iniezioni profonde. kbl
Se non vogliamo creare un nuovo contesto ma vogliamo comunque sfruttare le potenzialità della Use è possibile utilizzare la "Extended". Questa utility è in tutto e per tutto identica alla Use, ma la differenza principale è che "inietterà" il componente nell contesto padre, senza generarne uno nuovo.
Un terzo modo in cui è possibile creare componenti è quello delle Factory, ovvero funzioni che restituiscono componenti, e dunque una modalità più "React" style. tenendo però bene a mente che la risoluzione del componente NON sarà a run-time, e dunque NON è possibile modificare il componente a run time come in React negli "High Order Components". Tale possibilità è però raggiungile tramite le modalità Dinamiche come per esempio Dynamic Lazy Render e i SubRenderer.
L'idea alla base è quella di sfruttare lo scope o altre sofisticazioni come le external props etc, per passare a una funzione che costruirà il componente come serve a noi, ovvero come se lo avessimo scritto all'interno della definzione del parent direttamente.
const MyComponentByFactory({getText, setText, counter, incCounterFunc, onCounterChanged, bodyElement, ...subElements}) => cle.myComponent({
readOnlyCounter: counter, // read only! because every set will replace the evaluable to a value!
txt: Alias(getText, setText), // react style..this is a convenient method to recompose property
def_incCounterFunc: incCounterFunc,
on_readOnlyCounterChanged: onCounterChanged,
},
cle.h1($ => $.txt),
cle.div("The counter is: ", $ => $.readOnlyCounter) // or cle.div("The counter is: ", counter) if counter is in scope!
cle.button({h_onclick: $ => $.incCounterFunc()}, "Inc Counter"),
cle.div({},
cle.subelement({},
bodyElement || ""
)
),
...subElements
)
RenderApp(document.body, cle.root({
aCounter: 10,
aTxt: "Hi this is a Text",
def_incCounter: $ => { $.aCounter += 1 }
},
cle.h2("Hello from component factory"),
MyComponentByFactory({
getText: $ => $.aTxt,
setText: ($, v)=>{ $.aTxt = v },
counter: $ => $.aCounter,
incCounterFunc: $ => $.incCounter()
onCounterChanged: ($, v)=> console.log("do something, aCounteris changed!", v)
bodyElement: { div: "this is the body" },
subElements: [
cle.div("hi"),
cle.b("bold")
]
})
))
visita la demo demo/misc-example.js/ -> appDemoComponentFactory per altre info
T.B.D - Setup property on childs using the point of view of the parent without constructors / use. This enable react style coding using functions (functional components).
T.B.D
Components Registry is a Global Cle.js Components Dependency Injection System via tag name.
Define and use components by tag name without import into code (IoC / Dependency Injection)
Define and register a component into the ComponentsRegistry
, then use it in the tree by using the registered name as the tag with a prefix:
'component-'
or'component_'
- Retrive a "pure" cle component
'use-'
or'use_'
- Retrive a "Use "component and setup the
Use
function arguments
- Retrive a "Use "component and setup the
'extended-'
or'extended_'
- Retrive an "Extended" component and setup the
Extended
function arguments
- Retrive an "Extended" component and setup the
Define Example:
import { ComponentsRegistry } from "cle.js/lib"
// cle way
ComponentsRegistry.define({ Rectangle: {
...def...
}})
// or as function
ComponentsRegistry.define({ Rectangle: (props) => ({
...def...
})})
then import this file to be executed
Usage Example:
...
{ div: {
"=>": [
{ 'component-Rectangle': {} } // pure component retrive
{ 'use-Rectangle': {...Use overrides...} } // overrides only
{ 'use-Rectangle': [{...Use overrides...}, ...otherUseArgs] } // full Use args
// usage for function style is the same, except that overrides will be the "props/args" of the function
{ 'component-Rectangle': {} } // pure component retrive
{ 'use-Rectangle': {...props..., extra_use_args:...Use overrides...} } // overrides only
{ 'use-Rectangle': [{...props..., extra_use_args:...Use overrides...}, ...otherUseArgs] } // full Use args
]}}
The components defined into ComponentsRegistry can be also used into "html" components & derivates as the where into "deps".
Example:
...
<view>
<use-Rectangle [overrides]="as in standard html components" [use-options]="as in standard html components" >...childs injection</use-Rectangle>
</view>
...
Tips: for "use args" in function like used inside html component try [raw-def-extra_use_args]="({...})"
T.B.D
T.B.D
T.B.D
T.B.D
T.B.D /demo/misc-example.js/directives
T.B.D /demo/misc-example.js/appDemoSupportSvg
Advanced: fromHtml, remoteHtmlComponent & fromHtmlComponentDef & defineHtmlComponent (V1 & V2 with localComponents)
Cle can easly used in combination with other framework like React. visit /src/demo/mashup/react to see some example
{ div: {
// Component Identifier
id: "String" // A UNIQUE Identifier in the whole app
ctx_id: "String" // A UNIQUE Identifier in the Component Context
ctx_ref_id: "String" // A UNIQUE Identifier in the PARENT Context, used to be finded by ctx in the parent ctx.
name: "String" // A NON-UNIQUE Name, used to be finded by ref
// Life-Cycle Hooks
beforeInit: (unifiedDef, unifiedDefChilds) => editedUnifiedDef // handle and edit childs before init (at declare time, before self context creation)
constructor: ($, {...args}) => { } // Constructor, called at component creations
onInit: $ => { } // on Init, After constructor, but before childs creation
afterChildsInit: $ => { } // After childs onInit
afterInit: $ => { } // After 1ms of the afterChildsInit (auto-lazy)
onDestroy: $ => { } // After Childs Destroy, Before Destoing this
onUpdate: $ => { } // Only called in le-for components, called on data update
// Component Data / Property definition
let | data | props: {
counter: 0 // a counter property
computedProp: $ => $.scope.myProp * 10 // evaluable / computed props
my_alias: Alias(getter $=>..., setter $,v=>..., cachingComparer true | (new, old)=>new!==old...) // Alias of a property, with custom op on set and get! // caching comparer can also be set to "true" for default caching strategy enabled!
my_alias2: SmartAlias('@counter') // Same as alias but simple def (second parametr is cachingComparer, set to true to enable default comparer stategy)
my_alias3: Bind('@counter') //Same as Smart Alias, but more generic
myFunc: asFunc(($, ...args)=>{...}) // declare function costants as props (to be passed)
},
// shortcuts -> let_counter: 0
'@input': { // property passed by the parent using parent Point Of View (useful with react style component functional definition)
inputProp1,
inputHandler
}
// Functions definition 'a-la-python'
def: {
resetCounter: $ => {
$.this.counter = 0
$.this.counterReset.emit() // also lazy: $.this.counterReset.emitLazy(10) or wait signal responses emitWaitResp, emitWaitFirstToResp, emitWaitRespCondition
},
// You can also define a first lvl namespace for functions
utils: { // def namespace example
toUppercase: ($, txt) => txt.toUppercase()
}
},
// shortcuts -> def_resetCounter: $ => ...
// Signals Declaration
signals: {
counterReset: "stream => (void)" // definiamo il tipo di segnale, poi "=>" e infine una descrizione dei params (es il tipo dei parametry, la signature etc etc..è solo testo che documenta!)
},
// shortcuts -> signal_counterReset: ".." | s_counterReset: ".."
dbus_signals: { // qui definiamo i segnali globali..un modo per creare uno stream su un canale comune con un compagno che non riesco a raggiungere facilmente "by name", e che entrambi conosciamo
iEmitThisGlobalSignal_UniqueName: "stream => (int: counter status)"
}
// shortcuts -> dbus_signal_XXXX: ".." | dbs_XXXX: ".."
// Props Change Handlers
on: {
// on who?
this: {
// on what?
// Mangling! propertyName + "Changed"
counterChanged: ($, newCounter, oldCounter) => console.log($.this.counter)
},
parent: ...
scope: ... any prop in self or any parent anchestor
le: ..byname.. : {props | alias changed},
ctx: ...qui mettiamo solo i nomi dei sub_componenti + this di questo componente!
ref: ... by name ...
},
// shortcuts -> on_this_counterChanged: ... | on_XXX_YYY:
// Signal Handlers
on_s: {
// on who?
this: {
// on what?
counterReset: $ => {
console.log("counter reset!")
// return a value that can be captured in an emitWaitResp
}
},
parent: ...
scope: ... any prop in self or any parent anchestor
le: .component byname.. : { signal},
ctx: ...qui mettiamo solo i nomi dei sub_componenti + this di questo componente!
ref: ... by name ...
dbus: ...qui mettiamo i signals globali che ascolto
},
// shortcuts -> on_this_counterReset: ... | on_XXX_YYY:
// HTML Attributes, setted via setAttribute
attrs | a : {
class: "someclass"
style: $=>({
width: 200,
height: $ => 200
}),
"@lazy:scrollTop": 100 // con il prefisso '@lazy:' indico che quell'attributo lo voglio inizializzare lazy!
value: Bind($ => $.this.counter) // per effettuare 2 way data binding!
},
// shortcuts:
// "a.style": {color: "red"}
// a_style: {color: "red"}
// hattrs ... "harmfulAttr" in pratica qui non settiamo via "setAttribute", ma direttamente via this.el.xxxx = e anche in ricorsione..potenzialmente pericoloso se non si sa cosa si sa facnedo.
hattrs | ha: {
scrollTop: "0px",
'style.backgroundColor': "red",
'myAttr.nested.prop': $ => "follow some stuff"
"@lazy:scrollTop": Bind($ => $.this.counter) // per lazy binding!
}
// shortcuts:
// "ha.style.color": "red"
// "ha_style": "color: red"
// extreme attributes shortcut:
// style: ... -> attr: { style: ... }
// class: ... -> attr: { class: ... }
// Html Event Handlers
handle: {
onclick: ($, e) => $.this.count++
},
// shortcuts
// handle_onclick: $=>...
// h_onclick: $=>...
// ev_onclick: $=>...
// onclick_event: $=>...
// onclick: $=>...
// Configurable Html Event Handlers
when: { // html event (in the form of addEventListener). More configurable!
focusin: ($, e) => $.this.count++
focusout: { options: {capture: true, useCapture: true}, handler: ($, e) => $.this.count++, }
},
// w_focusin: $=>...
// CSS Rule Definition
css: [
".class { bla:bli ... }",
$=>".r0 { .."+$.this.somedeps +"}"
] | {
rule1: ".class { bla:bli ... }",
rule2: $=>".r0 { .."+$.this.somedeps +"}"
}
// Better HOISTED CSS Definition
s_css:{
".myClass": [{
display: "inline"
}]
".myClassDynRule": [$=>({
display: $.scope.condition ? "inline" : "none"
})]
} // Use/Extended(component, {s_css: {".myClass": [ExtendSCSS, {opacity: 0.8}]}})
// Dependency Checks
deps: { // check a runtime dell'esistenza delle deps
scope: ["myTextProp", "myFunc"],
parent: ["myParentDepsProp"],
...
},
// shortcuts: array for scope only deps
// deps: ["myTextProp", "myFunc"]
// auto smartfunc -> CLE_FLAGS.AUTO_SMARTFUNC_ENABLED
// f_let_myvar: "2 * @counter"
// f_def_mylog: "console.log($arg, @counter)"
// f_attr_stye: "..."
// f_hattr_stye: "..."
// f_handle_onclick: "..."
// f_on_signal1: "..."
// f_on_le_el_signal: "..."
// f_onInit: "..."
// f_onDestroy: "..."
// meta: { f_if: "..." }
// meta: { froEach: "...", f_of: "..."}
// text: { f_text: "'this will be computed' + (2 * @counter)" }
// Meta-programming
meta: {
if: $ => ...condition ... // Conditional Component - f_if support
forEach: "myVar", of: $ => $.MyArrayVar, // Element Repeater - f_of support
// Repeater Helpers
optimized: false, bool // enable leFor optimization (RECOMANDED)
full_optimized: false, bool // enable leFor optimization (RECOMANDED)
define:{
// define in meta this variables with this name
index:"idx",
first:"isFirst",
last:"isLast",
length:"len",
iterable:"myArr",
//...CUSTOM_PROP_NAME: value | ($: "parent" $this (same as meta), $child: real $this of the child)=> { ... }
// todoLabel: $ => $.this.todos[$.meta.idx].label } // easy alias! todo_label: $ => $.this.
},
//comparer, per identificare i changes
comparer: (_new, _old)=>_new !== _old
// comparer for leFor Elements
idComparer: (_new, _old)=>_new !== _old
// Scope Options
newScope: false, bool, // block scope visibility
noThisInScope: false, bool, //remove this from the scope
noMetaInScope: false, bool, // remove meta variable in scope
hasViewChilds: false, bool, // materialize Cle Object
metaPushbackAutomark: true, bool // autoset value into array when a 'forEach' variable is edited and mark array as changed.
}
// Sub Elements
contains | childs | text | view | '>>' | '=>' | '' | _ : [
"A Pure Text. Counter is: ",
$ => $.scope.counter, // evaluable, autoupdated
{ div: {
text: $ => $.scope.counter
}},
{ Model | Controller | Service | Component | Connector | Signals | Style | Css : { // Pure Object, invisible, usefull for app logic and data manipulation. Using meta: { hasViewChilds: true } can be materialized into the view as <leobj></lepbj> and also is childs.
}},
// Reusage / Componentizzation
Use( MyComponent,
// redefinition / edit component
{
let_prop1: 123 || $=>$.prop3 || Alias || Bind ... // basic setup a property-value example!
handle: {
onclick: ...
}
on_s: {
this: {
mySignal: ($, ...args) => do whatever
}
}
...
},
// Options & init for constructor
{
strategy: "merge" | "override", // define redefinition strategy. Override will replace on 1st lvl
// constructor parametrer. the second option to pass data to child (specially for newScope components.)
init: {
childPropToInitInConstructorByValue: $ => $.meta.idx // copy by value ONCE
childPropToInitInConstructor2WayBinded: BindToProp("text") || BindToProp("$.le.app.text")// pass & bind value to. search in scope or pass a full name. This require constructor to use .getter & .setter
},
// Placeholder Injection/Substitution
inject: {
placeholderName: cle-element
}
},
[...otherChildsToInject...]
),
], // can also be a single compnent, avoiding the unecessary [].
// define component as owner of the specified "named sub.childs components"
childsRef: {
myName: "single",
myNameMulti: "multi"
},
directives: {
name: {
...definition...
}
}
// inline shortcut:
// dir_name: {...definition...}
}
// any others unknonw key that cannot be deducted will become a props
// this way you can also declare myProp: ... without props/let etc.
}
$ => {
$.this = {
el // renderized html element
parent // parent $.this
comp_id // user defined id via "id" keyword
comp_ctx_id // user defined id via "ctx_id" keyword
comp_ctx_ref_id // user defined id via "ctx_ref_id" keyword
t_uid // tecnical id, the cle unique id overall, setted as attr over html element. used for css hoist
getAsExternalProperty = (prop_name)=>{ return Property instance } // dumb utils to retrive the real Property behind the $.this proxy..useful to be used as "external" deps in a dynamic context.. (via set value as useExternal([extractedProp], $=>extractedProp.value))
getAsExternalSignal = (signal_name)=>{ return Signal instance } // dumb utils to retrive the real Signal behind the $.this proxy..useful to be used as "external" deps in a dynamic context..
// dynamic signals subscribe & unsubscribe
subscribe = (name, who, handler, upsearch=false) => { return unsubscribe function }
subscribeAsync = async (name, who, handler) => { return unsubscribe function }
unsubscribe = (name, who, upsearch=false) => { void }
// edit reference prop inline without manually mark as changed!
// use: $.this.editRefVal.myProp(p=>p.value=12)
editRefVal // Proxy with getter: .name(v => action function)
// edit array reference prop inline without manually mark as changed! this will change array ref (eg [...array])
// use: $.this.editArrRefVal.myProp(p=>p.value=12)
editArrRefVal // Proxy with getter: .name(v => action function)
// + every props and function defined
},
$.oss: {/* any defined by user [untracked] */ },
$.parent,
$.scope,
$.meta,
$.le,
$.ctx,
$.ref,
$.dbus,
$.u = {
// dynamic new signal
newSignal: (name, definition="stream => void")=>{ },
// fastSetScopedVarsAsChanged,
changed: (scope, ...scopedVars)=>{ },
// block and await for property condition, then get value.. (instant get if true)
// to be used to await for a prop! tester can be: function | array of values IN OR
// onInit: async $=>{
// let ready = await $.u.propCondition($=>$.le.db, "readyProp", v=>v===true, 60*1000) [wait max 60 sec]
// $.initData()
// }
propCondition: (scopeGetter, prop, tester=v=>(v !== null && v !== undefined), ms_timeout=undefined, retry=5)=>{ return Promise },
// block and await for signal fired (eventually with condition filter), then get value
signalFired: (scopeGetter, signal, condition=v=>true, ms_timeout=undefined, retry=5) => { return Promise }
// utils per andare ad ottenre l'elemento Cle da elementi HTML/DOM .. per fare cosy tricky :(
getCleElementByDom: (dom_el)=>{ return cleElement | throw Error("Null-Query-Sel") },
getCleElementsByDom: (...dom_els)=>{ return cleElements | throw Error("Null-Query-Sel") },
// alias for $.ref.xxx return One child if single, Many childs if multi
getChildsRef: (name)=>{ return childsRefList | childRef },
//serach only DOWN, breadth first algo
getSubChildRef: (name)=>{ return result | null },
//serach only DOWN, breadth first algo, return all match or limit
getSubChildsRef: (name, limit=0)=>{ return results },
// Lazy Render: dynamic create, get and render template at run time!
/**definition_as_func signature: (parent.$this.this, state, ...args) => obj || obj[] */
lazyRender: (definition_as_func, {afterCreate=undefined, beforeDestroy=undefined, afterDestroy=undefined, auto=false}={}, ...args)=>{ return rendered.map(e=>el.$this) }, // restituisco il loro $this (per la rimozione by ref)
getLazyRenderState: () => dynamicChildsState,
clearLazyRender: (generatedDynComp, clearState=false, clearDestroyHook=false)=>{ },
// UTILS PER SUB - SUB APP NESTING! necessario che oj_definition sia una definition standard! { xxx: {yyy}}
// called on $.u.new... use the $ as parent, but a specific html element as mount point (maybe a react element..)
newConnectedSubRenderer: (html_mount_point, oj_definition, lazy=false)=>{ return SubRendererComponent }
}
}