CORD is a small experimental reactive javascript library to web development. I am building this tiny monolitic library just because I hate big frameworks full of dependences and a lot of externals shits. So I am doing my own reactive mini javascript framework. What do I hope to achieve with this? Anything, I am doing it just for fun.
I'm building this library just for fun. I started with a simple idea and gradually made it a bit more complex, so this piece of code is a bit messy and should definitely not be used in a real-world setting.
I usually update the library, but it takes me a while to get around to updating the documentation. So some parts of the documentation might not be entirely accurate.
“Teaching by example isn’t just one way to educate—it’s the only way.”
Albert Einstein
Well, let's take a step back from Albert's perspective, and instead of “educate by example,” let's “learn from examples”.
Let's start with a simple index.html:
<!DOCTYPE html>
<html>
<head>
<title>My title</title>
<style>
body {
padding: 0;
margin: 0;
}
.main {
width: 100vw;
height: 100vh;
}
.main::after {
content: attr(color);
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 5rem;
}
</style>
</head>
<body>
<div cord-id="example">
<div style="background-color: rgb(${counter * 10}, ${counter * 10}, ${counter * 10});
color: contrast-color(rgb(${counter * 10}, ${counter * 10}, ${counter * 10}));">
<button onclick="$CORD.update('example', 'counter', ${counter}-1)">Dec -</button>
${counter}
<button onclick="$CORD.update('example', 'counter', ${counter}+1)">Inc +</button>
</div>
</div>
<script>
window.addEventListener('cordready', function(e) {
const config = {
createGlobals: false,
strict: false,
containers: {
example: {
counter: 0
}
}
};
$CORD.init(config);
});
</script>
<script src="cord.js"></script>
</body>
</html>What happen here? CORD did the bootstrap process and then dispach cordready event. Inside the
event we call $CORD.init(...) function. What do you learn from this?
- We manage a CORD app using the
$CORDobject. initis a mandatory function to start to operate with CORD.- In
configconst we define some settings and containers. What is a container? It is the main unit of CORD. Inside a container you hava access to a local variables (but also you can access other containers variables - more about this below). - In this example we defined a container named
examplewith a local variablecounterinitialized in zero. - During init, CORD renderize the container content.
- You can change interactively the value of a local variable using function
update(...).
Some notes:
CORD always try to render the minimal fragment possible of a container, not the complete container.
Using update is not the only way to change variable content, you can do the same with set
function (more below).
Let's make some improvement now...
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div cord-id="example">
<div style="background-color: rgb(${counter * 10}, ${counter * 10}, ${counter * 10});
color: contrast-color(rgb(${counter * 10}, ${counter * 10}, ${counter * 10}));">
<button onclick="$CORD.set('example:counter', ${counter}-1)">Dec -</button>
${counter}
<button onclick="$CORD.set('example:counter', ${counter}+1)">Inc +</button>
</div>
</div>
<script>
...
</script>
<script src="cord.js"></script>
</body>
</html>What changed? Now we used set instead of update. The function update is more useful when you
need to update many variables in one step. For example, if the container example were this:
...
containers: {
example: {
counter: 0,
color: 'red'
}
}
...you could call update in this way:
$CORD.update('example', {counter: 10, color: 'blue});set function is useful for atomic changes. The way you reference a variable is with a kind of
hierarchical string domain: "<container_name>:<local_varname>". But this is more useful yet!
If you have:
...
containers: {
example: {
counters: [{c: 0}, {c: 99}]
}
}
...you could do this:
$CORD.set('example:counters:0:c', 1000);Of course you also count with get function:
$CORD.get('example:counters:0:c'); // return: 1000
$CORD.get('example:counters:1:c'); // return: 99The set function count with a third parameters that allow to instruct CORD to render after set
the value. By default it is true. If false it is a lazzy update, i.e., you need to call:
$CORD.refresh('example'); // 'example' is the container idto let the changes take effect.
Now, what happen if we want to have many counters? Should we repeat the 2 buttons and the counter many times? No, in CORD you can use templates:
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<cord-template cord-tpl-id="tpl-counter">
<!--
<div class="counter"
style="background-color: rgb(${counter * 10}, ${counter * 10}, ${counter * 10});
color: contrast-color(rgb(${counter * 10}, ${counter * 10}, ${counter * 10}));"
color="#${(counter * 10).toString(16).repeat(3)}">
<button onclick="$CORD.update('%cord-id%', 'counter', ${counter}-1)">Dec -</button>
${counter}
<button onclick="$CORD.update('%cord-id%', 'counter', ${counter}+1)">Inc +</button>
</div>
-->
</cord-template>
<div class="main">
<cord-container cord-id="counter1" cord-tpl-ref="tpl-counter"
style="display: block; height: 50%;">
</cord-container>
<cord-container cord-id="counter2" cord-tpl-ref="tpl-counter"
style="display: block; height: 50%;">
</cord-container>
</div>
<script>
window.addEventListener('cordready', function(e) {
const config = {
createGlobals: false,
strict: false,
containers: {
counter1: {
counter: 10
},
counter2: {
counter: 20
}
}
};
$CORD.init(config);
});
</script>
<script src="cord.js"></script>
</body>
</html>As you can see, when the template is embeded in the html, you MUST enclose its content in html comment marks. That's not very elegant but it is needed because CORD need that the browser do not render the template content before process it. But do not worry, there is a more elegant solution and soon we will see it.
In this example notice the use of %cord-id% macro field. Using it you make sure that every
container that implement this template will reference itself. In fact you can pass more mapped
macro fields using the cord-map attribute in cord-container tag:
<cord-container cord="example1" cord-tpl-ref="counter"
cord-map="title:I am example 1|var2:anything">
...Then in your template you can use %title% and %var2% and wait that the CORD render make the
sustitution.
Now is time to use template in a more suitable way. Instead of embed the template in the html you can create independent files and put inside one or more templates without necesity of use comment marks.
Filename: counter.ctpl (the extension is your choise)
<cord-template cord-tpl-id="tpl-counter">
<div class="counter"
style="background-color: rgb(${counter * 10}, ${counter * 10}, ${counter * 10});
color: contrast-color(rgb(${counter * 10}, ${counter * 10}, ${counter * 10}));"
color="#${(counter * 10).toString(16).repeat(3)}">
<button onclick="$CORD.update('%cord-id%', 'counter', ${counter}-1)">Dec -</button>
${counter}
<button onclick="$CORD.update('%cord-id%', 'counter', ${counter}+1)">Inc +</
</div>
</cord-template>Filename: index.html
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<cord-load-templates>
./counter.ctpl
</cord-load-templates>
<div class="main">
<cord-container cord-id="counter1" cord-tpl-ref="tpl-counter"
style="display: block; height: 50%;">
</cord-container>
<cord-container cord-id="counter2" cord-tpl-ref="tpl-counter"
style="display: block; height: 50%;">
</cord-container>
</div>
<script>
window.addEventListener('cordready', function(e) {
const config = {
createGlobals: false,
strict: false,
containers: {
counter1: {
counter: 10
},
counter2: {
counter: 20
}
}
};
$CORD.init(config);
});
</script>
<script src="cord.js"></script>
</body>
</html>Special attributes of kind :<attr> are useful for boolean type attribute like disabled or
readonly.
See this example:
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div class="main" cord-id="main">
<button onclick="$CORD.toggle('main:disabled')">Toggle</button>
<input :disabled="${disabled}" />
</div>
<script>
window.addEventListener('cordready', function(e) {
const config = {
createGlobals: false,
strict: false,
containers: {
counter1: {
counter: 10
},
counter2: {
counter: 20
}
}
};
$CORD.init(config);
});
</script>
<script src="cord.js"></script>
</body>
</html>You could do the same setting onclick button attribute as
"$CORD.$.main.$disabled=!$CORD.$.main.disabled".
But there is a better way to do it...
Now we will use special attributes on:<event> and bind:<value>.
See this example:
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div class="main" cord-id="main">
<button on:click="$self.$disabled=!$self.disabled">Toggle</button>
<input :disabled="${disabled}" on:input="$self.$text=this.value" />
${text}
</div>
<script>
window.addEventListener('cordready', function(e) {
const config = {
createGlobals: false,
strict: false,
containers: {
main: {
disabled: false,
text: ''
}
}
};
$CORD.init(config);
});
</script>
<script src="cord.js"></script>
</body>
</html>Notice how inside on:... attribute you need to use $self. to reference any field of the current
container. Also you could use $global. to reference the field of another container.
Now see what happend with special bind:... attribute.
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div class="main" cord-id="main">
<label><input type="checkbox" bind:checked="$self.$disabled">Toggle</label>
<input :disabled="${disabled}" on:input="$self.$text=this.value" />
${text}
</div>
<script>
window.addEventListener('cordready', function(e) {
const config = {
createGlobals: false,
strict: false,
containers: {
main: {
disabled: false,
text: ''
}
}
};
$CORD.init(config);
});
</script>
<script src="cord.js"></script>
</body>
</html>Bind allow to link the value of a property to a container field.
Let's go now with :if statement.
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div class="main" cord-id="main">
<label><input type="checkbox" bind:checked="$self.$enabled">Enable</label>
<br/>
:if enabled :do
<input on:input="$self.$text=this.value" placeholder="You can write now..."/>
${text}
:else
You can not write...
:endif
</div>
<script>
window.addEventListener('cordready', function(e) {
const config = {
createGlobals: false,
strict: false,
containers: {
main: {
enabled: false,
text: ''
}
}
};
$CORD.init(config);
});
</script>
<script src="cord.js"></script>
</body>
</html>Of course, :else statement is optional.
Now we will talk about the :foreach statement. See this example:
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div class="main" cord-id="main">
<button
onclick="$CORD.update_object('main', 'tasks',
{action: 'push', datas: [
{text: 'Dummy task', id: Math.round(Math.random()*1000)}
]})">
Add new dummy task
</button>
<ul>
:foreach task in tasks :do
<li>
${task_i} ${task}
<button
onclick="$CORD.update_object('main', 'tasks', {action: 'remove', datas: [${task_i}]})">
Remove
</button>
</li>
:endforeach
</ul>
</div>
<script>
window.addEventListener('cordready', function(e) {
const config = {
createGlobals: false,
strict: false,
containers: {
main: {
tasks: []
}
}
};
$CORD.init(config);
});
</script>
<script src="../../cord.js"></script>
</body>
</html>Nothing unusual, just a classic foreach. Now take in account some details:
- The item variable (
taskin this case) instances every item of the array. - For every instance, exists a variable named
<item>_i(task_iin this case) that store the current index.
Also in this example you can see $CORD.update_object; more details later.
Now let play a little more with this example. What happen if you want use <table> instead of
<ul>. Well, bad news, this cannot be done with an embedded container, you need to use a template for this. Why? When a container is embebed (no load its content with a template) the browser try
to parse and render its content, and stuff like this...
...
<table>
:foreach task in tasks :do
<tr>
<td>
${task_i} ${task} <button onclick="...">Remove</button>
</td>
</tr>
:endforeach
</table>
...... will cause problems.
CORD will not be able to parse this HTML snippet because, by the time CORD comes into play, the browser has already parsed or rendered it and has likely altered its structure.
See the solution:
Filename: tasks.ctpl
<cord-template cord-tpl-id="tpl-tasks">
<button
onclick="$CORD.update_object('%cord-id%', 'tasks',
{action: 'push', datas: [
{text: 'Dummy task', id: Math.round(Math.random()*1000)}
]})">
Add new dummy task
</button>
<table>
:foreach task in tasks :do
<tr>
<td>
${task_i} ${task} <button onclick="...">Remove</button>
</td>
</tr>
:endforeach
</table>
</cord-template>Filename: index.html
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<cord-container class="main" cord-id="main" cord-tpl-ref="tpl-tasks">
</cord-container>
...
</body>
</html>Now let's play around a bit with the fields of other containers. Take a look at this example:
Filename: tasks.ctpl
<cord-template cord-tpl-id="tpl-tasks">
:if $global.main.tasks.length > 0 :do
<h5>Container showing external values from another container:</h5>
:endif
<table border="1">
:foreach task in $global.main.tasks :do
<tr>
<td>
${task_i} ${task.text} (${task.id})
</td>
</tr>
:endforeach
</table>
</cord-template>Filename: index.html
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<cord-load-templates>
./tasks.ctpl
</cord-load-templates>
<table width="100%">
<tr valign="top">
<td width="50%">
<div class="main" cord-id="main">
<button
on:click="$self.add({text: 'Dummy task', id: $self.$id++})">
Add new dummy task
</button>
<ul>
:foreach task in tasks :do
<li>
${task_i}: ${task.text} (${task.id})
<button on:click="$self.remove(${task_i})">
Remove
</button>
</li>
:endforeach
</ul>
<cord-script>
$self.add = function(v) {
$self.$tasks = $self.$tasks.concat([v]);
}
$self.remove = function(i) {
$self.tasks.remove(i);
$self.$tasks = $self.tasks;
}
</cord-script>
</div>
</td>
<td>
<cord-container cord-id="main2" cord-tpl-ref="tpl-tasks">
</cord-container>
</td>
</tr>
</table>
<cord-container cord-id="other" cord-tpl-ref="tpl-tasks">
</cord-container>
<script>
window.addEventListener('cordready', function(e) {
const config = {
createGlobals: false,
strict: false,
containers: {
main: {
tasks: []
},
main2: {
}
}
};
$CORD.init(config);
});
</script>
<script src="../../cord.js"></script>
</body>
</html>Notice how we add function to container with <cord-script> tag and using $self.<fun> = function(...){...}.
TO BE CONTINUED
Accesing fields in the bad and ugly way. $CORD.$...
Loading templates: with tags or as args passed to new CORD(...). Also dynamic load.
Dynamic containers creation...
The config is in the form:
const config = {
createGlobals: true, // create a global variable for every container
strict: true, // if true return undefined when you reference
// container field that does not exists, if false
// return an empty string.
websocket: { // If defined set the way to connecto to websocket
url: 'ws://localhost:8080/websocket', // server. With reconnect in true the ws will do
reconnect: true, // the best effort to keep the connection open.
reconnect_delay: 1000,
onmessage: <function_name> // Generic message receiver. CORD intercept msg
// and verify if action is 'cord-update' or
// 'cord-update-object' to attend CORD updates.
// If not, call this 'onmessage' function with
// the message as parameter.
// When you use $CORD.ws.send(msg, ...), if you
// set in the msg 'msg_id' parameter and set in
// second parameter a callback, the interception
// of CORD is disabled for messages that arrive
// with the same 'msg_id'.
},
eventsource: { // Same as websocket but with one direction
url: 'http://localhost:8080/eventsource', // service backed by EventSource.
reconnect: true,
reconnect_delay: 1000,
onmessage: <function_name> // Generic message receiver. CORD intercept msg
// and verify if action is 'cord-update' or
// 'cord-update-object' to attend CORD updates.
// If not, call this 'onmessage' function with
// the message as parameter.
},
containers: { // Set initials values for the fields inside
'tasks-list': { // every cord-id defined container.
tasks: [
{desc: "task 1"}
]
},
counter: {
value: 0
}
}
};
// 'cordready' event is dispached when finalize CORD bootstrap process
window.addEventListener('cordready', e => {
$CORD.init(config);
});update allow update fields values inside a container. You can use it in 2 ways:
$CORD.update('counter', 'value', 1000);
// or
$CORD.update('tasks-list', {tasks: [ {desc: 'task 1'}, {desc: 'task 2'} ]});This is a more sofisticate way to update fields value. Ex: if you do not want to rewrite all the tasks with a new array and you only pretend to add a new task, you can do:
$CORD.update_object('tasks-list', 'tasks', {action: 'push', datas: [{desc: 'task 3'}] });
// or to remove the last pushed:
$CORD.update_object('tasks-list', 'tasks', {action: 'pop', datas: [] });Actually what this function do is call for the field tasks (that is an Array) the function
action with params ...datas:
$CORD.update_object('tasks-list', 'tasks', {action: 'pop', datas: [] });
// is like
<container-tasks-list>.tasks.pop(...[])
// You could also call:
$CORD.update_object('tasks-list', 'tasks', {action: 'splice', datas: [2,1] });
// that will be like call
<container-tasks-list>.tasks.splice(...[2, 1])Well, not much to explain, just refresh (re-render) all fields of each container.
################################################################################################
Missing DOC
-
$CORD.$...
-
Websocket bidirectional communication and containers updates
-
EventSource server events to update containers
Some details to remember
-
$CORD.$. It is a reference to the container
-
$CORD.$..<field_name> It is a reference to the field; it can be update but the refresh of the container will not happen until '$CORD.update()' be called. So it is an 'update delayed'.
-
$CORD.$..$<field_name> It is a reference to the field; it can be update and the refresh of the container will happen instantly. So it is an 'instant update'.
-
$CORD.set(':', )
-
$CORD.get(':')
-
################################################################################################
- Why templates? Can I just use directly the element? It seems that not :(
- I need an evaluator, it is not enough just replace field name.
- Check if there is changes, if not do nothing (if change only attrs no update inside).
- Open websocket to enable server side change container (SSCC)
- Dynamic fetch and add templates/html from server
- EventSource support (work as websocket but no channels)
- What happend with containers inside containers?
- Foreach: add index (i variable)
- Add :if/:else statement
- Improve foreach parser to allow nested statements
- Improve if/else parser to allow nested statements
- Create a custom element cord-template to create noscript templates
- Create a custom element cord-container to invoke a cord-template
- foreach and if parser while loop make security exit (max nested depth)
- If inside foreach
- Attributes type 'disabled' or 'checked'
- tag, work on local functions and vars. Also try containers vars as local var.
- Inside a container, not needed to use $CORD.$.<cont_id>. to modify, just .
- 'get_indentifier' function is a piece of SHIT, I need to remake this function from zero.
- Throw error when cord_id does not match [a-zA-Z0-9_].
- bind:... special attr. Useful to join element property with a local var.
-
Hide noscript and if/foreach templates. - 'get_indentifier' lexer still is not good enough, it need to be more accurate.
- Think in a way to pass a list of templates to load previous to bootstrap
- cord-style tag to load css and create superclasses.
- Define format for direct server update of cord_containers