Skip to content
This repository has been archived by the owner on Feb 21, 2020. It is now read-only.
/ brackets Public archive

[Discontinued]πŸ”₯ Small, flexible, easy to use, component-oriented javascript template engine.

License

Notifications You must be signed in to change notification settings

apicart/brackets

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

  • Small, flexible, easy to use, component-oriented javascript template engine.
  • βœ… 15 Kb minified (5 Kb Gzipped)
  • βœ… Supports IE 10 +
  • βœ… TRY IT ON CODEPEN

Content

Installation

Brackets are under development and therefore they are not yet available on npm. You can use the cdn link.

CDN

<!-- Master version from Github -->
<script src="https://cdn.jsdelivr.net/gh/apicart/brackets/dist/brackets.min.js"></script>

<!-- v1.0.0-alpha1 from jsdelivr.net -->
<script src="https://cdn.jsdelivr.net/npm/@apicart/brackets@1.0.0-alpha2/dist/brackets.min.js" integrity="sha256-TxTeKLp4t4vZVi131XgcBwX9LJfTg1N9zlMxPE2XE0o=" crossorigin="anonymous"></script>

Npm & Yarn

npm install @apicart/brackets

yarn add @apicart/brackets

Getting Started

Let's start with a simple example. We will render a text stored in the data object into the #app element. Notice that variables have dollar sign $ before the name.

<div id="app">
	{{$text}}
</div>
<script>
Brackets.render({
	el: '#app',
	data: {
		text: "I ❀️ Brackets!"
	}
});
</script>
I ❀️ Brackets!

You can also store the rendered view into a variable and change the data from there. The result will be the same. In case you use class selector and more views are rendered, you will get an array from the render function over which you can iterate.

<div id="app">
	{{$text}}
</div>
<script>
var appView = Brackets.render({
	el: '#app',
	data: {
		text: ""
	}
});

appView[0].data.text = 'I ❀️ Brackets!'
</script>

Cache

Cache speed's up the rendering process. There are two types of cache. Functions cache and results cache.

  • Results cache: Caches the whole content of the rendered instance. This cache is good for templates where the variables are not changed. You can enable it by setting the resultCacheEnabled to true.
  • Functions cache: Caches only the generated template function not the result of the rendering process. Is good for templates that are used multiple times and its variables needs to be changed dynamically. You can enable it by adding the template cacheKey name.
<div id="app">
	{{$text}}
</div>
<script>
Brackets.render({
	el: '#app',
	cacheKey: 'test',
	resultCacheEnabled: true,
	data: {
		text: "I ❀️ Brackets!"
	}
});
</script>

Templates

The template you want to render can be provided in a multiple ways. In the example above, the template was loaded from the target element #app.

Another way to provide the template is setting it directly as a text in the template parameter.

<div id="app"></div>
<script>
Brackets.render({
	el: '#app',
	cacheKey: 'test',
	data: {
		text: "I ❀️ Brackets!"
	},
	template: '{{$text}}'
});
</script>

Template parameter can also receive an id selector #elementId. If so, the template will be loaded from the given element (you shouldn't load complicated templates from a typical html elements because it can cause unexpected errors, we recommend to use <template>...</template> or <script type="text/plain">...</script> elements as template providers).

<div id="app"></div>
<template id="template">
	{{$text}}
</template>
<script>
Brackets.render({
	el: '#app',
	cacheKey: 'test',
	data: {
		text: "I ❀️ Brackets!"
	},
	template: '#template'
});
</script>

In case you need some condition for providing a correct template (for example for A/B testing) you can use a function that returns the right template based on given conditions. This function is called only once and can return a template string or a selector as in examples above. You should not change any parameters or data inside this function, because the code can become unclear.

<div id="app"></div>
<template id="templateA">Template - A</template>
<template id="templateB">Template - B</template>
<script>
Brackets.render({
	el: '#app',
	cacheKey: 'test',
	data: {
		version: 'a'
	},
	template: function () {
		return this.data.version === 'a' ? '#templateA' : '#templateB';
	}
});
</script>

Data - watch hook

Sometime it can be usefull to do something, when a value of a property in a data object changes. For that, you can use the watch property into which you can add a property with the same name like the property you want to watch.

<button b-on="click number++" class="app">{{$number}}</button>
<script>
	Brackets.render({
		el: '.app',
		data: {
			number: 0
		},
		watch: {
			number: function (newValue, oldValue) {
				console.log(newValue, oldValue);
				console.log(this) // Rendering instance
			}
		}
	});
</script>

Events

During the whole rendering process, there are triggered two events.

  • Before render (beforeRender) - this event is triggered before the whole rendering process starts
  • After render (afterRender) - this event is triggered after the rendering process is complete

Both events triggers methods with the configuration object provided as this.

<div id="app">
	{{$number}}
</div>
<script>
Brackets.render({
	el: '#app',
	data: {
		number: 1
	},
	beforeCreate: function () {
		this.data.number ++;
	},
	mounted: function () {
		alert("Generated number is " + this.data.number);
	}
});
</script>

Event Handlers

Every website needs some interactivity. For example after clicking on a button. Every element that should be interactive must have the b-on="" attribute. There you can set the target event and what should happen when is triggered. The syntax is following b-on="<event name> <callback>; <event name> <triggered callback>, ...". The callback can have two forms. Direct functionality, where the function is connected to the data object like b-on="click number++ or an independent function b-on="click updateNumber()". If you want the callback to be a function, you can provide arguments. Those arguments are passed into the target function and are always a string so you have to convert it if you want to get for example a number from it. b-on="click showAlert(Some text)"

<div id="app">
	{{$number}}<br>
	<button b-on="click showAlert(Clicked 1!); click number ++">{{$firstButtonText}}</button><br>
	<button b-on="click secondButtonText = 'Clicked 2!'; click showAlert()">{{$secondButtonText}}</button>
</div>
<script>
Brackets.render({
	el: '#app',
	data: {
		number: 0,
		firstButtonText: 'Click me 1!',
		secondButtonText: 'Click me 2!'
	},
	methods: {
		showAlert: function (event, parameters) {
			if (parameters) {
				this.data.firstButtonText = parameters;
			}
			alert('Hello World!');
		}
	}
});
</script>

Filters

Filters are used for interaction with values from variables. As an example, we will create a filter called firstToUpper and it will convert the first character to a capital letter.

<div id="app">
	{{$text|firstToUpper}}
</div>
<script>
Brackets
	.addFilter('firstToUpper', function (text) {
		return text.charAt(0).toUpperCase() + text.slice(1);
	})
	.render({
		el: '#app',
		data: {
			text: 'text'
		}
	});
</script>
Text

Filters can receive multiple arguments. The arguments must be added after the colon and must be separated by a comma. The example below returns the default first text and attaches the text 'second' and 'third'.

<div id="app">
	{{$text|appendWords: 'second', 'third'}}
</div>
<script>
Brackets
	.addFilter('appendWords', function (text, firstParameter, secondParameter) {
		return text + ', ' + firstParameter + ', ' + secondParameter
	})
	.render({
		el: '#app',
		data: {
			text: 'First'
		}
	});
</script>
First, second, third

Macros

There are the following macros defined by default.

Conditions

{{if $cond}} … {{elseif $cond}} … {{else}} … {{/if}} If condition

Loops

