Cahir is a function proxy that allows you interchangibly use tagged templates and method calls. You can use it as an imperative framework with declerative bits. You define the routines and you define the shortcuts.
Cahir is an imperative library, but still if you are wondering, this is how it works:
The characters you use for method calls are completely customizible:
__init__: Cahir.tagify({
strTransform: str => str
.trim()
.replace(/^\/>\s*/,"")
.replace(/^\|>/, "pipe")
...
.replace(/^👊/gi, "runtime")
.replace(/^👈/gi, "appendTo")
Since Ch
is a Proxy
, you get to define how it reacts to keys that it does not have:
ch.div //logs a div DOM object
ch.li //logs a lig object
Can even do:
ch[`li{
"attr":[["data-x", ${15}], ["data-y", ${0}]],
"prop":[["a",3],["b",2],["innerHTML", ${
`"<span>Hello World!</span>"`
}]]
}`] //logs a li with data-x, data-y attributes with span as child
- Create reusable
tagged template
s viach.pickle
- Allow arbitrary method names to be intercepted
- Use tagged templates to their full potential by allowing marking/spreading
value
s like namespaced variables - Interchange between tagged templates and method calls:
`method1 ${arg1}`.method2(arg2)`method3 arg3 ...${rest}`
- Create shortcuts to your methods and use them as operators like
->
,|>
,+->
in your templates. You can use any character as you want
include the base script and your custom method collections (or the ones included in the collections
folder of the repo):
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cahir.0.0.6.evergreen.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/collections/DOM/ch.js"></script>
npm i cahir
pnpm add cahir
//prepare your function
const ch = new Cahir({
__init__: Cahir.tagify({
strTransform: str => str
.trim()
.replace(/^\+\s*/,"add")
.replace(/^x\s*/, "multiply")
.replace(/^\|>/, "pipe")
})(function (...args) {
if (args.length <= 1) {
this.currentNumber = args[0];
return this;
}
return this[args[0]](...args.slice(1))
}),
add: function(a) {
return this(this.currentNumber + a)
},
multiply: function(a) {
return this(this.currentNumber * a)
},
pipe: function(command, ...args) {
this[command].apply(this, [...args]);
return this;
}
})
//usage
ch(1)`
|> add two:${2}
x ${({values}) => values.two}
+ ${-10}
`
Logging ch.curentNumber
will result in -4
. You can save the template and run it later with other parameters:
const pickle = ch.pickle`
|> add two:${2}
x ${({values}) => values.two}
+ ${-10}
`
ch(5)(pickle); //ch2.currentNumber === 4
ch(7)(pickle); //ch2.currentNumber === 8
- Start with a base function, this will be your primary method. Below function sets an arbitrary property
selected
on itself if given a single argument, otherwise do a method call:
function base (...args) {
if (args.length <= 1) {
this.selected = args[0];
return this;
}
return this[args[0]](...args.slice(1))
}
- Use the static
tagify
property onCahir
to convert the function for dual use (regular method calls + tagged templates)
const
tagger = Cahir.tagify(),
taggified = tagger(base);
- Provide the resulting function inside a configuration object with
__init__
key toCahir
const ch = new Cahir({
__init__: taggified,
method1: function(){...}
method2: function(){...}
...
})
- Now you can interchange between tagged templates and normal method calls:
ch(some_var).method1()`
method1 ${arg1}
method2 string-arg1 ${arg2}
`method2(arg1, arg2)(some_other_var)`
...
`
- You can use spread sytax within tagged templates:
ch`
method ...${some_array}}
...
`
- You can mark values within literals to be used later:
ch`
method my_field:${some_variable}
method2 string-arg1 ${({values}) => /*do something with values['my_field']*/}
`
Values in literals can be anything, with a special case for function
s. If you pass a function
object, it will be given the arguments:
{
thisArg, // Proxy: the proxy `ch` object
self, //Any: current value in literal
index, //Number: current index in literal in order of appereance
values, //Array: all the values in the literal
strings, //Array: all the strings in the liteal
stringsTransformed //Array: all the strings after transformations applied
}
- If you want to pass a
function
to a method, use afunction
that returns afunction
:
ch(...)`
method1 ${({thisArg, values, ...}) => () => /*this function is passed to method1*/}
`
If you want to label a function
, use an object:
ch`
some_method some_label: ${await import("/path/to/some_script.js")}
`
ch`
method1 comparer:${{ en: new Intl.Collator("en").compare }}
method2 ${({values}) => values.comparer.en(a, b)}
...
`
Cahir.tagify
static method accepts optional arguments:
delim
(string): used for splitting strings to seprate methods and string argumentsstrTransform
(function): transform the literal strings before evaluation during parsingvalTransform
(function): transform the literal values before evaluation during parsing
Default values are reasonable, using a custom strTransform
allows you to define method shortcuts:
const ch = new Cahir({
__init__: Cahir.tagify({
strTransform: str => str
.trim()
.replace(/^->/gi,"method1")
.replace(/^=>/,"method2")
.replace(/^\|>/, "method_pipe")
})(function (...args) {
//your function
}),
method1: function(...args) {
...
},
method2: function(...args) {
...
},
...
then:
ch`
-> ${arg1}
=> second:${arg2} |> method1 ...${[arg3, arg4]}
=> ${({values}) => ++values.second}
...
`
Cahir
accepts__intercepApply__
and__interceptGet__
properties on the configuration object. These 2 functions are called with:
- the
ch
proxy asthis
next
to be called with no arguments, if you do NOT want to interceptprop
which is the requested propertyreceiver
, same as the get handler for Proxies
const ch = new Cahir({
__init__: taggified,
method: function(){...}
__interceptGet__: function (next, prop, receiver) {
switch (true) {
case (!(prop in this)):
return this.method.bind(this, prop)
}
next();
}
})
In above, any method that is not defined on ch
will call method
with prop
. So ch.some_prop_string(arg1)
would be the same as:
ch.method("some_prop_string", arg1)
Inspect app code on card game example where each game card is a webcomponent:
ch`
<game-card ${{ data: {values, d} }}/>
+< ${values.cont}`.selected
Above creates a web component <game-card/>
, appends it to values.cont
and returns the component (via .selected
). The string <game-card...
is converted to a web component using strTransform
at ch:
...
strTransform: str => str
.trim()
.replace(/^<((?:[a-z]+-+[a-z]*)+)/,"wc $1")
...
Above calls wc
(webcomponent) method on ch, which in turn calls game-card
method that is adopt
ed earlier:
adopt ...${[
"game-card",
(await import(
"./component-game-card.js"
)).render
]}
component-game-card passes the user defined data to other downstream functions such as render-card
When creating webcomponents, you can provide several properties
ch`<game-card ${{
attrs = [["data-x", "y"], ...],
styles = [["width", "auto"], ...],
props = [["x", x], ["y", y], ...],
data = "Any arbitrary value",
innerHTML = Function|String,
select = truthy|falsey
}}/>`
All these are passed to method game-card
or whatever name you assigned during adopting.
wc
implementation of ch
uses Symbol
s to detect and call connectedCallback
only ONCE. To detect multiple additions/removals from DOM, you can use custom functions or MutationObserver
. Or you can define your custom wc
.
Card data for the website was taken from gwent.one.
You can help documenting different Ch
implementations under collections using JSDoc
syntax
Reasonable PRs are always welcome:
For Cahir
itself:
- state the bug/feature clearly
- state what the proposition does
- submit few test cases
For method collections:
- state what the collection is aimed at (DOM manipulation, data visualization, calculations etc.)
- explain method briefly in
JSDoc
syntax - include examples if possible