') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - hackal/Exp: Minimalistic JavaScript framework for Exponea web-layers. Powering millions of banners daily. · GitHub
Skip to content

Repository files navigation

Exp framework

Introduction

Exp framework is a lightweigth minimalistic JavaScript framework designed specifically for building banners in Exponea platform. Many architectural designs reflect this aim.

One of the main motivations is to code in a declarative manner rather than procedural. Exp framework can be thought of as an engine performing many unneccessary routines on behalf of you.

Neccesarrily to mention, we have been greatly influenced by already existing frameworks such as VueJS or AngularJS.

Installation

  1. Clone repository
  2. Install dependencies npm install
  3. Run development server npm run start
  4. Build npm run webpack

Note that running npm run start will open up server on localhost:1234, where you have interactive Exp tutorials and further explanations.

Example

The following example illustrates the power of Exp, specifically iterating over an array using exp-for.

<divid="root"><buttonexp-click="addNumber">Add number!</button><divexp-for="number in numbers"><pexp-bind="number"></p></div></div><script>varbanner=newExp({el: "#root",data: {counter: 1,numbers: []},methods: {addNumber: function(){this.numbers.push(this.counter++);}}})</script>

Documentation

JavaScript Exp constructor

The framework is initialized through the Exp object, which you have to instantiate with proper dictionary argument, describing the setting.

varbanner=newExp(settings)

The settings object has the following available attributes