{{for condition} … {{/for}} For loop
{{foreach values as key, value}} … {{/foreach}} Foreach loop, the this object is an iterator object with iterableLength and counter parameters and isFirst, isLast, isOddd and isEvent functions.
{{while condition}} … {{/while}} While loop
{{continue}} Jump to the next iteration
{{continueIf condition}} Conditional jump to the next iteration
{{break}} Loop break
{{breakIf condition}} Conditional loop break
{{sep}}...{{/sep}} Alias to if ( ! this.isLast() (this = iterator object)
{{last}}...{{/last}} Alias to if (this.isLast() (this = iterator object)
{{first}}...{{/last}} Alias to if (this.isFirst()(this = iterator object)
<div id="app">
	{{foreach letters as key,letter}}
		{{first}}<strong>{{/first}}
		{{last}}<em>{{/last}}

		{{$key}} => {{$letter}}

		{{last}}</em>{{/last}}
		{{first}}</strong>{{/first}}

		{{sep}}<br>{{/sep}}
	{{/foreach}}
</div>
<script>
	Brackets.render({
		el: '#app',
		data: {
			letters: ['a', 'b', 'c']
		}
	})
</script>
<strong>0 => a</strong>
<br>
1 => b
<br>
<em>2 => c</em>

Variables

{{var foo = value}} Creates variable
{{capture myVariable}}...{{/capture}} Captures output created between blocks into the defined variable
<div id="app"></div>
<script type="text/plain" id="app-template">
	{{capture numbers}}
		{{for i = 0; i < 3; i++}}
			{{$i}}
		{{/for}}
	{{/capture}}

	First line: {{$numbers}}
	<br>
	Second line: {{$numbers}}
</script>
<script>
	Brackets.render({
		el: '#app',
		template: '#app-template'
	})
</script>
First line: 0 1 2 
Second line: 0 1 2

Other

{{dump variable}} Similar to console.log()
{{js code}} Allows you to write pure javascript
{{component name, param1: value, parameter2: value}} Allows you to reuse components

How to create a macro

Macro in the context of the template engine is a piece of executable code.

First we will create a simple macro that will execute alert function. The macro name will be alert and number its parameter. Macro is separated into two parts {{<name> <parameters>}}. #0 is a placeholder on which place the <parameters> will be placed. In the following case, the #0 will be replaced by 1.

<div id="app">
	{{alert number}}
</div>
<script>
Brackets
	.addMacro('alert', 'alert(#0);')
	.render({
		el: '#app',
		data: {
			number: 1
		}
	});
</script>

Macro can also be a function. In another example, we will use the _template variable available during the rendering. The _template is used to return the generated template. We will used it, because we want to return a content from our macro.

The code during the compilation is separated by Template literals or by single quotes (depends on browser support). The correct separator is stored in the Brackets.templateLiteral variable and you should use it to prevent incompatibility with older browsers.

On the end of the macro, there is a semicolon. In case you do not provide it the compilation will end with an error.

<div id="app">
	{{dumpNumber number}}
</div>
<script>
var sep = Brackets.templateLiteral;
Brackets
	.addMacro('dumpNumber', function () {
		return '_template +=' + sep + 'Number: ' +  sep + ' + number;'
	})
	.render({
		el: '#app',
		data: {
			number: 1
		}
	});
</script>
Number: 1

It is also possible to use _templateAdd function. This function automatically applies the escaping filter.

<div id="app">
	{{dumpNumber number}}
</div>
<script>
var sep = Brackets.templateLiteral;
Brackets
	.addMacro('dumpNumber', function () {
		return '_template += _templateAdd(' + sep + 'Number: ' +  sep + ' + number);';
	})
	.render({
		el: '#app',
		data: {
			number: 1
		}
	});
</script>

The _templateAdd function also allows you to use your already defined filters. Just add it as a second parameter.

<div id="app">
	{{dumpText text|firstToUpper}}
</div>
<script>
Brackets
	.addFilter('firstToUpper', function (text) {
		return text.charAt(0).toUpperCase() + text.slice(1);
	})
	.addMacro('dumpText', function () {
		return '_template += _templateAdd(text, \'firstToUpper\');';
	})
	.render({
		el: '#app',
		data: {
			text: 'text!'
		}
	});
</script>

Complete Rendering Configuration

Brackets.render({
	addData: <function|null>,
	cacheKey: <string|null>,
	data: <object|null>,
	el: <string|Element|NodeList|function>,
	instanceId <string|null>,
	methods: <object|null>,
	onStatusChange: <function|null>,
	resultCacheEnabled: <function|null>,
	template: <string|null>,

	// Lifecycle hooks
	beforeCreate: <function|null>,
	created: <function|null>,
	beforeMount: <function|null>,
	mounted: <function|null>,
	beforeUpdate: <function|null>,
	updated: <function|null>,
	beforeDestroy: <function|null>,
	destroyed: <function|null>
})

Components

Components helps to create your code more reusable. You can for example create a button with some functionality and use it on multiple places with different parameters.

In the first example, there is a component that returns a text. The text is different in each .app element. Components can receive arguments. Arguments are placed behind a comma (,) and are also separated by a comma (,).

<div class="app">{{component text}}</div>
<div class="app">{{component text, text: 'Second app'}}</div>
<div class="app">{{component text, text: 'Third app'}}</div>
<script>
Brackets
	.addComponent('text', {
		data: {
			text: 'First app'
		},
		template: '{{$text}}'
	})
	.render({
		el: '.app',
		data: {
			text: 'First'
		}
	});
</script>

Now, let's take a look on nested components. If the component is nested inside another component, then it's parent component must have some root element in which the component is placed. The root element is not necessary for a plain text.

<div class="app">{{component shareArticle, articleName: 'Article 1'}}</div>
<div class="app">{{component shareArticle, articleName: 'Article 2'}}</div>
<div class="app">{{component shareArticle, articleName: 'Article 3'}}</div>
<script>
Brackets
	.addComponent('shareButton', {
		instanceId: 'shareButton',
		data: {
			number: 0
		},
		methods: {
			updateNumber: function () {
				this.data.number ++;
			}
		},
		template: '<button b-on="click updateNumber()">Share ({{$number}})</button>'
	})
	.addComponent('shareArticle', {
		instanceId: 'shareArticle',
		template: '<div>{{$articleName}} => {{component shareButton}}</div>'
	})
	.render({
		instanceId: 'app',
		el: '.app'
	});
</script>

Complete Components Configuration

Brackets.addComponent({
	addData: <function|null>,
	cacheKey: <string|null>,
	data: <object|null>,
	instanceId <string|null>,
	methods: <object|null>,
	onStatusChange: <function|null>,
	resultCacheEnabled: <function|null>,
	template: <string>,

	// Lifecycle hooks
	beforeCreate: <function|null>,
	created: <function|null>,
	beforeMount: <function|null>,
	mounted: <function|null>,
	beforeUpdate: <function|null>,
	updated: <function|null>,
	beforeDestroy: <function|null>,
	destroyed: <function|null>
})

Rendering Instances

Rendering instances are interactive objects that were used during the rendering process of each template or component. Each rendering instance have an id. Because there can be multiple instances during the rendering process, you can set instanceId parameter. This parameter will be than used as a prefix for the instance so the instance id will be <your-id>-<unique hash>. This will help you to find the instance you want to work with.

The following example shows how to work with instances.

Brackets.findRenderingInstances('my-instance') // Finds all rendering instances that matches id. Returns object
Brackets.getRenderingInstances() // Returns an object containing all rendering instances

var myInstance = Brackets.findRenderingInstance('my-instance') // Finds only one instance that matches id.
var myInstance = Brackets.getRenderingInstance('my-instance-1234') // Returns the selected instance

myInstance.data.number += 2 // Changing data structure in the renderingInstance will trigger the selected instance redrawal
myInstance.addData('key', 'value'); // This will add new data by key into the data object and makes it reactive

Rendering instances lifecycle hooks

There are multiple lifecycle hooks available:

  • beforeCreate - Triggers before an instance is created and stored into a register. The data object is reactive. Instance is not attached to DOM yet and cannot be redrawed.
  • created - Similar to the beforeCreate hook except the instance is already registered in register.
  • beforeMount - Before the instance is attached to DOM. Triggers only during first rendering.
  • mounted - Right after the instance is attached to DOM. Triggers only during first rendering. Since now, the instance will be redrawed if the data changes.
  • beforeUpdate - When the instance was already mounted and is going to be redrawed.
  • updated - Right after the instance is redrawed.
  • beforeDestroy - Before the instance is destroyed.
  • destroyed - Right after the instance is destroyed.
Brackets.render({
	beforeCreate: function () {},
	created: function () {},
	beforeMount: function () {},
	mounted: function () {},
	beforeUpdate: function () {},
	updated: function () {},
	beforeDestroy: function () {},
	destroyed: function () {}
});

Security

Every variable passed into the template is autoescaped! If you want to disable the autoescaping for your variable add the noescape filter.

{{$variable|noescape}}