KeyValueDescriptionExample
contextDictionaryThe Exponea weblayer context data that are passed from Exponea JS SDK.{ banner_id: "123" } or this when working in Exponea weblayer editor
elStringSelector which points to an element on which Exp will be initialised. Either el or html should be used."#exp-banner"
insertStringSelector which points to an element that the html code will be inserted into as a child. Default document.body."#banner-window"
dataDictionaryData initially populating the model.{ sizes: ['large', 'small'], logged_in: false }
methodsDictionaryDefinition of the user functions, that can be used in JS or triggered by HTML objects.{ onClick: () => alert("Hello, World!) }
htmlStringA custom HTML code of the weblayer. Either el or html should be used."<div class="test">aaa</div>"
styleStringA custom CSS code of the weblayer..test{color: blue;}
scopedBooleanAdding a generated hash to weblayers CSS rules, causing the CSS to affect only the weblayer. Default false.true
trackingBooleanDefault tracking of event banner when weblayer is showed. Default true.false
control_groupBooleanWeblayer will not be shown, but tracking will run as for a custom control group. Default false.false
mountedFunctionA function that is called when the weblayer is loaded.function() { setTimeout(function() { this.ref.header.style.display = “none” }, 5000) }
triggerDictionarySpecification of the trigger that activates the weblayer.{ type: "onready", delay: 1000 }
backdropDictionary or BooleanCSS style for the backdrop for the weblayer. Default false.{ background: :rgba(0, 0, 0, 0.5)" }
brandedString or BooleanAdding Exponea branding in the selected color. Default false. Available options: "black", "white" and false."white"
recommendationsDictionaryDefinition of recommendation models used with the banner.{ rcm1: {id: "123", total: 4} }
sentryDictionaryOptions settings for Sentry integration{use: true, project: ''}
formattersDictionaryDictionary of formatters to apply for outputting binded values in view{ uppercase: (v) => v.toUpperCase() }

HTML Exp attributes

Exp framework is able to recognize the following set of HTML attributes and add corresponding logic.

AttributeValueDescriptionExample
exp-bindVariableUpdating text real-time with the value of JavaScript variable (i.e. JavaScript -> HTML binding).<span exp-bind="product.name"></span>
exp-htmlVariableUpdating html real-time with the value of JavaScript variable (i.e. JavaScript -> HTML binding).<span exp-html="product.name"></span>
exp-modelVariableUpdates the JavaScript variable value with the input value (i.e. HTML -> JavaScript binding).<input exp-model="email">
exp-ifVariableShowing element if the value of the variable is true.<div exp-if="bonus_club">Discount 50%</div>
exp-forIterationCopying HTML elements for every element in array and populating it with corresponding values.<div exp-for="product in products"><span exp-bind="product.title"></span></div>
exp-rcmIterationCopying HTML elements for every element in array returned by Exponea recommendation and populating it with corresponding values.<div exp-rcm="item in rcm1"><span exp-bind="item.name"></span></div>
exp-clickMethodCalling a custom method when clicked.<button exp-click="submit_form">
exp-srcVariableThe img object will get a source given by the variable value.<img exp-src="product.img">
exp-actionN/AClicking will cause a default banner event with click action.<div class="exponea-button" exp-action><a href="http://exponea.com/careers">Join us!</a></div>
exp-closeN/AClicking will remove banner.<button class="exponea-close-button" exp-close></button>

Note that there are further special attributes like exp-click and exp-src, those are exp-href and exp-alt which update HTML attributes and exp-submit, exp-input, exp-hover, exp-blur, exp-focus, exp-mouseenter and exp-mouseleave which add listeners to specific DOM events. Both of these are described in sections bellow.

Guides

Initialisation

There three modes in which you can initialise Exp.

  1. Using already existing DOM element on webpage Using value of el attribute (in the Exp constructor options), Exp selects element in the DOM, and initialises Exp over it. That is useful, when the website itself uses Exp and the webpage already contains Exp HTML code. For example, var banner = Exp({el: "#exp-banner"}).
  2. Inserting your custom HTML code Using value of html attribute, which contains HTML code as string, it creates the weblayer and inserts it into the DOM element specified through the insert attribute. If insert is not specified, it adds it to document.body by default. For example, var banner = Exp({insert: "#exp-banner", html: "<p>Hello, World!</p>"}) or just var banner = Exp({html: "<p>Hello, World!</p>"}).
  3. Exponea weblayer editor This mode utilises the functionality of Exponea JS SDK. You must pass the Exp constructor the context attribute. In the Exponea app, you will usually initalise Exp object as var banner = Exp({ context: this }), because this contains lots of banner information injected by the Exponea JS SDK (for instance it contains HTML and CSS codes). Hence, in case of using context you do not need to pass html or style attributes, because this contains HTML and CSS codes from the Exponea weblayer editor. In case you do include html, html will by default override the HTML code from context.

Model and data binding

The most important concept of Exp is of model. That can be thought of as a store, containing all the variables and data required by the weblayer. Those can be constants, functions, arrays.

You initialise model with the data attribute, for instance, var banner = Exp({ data: { foo: "bar" }}). Firstly, Exp creates a variable in the model called foo and populates it with value "bar". After the weblayer is rendered, Exp updates the model whenever user performs an action through a binded method. Also conversely, Exp uses the values from the model to update the view. This design is the Model-View-Controller (MVC) paradigm.

Having that said, Exp is capable of two-way binding between HTML and JavaScript. We can think of the two bindings as Model-View and Controller-Model bindings.

Model-View binding is from JavaScript to HTML. Any changes in the JavaScript model can get reflected on the view. You must use the exp-bind attribute in HTML element. For instance, <p exp-bind="foo"></p> inserts and updates the text in the element with the current value of foo variable in the model, hence after initialization it will be "bar".

Controller-Model binding is from HTML to JavaScript. The JavaScript model can get updated from HTML events using the exp-model HTML attribute. For instance, <input exp-model="foo" type="text" /> will update the variable foo in the model with the value inserted into the input element by the user.

Methods

In the methods scope, you can access the model via this object. Exp calls methods with the scope of the model. To illustrate the example, suppose we have a banner which shows an incrementing counter.

<divid="banner"><pexp-bind="counter"></p><buttonexp-click="increment">Increment</bind><buttonexp-click="incrementByTwo">Increment By 2</bind></div><script>varbanner=newExp({el: "#banner",data: {counter: 0},methods: {increment: function(){this.counter++;},incrementByTwo: function(){this.increment();this.increment();}}})</script>

Clearly, we can access the model variable counter from within the method. The same applies for methods, as illustrated in the incrementByTwo method. Exp adds both variables and methods into the model.

Iterations

Exp makes iterating over arrays extremely easy and efficient. When you have an array declared in the model, you can iterate over it in the following fashion.

<divid="banner"><divexp-for="number in numbers"><pexp-bind="number"></p></div><divexp-for="person in people"><pexp-bind="person.age"></p><pexp-bind="person.name"></p></div></div><script>varbanner=newExp({el: "#banner",data: {numbers: [1,2,3],people: [{age: 21,name: "Lukas"},{age: 23,name: "Adam"}]}})</script>

Notice that numbers array contains primitive values, so we can later access it just by relative variable number. On the other hand, people is an array of objects, so we have to access nested properties within exp-for as person.age. There is no limit on the depth of the nesting. Also, beware of how Exp renders the exp-for elements. The following shall be generated for the snippet above,

<divid="banner"><div><pexp-bind="number">1</p></div><div><pexp-bind="number">2</p></div><div><pexp-bind="number">3</p></div><div><pexp-bind="person.age">21</p><pexp-bind="person.name">Lukas</p></div><div><pexp-bind="person.age">23</p><pexp-bind="person.name">Adam</p></div></div>

Conditionals

Suppose you want to hide some DOM elements in case of some specific event. You can use the exp-if functionality. In the attribute of the HTML element specify the boolean variable in model or method returning a boolean, subject to which the DOM element will (dis)appear. For example,

<div><spanexp-if="booleanVariable">Variable is set to true</span><spanexp-if="booleanMethod">Method is returning true</span></div><script>varbanner3=newExp({context: this,data: {booleanVariable: false},methods: {booleanMethod: ()=>{constx=Math.random();if(x<0.5){/* do stuff */returntrue;}else{/* do stuff */returnfalse;}}}})</script>

This will show text Variable is set to true only when booleanVariable variable is set to true. And it will show Method is returning true only when booleanMethod returns true (50 percent of time in our example).

Note that if exp-if is binded to a boolean method (like booleanMethod in our example is), then the method is only evaluted once at initialisation and does not get called on model changes.

HTML element attributes

Currently we support only updating src, href and alt HTML attributes. Suppose you want to update one of these, lets say href. Then in your target HTML element create exp-href attribute and bind it to any value, as you do normally with exp-bind. For example,

<imgexp-href="product.url" />

Exp then creates a new attribute href and pastes in the binded JavaScript value. It also updates the href attribute whenever the variable in the model changes. So, after initialisation it can look like this

<imgexp-href="product.url" href="www.example.com/product1" />

Listeners to DOM events

Currently we support only listening to click, submit, input, hover, blur, focus, mouseenter and mouseleave DOM events. If you want to bind some function to this event, suppose you want to add listener to submit, then create exp-submit attribute in the target element. For example

<formexp-submit="submitForm"></form>

In the methods section of Exp constructor settings create a function under the key submitForm. This function can take one argument (event) which is the same as normal JavaScript listeners do. So, your Exp initalisation can look like

varbanner=newExp({methods: {submitForm: function(e){/* do stuff */}}})

This will create listener on the form element on submit event and call submitForm function with the event as an argument.

Triggers

With Exp, it became very easy to setup specific predefined scenarios which trigger the weblayer to render. Currently we support onready, onexit and onaction triggers, where each must be declared with different parameters. Lets go through all of them.

onready trigger

This renders the weblayer once the webpage is fully loaded. You can initalise it in the following manner

varbanner=newExp({trigger: {type: "onready",/* Required. */delay: 5000/* Optional, default 0. Specifies the miliseconds of delay. */}})

onexit trigger

This renders when user moves his coursor to the top of the browser. You can initalise it in the following manner

varbanner=newExp({trigger: {type: "onexit",/* Required. */delay: 5000/* Optional, default 0. Specifies the miliseconds of delay. */}})

onaction trigger

This renders when a specific action listener is triggered. You can initalise it in the following manner

varbanner=newExp({trigger: {type: "onaction",/* Required. */element: document.querySelector("#button"),/* Required, the element which the listener will be inserted to. */action: "click",/* Required, the DOM event which listener is listening to. */delay: 5000/* Optional, default 0. Specifies the miliseconds of delay. */}})

Recommendations

Exp includes a utility for working with Exponea recommendations. You must use the context option in the constructor to pass the Exponea JS SDK. Afterwards, use the exp-rcm directive, which works the same way as exp-for but it does a little more work in the background to make our lives easier. Below is an example of using exp-rcm for rendering 4 products with their image, name, price and link in a table:

<div><table><tr><th>Name</th><th>Price</th><th>Link</th><th>Image</th></tr><trexp-rcm="product in products"><tdexp-bind="product.name"></td><tdexp-bind="product.price"></td><td><aexp-href="product.link" target="_blank">link</a></td><td><imgexp-src="product.image" width="100"></td></tr></table></div><script>varbanner=newExp({context: this,recommendations: {products: {id: "recommendationId",/* Required. Valid Exponea recommendation ID */total: 4,/* Required. The total amount of recommended items to be displayed */loadingKey: "foo"/* Optional, default undefined. Name of the model boolean variable, which is set to true once recommendation is loaded */}}})</script>

Note that the recommendationId must be a valid recommendation ID generated by Exponea. Example above depicts all the available options.

Event tracking

Events can be tracked through the Exponea JS SDK only when you use Exp from Exponea weblayer editor and instantiate the banner object with the context: this attribute. Later, there are two ways how you can track events to Exponea.

  1. Setup default tracking through settings tracking: true When the banner is displayed to the user it will automatically track banner event with action: show and attributes including other banner specific properties. Similarly, when a user closes a banner through a DOM element with exp-close attribute, a banner event with action: close is automatically tracked.

It is possible to disable automatic tracking of the banner by setting tracking: false in the Exp constructor settings. By default it is set to true. 2. Custom tracking through Exponea JS SDK Users can track from within methods in model by calling this.sdk.track("eventName", properties). For example,

varbanner=Exp({context: this,methods: {onClick: function(){this.sdk.track("cart_update",{})}}})

Formatters

Exp allows you to define formatters that can be used to apply common text formatting. Formatters are usable only with exp-bind directive. Formatters should be appended to the end of the expression denoted by the "pipe" symbol |. An example can look like this,

<pexp-bind="greetings | trim | uppercase"></p><script>varbanner=newExp({context: this,data: {message: 'hi lukas '},formatters: {uppercase: function(str){returnstr.toUpperCase();},trim: function(str){returnstr.trim();}},branded: false})</script>

Exp goes through all the formatters from left-to-right and applies the transformations on the string, so that the following will be outputed:

<pexp-bind="greetings | trim | uppercase">HI LUKAS</p>

Sentry

Exp is fully integrated with Sentry.io. Sentry is an open-source error tracking tool that helps developers monitor and fix crashes in real-time. By default Sentry is turned off. Sentry can be enabled on Exp instance in the following way,

varbanner=newExp({context: this,sentry: {use: true,/* Enables Sentry usage. Default false */noConflict: true,/* Creates independent Raven instance from potentially others on page. Default true */project: '',/* Endpoint for Sentry URL. Default '' */options: {/* Options for Sentry tracking, can be used for tagging for example. Default {} */tags: {project_id: "exp-test",instance: "exp"}}}})

By default Exp will make embed Raven JavaScript SDK from Sentry CDN. It will create new instance of Raven object per each weblayer.

noConflict mode can be set to false to use Raven integration already preconfigured on the website. One downside of disabling noConflict mode is that we will lose the ability to correctly identify which weblayers are causing errors. It is recommended to always use noConflict: true setting.

If context: this is set from Exponea weblayer editor, Exp will automatically tag the Sentry events with weblayer ID, weblayer name and project token.

About

Minimalistic JavaScript framework for Exponea web-layers. Powering millions of banners daily.

Resources

Stars

5 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages