Coder Social home page Coder Social logo

hashspace's Introduction

Aria Templates - JavaScript Framework

ci codecov Dependencies devDependencies

npm

Aria Templates (aka AT) is an application framework written in JavaScript for building rich and large-scaled enterprise web applications. Developed since 2009 by Amadeus for its professional products, it has been designed to build web apps used intensively that need to display and process a high amount of data with a minimum of bandwidth consumption.

Some details

Aria Templates in a nutshell:

  • MVC based framework
  • powerful templating engine
  • data binding and automatic refresh
  • widgets libraries
  • lots of utilities libraries

The MVC's terminology in AT:

  • model -> JSON-based data model stored in DOM nodes corresponding to templates
  • view -> template (.tpl file)
  • controller -> module controllers and template scripts (.js files)

Templates can either be evaluated client-side or precompiled at build time.

Getting started

To get started with Aria Templates, you have several options:

  • you can clone our Git repository / download a ZIP from GitHub and then include bootstrap.js file in your HTML page, to run the original, development source code,
  • after obtaining our source code, you may run Grunt build and then include a packaged, minified (much faster) version of the framework in your page,
  • or to use the framework in your NodeJS application, issue npm install ariatemplates in the command line, then call require('ariatemplates') from your code to load the framework.

Head to README.md files in src and build folders to read more.

License

Apache License 2.0

Browser support

  • Firefox latest
  • Chrome latest
  • Edge latest

For accessibility, we support the combination of Edge (latest) with Jaws 2021.

Dependencies

The framework itself doesn't have any external dependencies.

We use Grunt, JSHint, UglifyJS, attester and a couple of other tools for building and testing.

Tools & apps

Syntax highlighters:

Other tools:

Feel invited to contribute highlighters for editor of your choice, or other useful tools!

Testing

  • Attester is the tool we use for running Aria Templates tests. You may also use it for running tests of your project.
  • Aria Templates TDD guide can help you writing tests for AT widgets and templates

Releases & backward compatibility

We release a new minor version (1.3.5, 1.3.6, ...) every 3 weeks, containing new features and bugfixes. Each version is thoroughly tested before the release. These releases are backward compatible. Occasionally we also backport important fixes to some of the older releases (1.3.1A, 1.3.1B etc.) - see tags.

Twice or three times a year, we issue a non-backward-compatible release, bump the the second digit (e.g. 1.3.x -> 1.4.1) and provide migration guide.

Before removal, items are marked as deprecated for at least 9 weeks (usually much more). We inform about deprecation in the docs, release notes and by flooding your console -- you won't overlook it.

Support & contributing

If you spotted some code problems in the framework, please open an AT issue or ideally, a pull request with the fix and a test.

See more in CONTRIBUTING.md and TDD guide for Aria Templates.

hashspace's People

Contributors

b-laporte avatar divdavem avatar fbasso avatar flongo avatar jakub-g avatar marclaval avatar piuccio avatar pk1a avatar ymeine avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hashspace's Issues

Type attribute is bindable in all supported browsers but IE8

Following PR #45, consider this template:

# template inputSample(data)
    <div class="info2">All the following inputs are synchronized:</div>
    <div class="section">
        Comment #1: <input type="text" value="{data.comment}"/><br/>
        Comment #2: <input type="{data.comment}" value="{data.comment}"/><br/>
    </div>
# /template

var d={comment:"edit me!"}
inputSample(d).render("output");

The type="{data.comment}" part is not working only in IE8. As a consequence, the behavior of hashspace is inconsistent across the supported browsers.

Possible solutions:

  1. Keep the limitation in IE8 only, and so the inconsistency.
  2. Implement a complex workaround only for IE8.
  3. Extend the limitation to all browsers, until IE8 is no longer supported.

Sub-template parameters are not bound to the data model

Changing in the data model an object that is passed as a parameter to a sub-template does not update the sub-template.

HSP file

var json=require("hsp/json");
var changeObject = function(data) {
  json.set(data, "object", {
    value: (new Date()) + ""
  });
};

# template displayObject(data)
  {data.value}
# /template

# template main(data)
  <#displayObject data="{data.object}" /><br/><br/>
  <button onclick="{changeObject(data)}">Change object</button>
# /template

main({object: {
    value:"initial value"
}}).render("output");

Expected behavior

When clicking on "Change object", the current time should be displayed.

Actual behavior

Nothing changes.

Hashspace version

Tested with commit 930d7b8.

Need a way to know if an event handler attribute value was provided

Consider the following component:

<#alert onclose="{iWasClosed()}">Foo</alert>

and

<#alert>Foo</alert>

where the close callback wasn't passed - currently there is no way (?) of distinguishing those 2 situations. It might be useful in cases like this one where we want to add (or not) a close button to the alert, depending if a callback was provided or not.

Variable update not processed

This does not work:

var json=require("hsp/json");

var data = {counter : 5};

setInterval(function(){
    json.set(data, "counter", data.counter+1);
}, 1000);

# template count()
    Count: {data.counter}
# /template

// display the template in the #output div
hsp.display(count(),"output");

data.counter is incremented all right but it's never displayed.
If I modify the code to pass data.counter as an argument it's only displayed once at the start.

White space **before** template definition

Wouldn't it be nice to support whitespace between a new line and the # template

There might be cases, similar to #85 when a .hsp file looks like this

define("templates", function () {

    # template hello()
       Hello
    # /template

    # template world()
       World!
    # /template
});

Well the compiler here throws an exception saying Unexpected character \'#\''

But write the same file without indentation and it works

define("templates", function () {

# template hello()
       Hello
# /template

# template world()
       World!
# /template

// yes it does work
});

Make templates args naming optional

I know the rationale behind templates named arguments is to outline similarities with HTML5's future custom elements, however as a developer I would really appreciate that syntax to be optional, mainly for these two reasons:

  1. <# subtpl argname="value"> is more verbose than <# subtpl value> (or <# subtpl(value)>) while not necessarily more helpful if your variables are well named.
  2. named arguments prevent templates from being completely dynamic. As illustrated in this example, even though the template name can be dynamic, arguments must be named the same (which might not be the case.)

Would that be feasible?

Template not fully regenerated after a bound value change

When building a template with binded values, the whole template is not fully regenerated when a binded value change.

It can led to an inconsistant display. To explain it, let's consider this simple use case, which can happen quite often:

the template:

var ctrlDef = require("./togglebuttonCtrl");

# template main using ctrl:ctrlDef
    <button class="{ctrl.getClass()}" onclick="{ctrl.onclick()}">
        {if ctrl.isOpen}
            {ctrl.closeLabel}
        {else}
            {ctrl.openLabel}
        {/if}
    </button>
# /template

The controller:

var klass = require("hsp/klass");

module.exports = klass({
    attributes : {
        isOpen : {type: "boolean", defaultValue: false, binding: "2-way"},
        openLabel : {type: "string", binding: "1-way"},
        openClass : {type: "string", defaultValue: "", binding: "1-way"},
        closeLabel : {type: "string", binding: "1-way"},
        closeClass : {type: "string", defaultValue: "", binding: "1-way"},
        onclick : {type: "callback"}
    },
    getClass : function() {
        return this.isOpen ? this.openClass : this.closeClass;
    }
});

Usage :

    <#togglebutton
        openLabel="Open"
        openClass="openClass"
        closeLabel="Close"
        closeClass="closeClass"
        isOpen="{data.isOpen}"
        onclick="{toggleIsOpen(data)}"
     />

The button is first displayed with the right class (openClass attribute).
When running $set(data, "isOpen", !data.isOpen), the button text change, but not its class.

Local assignment not supported yet

It would be useful to be able to assign variable locally in a template.

The idea is to get rid of repeated long path in a template. It would also result in a smaller code size after a minification.

For example:

# template main using ctrl:ctrlDef
    {myVar = ctrl.a.b.c.myVar}
    <span>{myVar}</span>
# /template

Fun with {let} and curly braces in general

I (don't want to flood the issue list | am too lazy to open several issues) so, here goes:

  • {let} lets (!) you use statements that are not reserved words as variables. I'm not sure this is a big deal really, but it can lead to fun code such as:
# template malkovichmalkovichmalkovich (somearray)
    {let foreach=somearray}
    {foreach foreach in foreach}
        {foreach}
    {/foreach}
# /template
  • Some global variables leak into templates scope. For instance:
    {require}, {n} or {exports}
  • The more I play with code, the less I'm comfortable with using curly braces to invoke statements AND to generate outputs. I'm biased by too much AT I guess but I liked the distinction between {statement} and ${output} which I find more readable.
    What do you think?

IE8: inline style not working

# template hello(name)
    <div style="color:red;">
        Hello {name}!
    </div>
# /template

In the output, it gives <div style="">

It seems that setAttributes("style", "...") doesn't work in IE8

Packaging / publishing strategy

With the 8193adc landed we've got now a publishing mechanism in place that can push build files to the gh-pages branch and thus have them accessible through GitHub pages. The results are available here (still some cleanup needed): https://github.com/ariatemplates/hashspace/tree/gh-pages

As soon as those were published I was trying to use new scripts like so:

<head>
    <script type="text/javascript" src="lib/noder.js"></script>
    <script type="text/javascript" src="http://ariatemplates.github.io/hashspace/dist/0.0.1-SNAPSHOT/hashspace.js"></script>
    <script type="noder">
        var main = require('main.hsp.js');
        main({myopen:true}).render('out');
    </script>
</head>

which is basically like trying to use browserify-built #space runtime with noder to load app assets. This obviously doesn't work as require function defined by browserify is different from the one used by noder.... Which is not cool.

I'm opening this issue to discuss possible way of moving forward. I can think of:

  1. abandon browserify and use noder only
  2. abandon noder and use browserify only
  3. make the 2 co-exist if possible
  4. expose part of the runtime as a global variable and check this variable before doing require("hsp/rt") in generated templates

Not sure what is the best course of action here. What I'm trying to achieve here is to have a packaged version of the runtime that I can include in the <script> tag and leave people freedom in packaging / loading they app assets.

Thoughts?

Components - new use cases

Here are a few use cases that I would like to introduce in the hashsapce components:

# template foo(c)
    <#list value="{c.city}">
        <#option key="1" value="First option"/>
        <#separator/>
        <#options collection="{c.cities}" keyref="code" valueref="name"/>
    </#list>
# /template

# template foo(c)
    <#list value="{c.city}">
        <#option key="1">First option</#option>
        <#separator/>
        <#options collection="{c.cities}" keyref="code" args="city:'item',index">
            // This is the HTML template that will be used for each item
            Option #{index+1}: {city.description.shortName}
        </#options>
    </#list>
# /template

# template foo(c)
    <#list value="{c.city}">
        <#option key="1">First option</#option>
        <#separator/>
        {foreach idx,city in c.cities}
            <#option key="{city}">Option #{idx+1}: {city.description.shortName}</#option>
        {/foreach}
        {if c.doSomething}
            <#separator/>
            <#option key="-1">Last option</#option>
        {/if}
    </#list>
# /template

# template foo(c)
    <#panel head="Test panel" expandable="true" args="expanded">
        {if expanded} 
            Here is the exanded content
        {else}
            Here is the collapsed content
        {/if}
    </#panel>
# /template

I think most of this is self-explanatory, except I guess the args attribute: the idea is that component should be able to expose new scope variables to the child templates. Those scope variables should be publicly exposed in the component interface (e.g. attributes). To use them the developer should just pick them by using the args attribute.

Usage should be quite simple: if one wants to use the item and index component variables, she should just write args="item,index".

In some cases it is interesting to be able to name some variables differently (e.g. for readability or to avoid collisions), in which case the syntax could be something like args="city:'item',index" (to rename item into city)

I would be keen to work on this soon, so feel free to shoot (!)

0 as expression is not displayed

The following

var counter = 0;

# template hello()
        Count: {counter}
# /template

only displays Count:, any other counter value is properly displayed.

ObjectLiteral(s) are not supported yet

This error is raised with the following template:

# template hello(config)
    <div class="msg">
        Hello {config.name}!
    </div>
# /template

# template main()
    <#hello config="{{name: 'World'}}"/>
# /template

// display the template in the #output div
main().render("output");

Cssclass syntax no space allowed

I have to be honest, I like the cssSyntax once you get used to it, very concise, however when you have a few classes things get awkward.
You can have a look at this template

      # template cssclass(data)
            <div class="{'one', 'two':data.isTrue, 'three':data.isFalse}">
                This one works
            </div>

            <div class="{'one',
                'two':data.isTrue,
                'three':data.isFalse}">
                This one works too
            </div>

            <div class="{ 'one',
                'two':data.isTrue,
                'three':data.isFalse}">
                This one doesn't (there's a space after the opening bracket)
            </div>

            <div class="{
                'one',
                'two':data.isTrue,
                'three':data.isFalse
            }">
                This one doesn't either (it's my favourite syntax)
            </div>
        # /template

Would it be too difficult to allow optional white spaces for this special syntax?

Wrong behavior with the {foreach} with the {if} statement.

HSP file

var json=require("hsp/json");

var startEdit = function(item) {
    json.set(item, "edit", true);
};
var endEdit = function(item) {
    json.set(item, "edit", false);
};
var add = function(items) {
    json.push(items, {edit:true});
};
var remove = function(items, index) {
    json.splice(items, index, 1);
};

# template list(data)
  <ul>
    {foreach index,item in data.items}
       {if item.edit}
          <li><input type="text" value="{item.value}"></input>
          <button onclick="{endEdit(item)}">OK</button></li>
       {else}
          <li>{item.value}
            <button onclick="{startEdit(item)}">Edit</button>
            <button onclick="{remove(data.items,index)}">Remove</button>
          </li>
       {/if}
    {/foreach}
    <li><button onclick="{add(data.items)}">Add</button></li>
  </ul>
# /template

list({
    items:[]
}).render("output");

Scenario

Click on "Add", type something in the field and click on "OK".

Expected behavior

The field and the "OK" button disappear, and are replaced by some text (what was typed in the field) with the "Edit" and "Remove" buttons.

Actual behavior

The field and the "OK" button stay and a new line is added for the text with the "Edit" and "Remove" buttons.
Note that this only happens for the first item in the list. For the following items, the behavior is correct.

Hashspace version

Tested with commit 930d7b8.

Long dynamic path for sub-templates and/or sub-components

I was lately trying to implement a Tabbar component such as this one

<#tabbar selected="{selected}">
  <@tab label="Tab1">This is tab 1</@tab>
  <@tab label="Tab2">This is tab 2</@tab>
  <@tab label="Tab3">This is tab 3</@tab>
</#tabbar>

Consider the following source code

var Class = require("hsp/klass");

var TabCtrl = Class({
  attributes: {
    onselect: { type: "callback" },
    label: { type: "template" },
    body: { type: "template", defaultContent: true }
  },
  $init: function(parent) {
    this.parent = parent;
    this.selected = false;
  },

  onSelectedChange: function() {
    this.onselect && this.onselect(this.parent.selected);
  }
});

var TabBarCtrl = Class({
  attributes: {
    "class": { type: "string" },
    selected: { type: "int", defaultValue: 0, binding: "2-way" }
  },
  elements: {
    "tab": { type: "component", controller: TabCtrl }
  },
  $init: function() {
    this.selected = 0; //TODO remove when defaultValue is really implemented
    this.updateSelectedBody(null, this.selected);
  },

  onSelectedcontentChange: function() {
    console.log("selectedcontent changed");
  },

  updateSelectedBody: function(previous, next) {
    if (previous) {
      this.content[previous].selected = false;
    }
    this.selectedcontent = this.content && this.content[next];
    this.selectedcontent.selected = true;

    this.selected = next;
  },

  onselect: function(event) {
    var target = event.target,
        previous = this.selected,
        next;

    event.preventDefault();

    while(target.tagName.toLowerCase() !== "a") {
      target = target.parentNode;
    }
    next = parseInt(target.dataset.index, 10);
    if (next === previous) { return; }
    this.updateSelectedBody(previous, next);
  }
});

#template tabbar using ctrl:TabBarCtrl
  <div class="x-tabbar {ctrl.class}">
    <nav class="x-tabs" onclick="{ctrl.onselect(event)}">
    {foreach idx, tab in ctrl.content}
      <a class="{'selected': ctrl.selected === idx}" data-index="{idx}"><#tab.label /></a>
    {/foreach}
    </nav>
    <div class="x-tab-content">
        <#ctrl.selectedcontent.body />
    </div>
  </div>
#/template

module.exports = tabbar;

It appears that when the property ctrl.selectedcontent is updated, the corresponding onSelectedcontentChange() is called, but the child component inclusion is apparently not refreshed, which is not the awaited behavior

PS: ideally I would even have written that in the first place
<#ctrl.content[ctrl.selected].body /> and be waiting for the same refresh to occurs but unfortunately it is not supported as well for now...

Compiler should throw an exception when compilation fails

Currently compiler will generate output even if compilation fails. While it is true that it also sets a flag indicating if a compilation failed or not, it makes working with the compiler API harder than it needs to be. For example, while working on a Grunt / gulp plugin, we shouldn't need to generate a file that would output errors in a browser, we should simply break the compilation (fail fast). We can still generate client-side files in the express integration, though.

My proposal here is the following one - let's remove the error-reporting content generation from the compiler and let's move it to a separate class that could be plugged on top of the compiler.

Introduce jshint checks ?

Not sure how others feel about it but personally I find it useful to fail build on failing lint rules. From the experience I know that it gets harder and harder to introduce it as the code-base grows.

NotFoundError raised when displaying again a component template attribute

Hiding and displaying again a component template attribute raises a NotFoundError error.
Here is a sample HSP file with an expand/collapse component:

HSP file

var json = require("hsp/json");
var klass = require("hsp/klass");
var ExpandCollapseController = klass({
    attributes: {
        content: {
            type: "template",
            defaultContent: true
        }
    },
    $init: function() {
        this.visible = true;
    },
    show: function() {
        json.set(this, "visible", true);
    },
    hide:function() {
        json.set(this, "visible", false);
    }
});

# template expandCollapse using c:ExpandCollapseController
    {if c.visible}
        <a onclick="{c.hide()}">Hide</a><br/>
        <#c.content />
    {else}
        <a onclick="{c.show()}">Show</a><br/>
    {/if}
# /template

# template main(data)
    <#expandCollapse>
        My content
    </#expandCollapse>
# /template

main().render("output");

Scenario

Click on the "Hide" link. "My content" disappears.
Then click on the "Show" link.

Expected behavior

"My content" appears again.

Actual behavior

The following error is raised:

NotFoundError: An attempt was made to reference a Node in a context where it does not exist.

The error is raised from hsp/rt/cptattinsert.js:30 (cptAttribute.node seems to be null according to the debugger in Chromium):

this.node.appendChild(cptAttribute.node);

Hashspace version

Tested with commit 930d7b8.

Missing end component undefined

Try to compile this template

# template eat()
    <#fruit>Banana</fruit>
# /template

And you'll get

Sorry [ { description: 'End element </fruit> does not match any <fruit> element',
    line: 2,
    column: 16 },
  { description: 'Missing end component </undefined>',
    line: 2,
    column: 2 } ]

The second message should probably be Missing end component </#fruit>

Dialogs and popovers

One of the important (still) missing feature in hashspace is the support of dialogs (and popovers) - so here is a proposal on how I see it:

Dialogs

First I think dialogs should be as easy to use a dialog as using normal templates. This is why I think opening a dialog could be done in a similar way as calling render():

mytemplate("Hello World!").show();

The difference with render() is that show() shouldn't take a DOM element or DOM element id as argument - as hashspace should automatically create a poping div to host the template content. However hashspace should not generate any special surrounding HTML elements to frame the template content - as we should let the template choose how the dialog frame should be rendered (e.g. by using a 3rd party component).

The next question that comes to mind is then: how to pass dialog arguments (e.g. modal, center-horizontal, center-vertical, etc.). I don't think it should be passed as show() argument as it is not the controller's role to determine those rendering options. As a consequence, I would prefer going for a new {dialog-options} statement, that could look like this:

# template mytemplate(msg)
  {dialog-options center:true, modal:true}

  <div class="mydialog">
    <div class="msg">{msg}</div>
    <div class="btn"><input type="button" value="OK" onclick="{dialog.close()}"></div>
  </div>
# /template

Of course {dialog-options} should be ignored if the template is not called in a dialog context (and if multiple dialog-options are found in the template stack, the first one should be used). The arguments could be a JSON structure, without the surrounding curly brackets (btw. syntax should be similar to CSS expressions that should be updated as well to match this syntax).

As you can read, the next problem to solve is: how to close a dialog, and pass a closing argument (e.g. 'ok' or 'cancel'). For this, I think hashspace should automatically create a dialog object accessible in the template scope. This dialog object would let the template know that it is in 'dialog mode' and would expose the dialog properties. It would also expose a close() method that would allow to callback the dialog caller, if a callback is provided as show() argument:

# template mytemplate(msg)
  {dialog-options center:true, modal:true}

  <div class="mydialog">
    <div class="msg">{msg}</div>
    {if dialog}
      // only show buttons if in dialog mode
      <div class="btn">
        <input type="button" value="OK" onclick="{dialog.close('ok')}">
        <input type="button" value="OK" onclick="{dialog.close()}">
      </div>
    {/dialog}
  </div>
# /template

mytemplate("Hello World!").show(function(closeArg) {
  // do sth when the dialog is closed
});

Of course, hashspace should automatically manage the 'ESC' key to close a dialog and call the callback in cancel mode (i.e. without any argument) - this default behaviour could also be overridden through a dialog-options argument.

The dialog object structure could look like this:

dialog={
  modal:true,
  center:"both",
  centerH:true,
  centerV:true,
  backdrop:true,
  autoClose:true, // i.e. if click on the backdrop
  close:function() {}
  // etc. (to be refined)
}

Popovers

Even though popovers are a bit different from dialogs, I think we should also be able to manage them in a similar way:

# template anothertpl(msg1,msg2)
  <div class="foo" onmouseover="{info(msg2).pop('right-top')}">
    Main message: {msg1}
  </div>
# /template

# template info(msg)
  <div class="pop" onclick="{popover.close()}">
    Popup message: {msg}
  </div>
# /template

As dialog and popovers have many arguments that are not common (e.g. modal, center, etc.), I would suggest to have a 2nd context object (i.e. popover) instead of reusing dialog.

Of course, it should be possible to call it from a component's controller:

var SampleCtl=klass({
  attributes:{
    "msg":{type:"string"},
    "popupmsg":{type:"string"}
  },
  triggerPopover:function() {
    // $select should call querySelector() or sizzle on old browser
    info(this).pop('bottom-left', this.$select('.foo'), function (arg) {
      if (arg==='ok') {
        // do something
      }
    });
  }
});

# template sample using c:SampleCtl
  <div class="foo" onmouseover="{c.triggerPopover()}" ontap="{c.triggerPopover()}">
   {c.msg}
  </div>
# /template

# template info(c)
  <div class="pop" onclick="{popover.close('ok')}">
    {c.popupmsg}
  </div>
# /template

... so - what do you think?

Hashspace files Content-Type

If we plan to use .hsp filesystem extension to store template files, we should think about a proper named content-type to associate with them.

It would be useful especially for webservers to respond with the proper content-type.

Right now it's just sending back plain application/octet-stream

I suggest text/x-hashspace-template (based on what is used for handlebars...)

Coding guidelines

This issue has been created after a post from @dpreussner on the at.com forum.

Since the code base is still at an early stage, we should take the opportunity to lay out a set of basic rules to follow so that code style stay consistent everywhere.

These rules could be listed in the (to be created) Contributing.md file.

Changing an intermediate object breaks bindings in one way

Changing an intermediate object in the data model breaks bindings from the data model to the UI (bindings from the UI to the data model still work though).

HSP file

var json = require("hsp/json");

var changeObject = function(data) {
    json.set(data,"object",{value:new Date()+""})
};

var changeValue = function(data) {
    json.set(data.object,"value",new Date()+"")
};

var displayValue = function(data) {
    alert(data.object.value);
};

# template bindings(data)
    <input type="text" value="{data.object.value}" />
    <input type="text" value="{data.object.value}" /><br />
    <button onclick="{changeObject(data)}">Change object</button>
    <button onclick="{changeValue(data)}">Change value</button>
    <button onclick="{displayValue(data)}">Display value</button>
# /template

bindings({object:{value:"initial value"}}).render("output");

Expected behavior

As both fields are linked to the same value in the data model, both fields and the data model should always be synchronized, even when changing the intermediate object data.object.

Actual behavior

As long as we do not click on the "Change object" button, everything is synchronized as expected.
However, after clicking on "Change object", no listener seems to be is added to the new object, and changing the value by clicking on "Change value" no longer updates the display. Typing in one of the fields still updates the data model but does not update the other field.

Hashspace version

Tested with commit 930d7b8.

"this.parent is null" exception, then everything is blocked

Plunk

Here is the plunk to reproduce this issue: http://embed.plnkr.co/rGe1i4limHyvVeNtmRCq

Click on the close link next to "Tab A".
The following exception is raised: this.parent is null
Then the whole UI is blocked and no longer react to anything.

This issue is linked to the fact that a sub-template is used for "Tab A".
The issue does not happen when closing "Tab B".

HSP file

var klass = require("hsp/klass");

# template tabA(tab)
  {tab.title}
# /template

var TabCtrl = klass({
  $init: function () {
    this.tabs = [{title:"Tab A", template: tabA},{title:"Tab B"}];
  },
  removeTab: function (event, index) {
    event.preventDefault();
    this.tabs.splice(index, 1);
  }
});

# template tabs using c:TabCtrl
  <ul class="nav nav-tabs">
  {foreach index,tab in c.tabs}
    <li><a href="#">
      {if tab.template}
        <#tab.template tab="{tab}" />
      {else}
        {tab.title}
     {/if}
     &nbsp;<a class="close" onclick="{c.removeTab(event, index)}">&times;</a>
    </a></li>
  {/foreach}
  </ul>
# /template

tabs().render(document.body);

Hashspace version

Tested with commit d692180.

Implement long paths for insert statements

This is a feature not coded yet, but it would be quite useful:

This syntax works:

but not this one:

{ctrl.getText()}

We currently get this message: Long paths for insert statements are not supported yet.

{foreach} not working if collection not in the scope of the template

Please consider the following templates:

var items = [1,2,3];

# template hello(name)
    {log items}
    <ul>
    {foreach item in items}
      <li>{item}</li>
    {/foreach}
    </ul>
    <ul>
      <li>{items[0]}</li>
      <li>{items[1]}</li>
      <li>{items[2]}</li>
    </ul>
# /template

hello().render("output");

It displays the list only once, {foreach} doesn't output anything but the {log} works fine ... hence not user friendly.
Even if the binding to items shouldn't be done as it is defined outside of the template, it should work in 'read-only' mode.

For users, it is even more confusing as a successful workaround is using {let}:

var items = [1,2,3];

# template hello(name)
    {let myItems = items}
    <ul>
    {foreach item in myItems}
      <li>{item}</li>
    {/foreach}
    </ul>
    <ul>
      <li>{items[0]}</li>
      <li>{items[1]}</li>
      <li>{items[2]}</li>
    </ul>
# /template

hello().render("output");

This way, the list is displayed twice.

ArrayLiteral(s) are not supported yet

Raised with the following template:

var klass = require("hsp/klass");

var Ctrl = klass({
      attributes: {
        "states": {type: "object", defaultValue: [1,2,3], binding: "1-way"}
    }
});

# template cpt using c:Ctrl
  Do nothing!
  {c.states[0]}
# /template

# template hello(name)
    <div class="msg">
        Hello {name}!
    </div>
    <#cpt states="{[4,5,6]}"/>
# /template

// display the template in the #output div
hello("World").render("output");

Initial bootstrap

We should consider having a real javascript file to be included in pages where you want to use hashspace.

This is mainly for people to avoid having to require('hsp/rt'). Once the bootstrap loaded, it would be available to you on window.hsp for example.

Then up to you to wrap your code in a closure, passing hashspace runtime as an argument

(function (hsp) {

...

// do whatever you want with hsp, it's here for you
// don't have to bother loading it yourself with a require

...

})(window.hsp)

What do you think ?

Void elements

Do you think it would make sense to support void elements according to the html5 specifications

A void element is an element whose content model never allows it to have contents under any circumstances.

For instance br, hr, img and others.

Void elements only have a start tag; end tags must not be specified for void elements.

And the description of start tags says

Optionally, a "/" character, which may be present only if the element is a void element.

That is to say that <br> is perfectly fine in html5, but not in hashspace, try it and the compiler will tell you

Missing end element </br>

Useless (?) line in compiled templates

When compiling a template the generated file is

  // ################################################################ 
  //  This file has been generated by the hashspace compiler          
  //  Direct MODIFICATIONS WILL BE LOST when the file is recompiled!  
  // ################################################################ 

  var hsp=require("hsp/rt");
  var hello = require("hsp/rt").template([], function(n){
      return [n.$text(0,["Hello"])];
   });

The line var hsp=require("hsp/rt"); is not needed, unless I miss something.

It's not big of a deal, but if we want to keep it, it should be part of the compiled template, rather than the header.

Why do I say that?

// Trying to convert a template into a require.js module
// I know that all the require might not work, but this is just an example
define("amd-template", ['my-amd-dependencies'], function (dep) {

# template hello()
   Hello
# /template

   return function () {
      return hello;
   };
});

this is compiled into

  // ################################################################ 
  //  This file has been generated by the hashspace compiler          
  //  Direct MODIFICATIONS WILL BE LOST when the file is recompiled!  
  // ################################################################ 

  var hsp=require("hsp/rt");

// Trying to convert a template into a require.js module
// I know that all the require might not work, but this is just an example
define("amd-template", ['my-amd-dependencies'], function (dep) {

  var hello = require("hsp/rt").template([], function(n){
      return [n.$text(0,["Hello"])];
   });

   return function () {
      return hello;
   };
});

Which does the job except for the fact that hsp is defined outside my define method, thus 'leaking' into a different scope.

If it must be kept, than it should be

define("amd-template", ['my-amd-dependencies'], function (dep) {

  var hsp=require("hsp/rt");
  var hello = require("hsp/rt").template([], function(n){

Template as component attribute tests failing in FFox

4 tests in the Template as component attribute are failing under FFox (fine under Chrome, didn't check on other browsers).

Tests in question are:

Firefox 25.0.0 (Ubuntu) Template as component attribute tests template attribute passed as sub-element FAILED
    Expected undefined to equal 'Hello World! '.
    window.__cjs_module__["/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp"]/</<@/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp:83

    Expected undefined to equal 'Hello folks! '.
    window.__cjs_module__["/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp"]/</<@/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp:89

Firefox 25.0.0 (Ubuntu) Template as component attribute tests template attribute passed as static text attribute FAILED
    Expected undefined to equal 'Hello World!'.
    window.__cjs_module__["/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp"]/</<@/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp:101

Firefox 25.0.0 (Ubuntu) Template as component attribute tests template attribute passed as dynamic text attribute FAILED
    Expected undefined to equal 'Hello World!'.
    window.__cjs_module__["/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp"]/</<@/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp:114

    Expected undefined to equal 'Hello folks!'.
    window.__cjs_module__["/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp"]/</<@/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp:120

Firefox 25.0.0 (Ubuntu) Template as component attribute tests default template attribute passed as node content FAILED
    Expected undefined to equal 'Hello World!'.
    window.__cjs_module__["/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp"]/</<@/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp:131

    Expected undefined to equal 'Hello folks!'.
    window.__cjs_module__["/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp"]/</<@/home/pkozlowski/work/os/ghrepos/pk1a/hashspace/public/test/rt/tplattribute.spec.hsp:137

Firefox 25.0.0 (Ubuntu): Executed 102 of 102 (4 FAILED) (0.528 secs / 0.093 secs)

Event utility

This is probably an enhancement or feature request.

In both gestures/gestures.js and rt/elnode.js there is code like this

var addEL = (nd.addEventListener !== undefined);
if (addEL) {
   node.addEventListener(hnd.evtType, this, false);
} else {
    node.attachEvent("on" + hnd.evtType, this._attachEventFn);
}

Wouldn't it be better to create an event module?

By doing so you move browser dependent code in a single place and you also allow hashspace users to use this method.
Imagine I don't want to use jQuery (silly me!) then it's up to me to solve a cross-browser issue that is already solved by hashspace.

For instance in Google maps API (which is not a template framework) add and remove listeners are public. You can call google.maps.event.addDomListener(window, 'load', initialize); instead of using jQuery or similar.

Does that make sense?

Default attribute value / type in controllers

I'm not sure if I misunderstand something

var klass=require("hsp/klass");
var controller = klass({
    attributes: {
        value: {
            // HERE PLEASE: I'm saying that this attribute is an integer
            type: "int",
            defaultValue: 20
        }
    },
    $afterRender: function () {
        console.log(typeof this.value, ":", this.value);
    }
});

Use this template

# template helloworld(data)
    <#child />
# /template

# template child using c:controller
    // whatever
# /template

The log correctly says number : 20

Use <#child value="" /> and you get number : 20, arguable but I'm fine with it.

The problem is when you use <#child value="10" />, logs say string : 10.

Why is that?

What is this convert method for? I would expect that my attribute is automatically cast into a number, am I wrong?

Force a white space

Ok I think this one is tricky. Have a look at this page

<!DOCTYPE html>
<html>
    <head>
        <title>Hashspace</title>
        <script type="text/template" id="helloworld">
            # template nospace()
                <div class="container">
                    <span class="block">One</span>
                    <span class="block">Two</span>
                </div>
            # /template
        </script>
        <style>
        .block {
            background-color: red;
            padding: 5px;
        }
        .container {
            background-color: green;
            padding: 10px;
        }
        </style>
    </head>
    <body>

        <h3>Plain HTML - This is what I want</h3>
        <div>
            <div class="container">
                <span class="block">One</span>
                <span class="block">Two</span>
            </div>
        </div>

        <h3>Hashspace - Copy and paste the same content as before</h3>
        <div id="output"></div>


        <script src="jquery-1.10.2.min.js"></script>
        <script charset="UTF-8" src="hashspace.min.js"></script>
        <script charset="UTF-8" src="hashspace.compiler.min.js"></script>

        <script type="text/javascript">
        var compiler = require("hsp/compiler");
        var template = compiler.compile($.trim($("#helloworld").html()));
        eval(template.code);


        nospace().render("output");
        </script>
    </body>
</html>

Output
snap

I tried to force a whitespace but this doesn't work either

            # template nospace()
                <div class="container">
                    <span class="block">One</span>
                    &nbsp;
                    <span class="block">Two</span>
                </div>
            # /template

It literaly writes &nbsp; in the generated div, without interpreting it

What works instead is a comment (even empty)

            # template nospace()
                <div class="container">
                    <span class="block">One</span>
                    //
                    <span class="block">Two</span>
                </div>
            # /template

Just as reference, other libraries provide a way to insert a white space, and interpret (correctly?) html entities.

Using a sub-template stored on a controller raises an error.

Using a sub-template stored on a controller raises an error.

HSP file

# template firstTemplate
  This is my first template.
# /template

# template secondTemplate
  This is my second template.
# /template

var json = require("hsp/json");
var Controller = function () {
  this.currentTemplate = firstTemplate;
  this.changeTemplate = function (template) {
    json.set(this, "currentTemplate", template);
  };
};

# template main using ctl:Controller
  Choose template:
  <ul>
  <li><a onclick="{ctl.changeTemplate(firstTemplate)}">First template</a></li>
  <li><a onclick="{ctl.changeTemplate(secondTemplate)}">Second template</a></li>
  </ul>
  <#ctl.currentTemplate />
# /template

// display the template in the #output div
hsp.display(main(),"output");

Expected behavior

The content of firstTemplate should be displayed.

Actual behavior

The following error is raised:

Invalid component reference: ctl is not defined.

Hashspace version

Tested with commit 6fc27f7.

setTimeout (called on keydown) changing a property on a controller prevents typing in a field

Consider the following file (which is an extract of an autocomplete component). Try typing something in the field.

HSP file

var klass = require("hsp/klass");
var $set= require("hsp/$set");
var Ctrl = klass({
    keyDown: function () {
        if (!this.myTimeout) {
            var self = this;
            $set(this,"myTimeout", setTimeout(function () {
              $set(self,"myTimeout", null);
              // ...
            },5));
        }
    }
});

# template myTemplate using c:Ctrl
      <input onkeydown="{c.keyDown()}" value="{c.value}">
# /template

myTemplate().render("output");

Expected behavior

The input field contains what was typed.

Actual behavior

The input field does not receive all typed keys.

Hashspace version

Issue introduced by commit 601e25f.
It does not happen with previous commit 33b7794.

index sometimes not updated in a foreach containing `if` and sub-template call

Consider the following file. Click 3 times on the first Remove link.

HSP file

var count = 0;
var addItem = function (items) {
   count++;
   items.push({name:"item"+count, edit:true});
};

var removeItem = function (items, index) {
   items.splice(index, 1);
};

# template displayItem(curItem, index)
    <tr>
        <td>{index}</td>
        <td>{curItem.name}</td>
        <td><a onclick="{removeItem(items, index)}">Remove</a></td>
    </tr>
# /template

# template list(items)
    <div style="background-color:white;">
        <table> 
            {foreach index,curItem in items}
                {if curItem.edit}
                    <#displayItem curItem="{curItem}" index="{index}"/>
                {/if}
            {/foreach}
        </table>
        <a onclick="{addItem(items)}">Add item</a>
    </div>
# /template

var items = [];
addItem(items);
addItem(items);
addItem(items);

list(items).render("output");

Expected behavior

All items in the list are removed. Indexes (in the first column) are updated correctly each time an item is removed.

Actual behavior

item3 cannot be removed from the list and has the wrong index (1 in the first column, instead of 0).
Note that this behavior does not happen if we remove the if statement from the loop, or if we remove the displayItem template (and make its content inline).

Hashspace version

Tested with commit d71dd37.

Remove a space after # template?

This is more a question to discuss: should we keep a space after # when defining a template?

Personally I keep forgetting it all the time and it is not very intuitive...

What if we could simply write:

#template alert using c:AlertController
...
#/template

instead of

# template alert using c:AlertController
...
# /template

?

Remove empty attributes when binded to an empty value

At the moment, an attribute binded to an empty value generate an empty attribute.

For example :

will output:

It means a lot of useless markup to be evaluated by the browser. Moreover, it can lead to some issues, for example if you want to manage a disable/not disable input with the binding :

will output:

which leads to a disabled field.

Sorting, Paging and filtering on collections

Here is a proposal to support the classical collection manipulations in hashspace. The idea is to support piped expressions with standard function filters - and support as well the possibility to use objects in these piped expressions. This allows to bring simplifications for advanced use cases (cf. dynamic sorting or selections)

var Sorter = require("foo/Sorter"),
    Filter = require("foo/Filter"),
    SelectionMgr = require("foo/SelectionMgr");

function isAdult(itm) {
    return itm.age>17;
}

function nameSort(itm1,itm2) {
    // same sort function as for Array.sort
    var nm1=itm1.lastName+" "+itm1.firstName, nm2=itm2.lastName+" "+itm2.firstName;
    if (nm1===nm2) {
        return 0;
    } else {
        return (nm1>nm2)? 1 : -1;
    }
}

{template test(model)}
    <div class="blah">
        {let age=new Sorter("age","OAD"),       // sort by age property, state sequence Original->Ascending->Descending
             name=new Sorter(nameSort),         // custom sort function: nameSort, default state sequence: "AD"
             adult=new Filter(isAdult),         // use custom function: isAdult
             pages=new PageBuilder(20),         // 20 items per page
             selection=new SelectionMgr(3)}     // 3 selections max

        // filter
        <a onclick="{adult.toggle()}">
            {if adult.state === 'ON'}
                Show all people
            {else}
                Show adults only
            {/if}
        </a>

        // headers
        <a onclick="{age.toggle()}">Age</a> ({age.state}) // e.g. "O", "A" or "D" - can be used to change the CSS
        <a onclick="{name.toggle()}">Name</a> ({name.state}) // e.g. "A" or "D" 

        // list
        <ul>
            {foreach itm,idx,itmd in (model.persons|adult.filter|pages.filter|orderBy:"title"|age.sort|name.sort|selection.decorate)}
                // itmd stores meta-data assotiated to the item and created by the different filters
                <li class="{md.selected? 'selected' : cssClass}" onclick="{selection.toggle(itm)}">
                    <span>
                        {itm.firstName} {itm.lastName} ({itm.age})
                    </span>
                </li>
            {/foreach}
        </ul>

        // pages -> could be of course proposed as a component
        {if pages.indexes}
            <span 
                class="{pages.selection.isFirst? 'link' : 'disabled'}" 
                onclick="{pages.shiftSelection(-1)}">
                Prev. page
            </span>
            {foreach idx in pages.indexes}
                {if idx===pages.selection.index}
                    <span class="current">{idx}</span>
                {else}
                    <a onclick="{pages.select(idx)}">{idx.value}</a>
                {/else}
                {if !idx_islast}&nbsp;{/if} // separator
            {/foreach}
            <span 
                class="{pages.selection.isLast? 'link' : 'disabled'}" 
                onclick="{pages.shiftSelection(1)}">
                Next page
            </span>
        {/if}

        <button onclick="submit(selection.selectedIndexes)">OK</button>
    </div>
{/template}

Variable scope issue

I'm reopening what was initially issue #16 since @Mlaval fix only corrected a side effect in it.

So, this does not work:

var json=require("hsp/json");

var data = {counter : 5};

setInterval(function(){
    json.set(data, "counter", data.counter+1);
}, 1000);

# template count()
    Count: {data.counter}
# /template

// display the template in the #output div
hsp.display(count(),"output");

data.counter is incremented all right but it's never displayed.

Admittedly, this is because hsp is not able to know which variables should be monitored for bindings. However I find this disturbing since the template has access to it (it is displayed the first time.) Also, since the code is compiled, couldn't the parser identify variables used in expressions?

Binding function calls

I'm opening this issue to discuss the possibility to extend the binding mechanism to function calls.

Background:

Yes indeed - function calls should be supported, like in any expression. Having said that, the current function call binding should be improved so that function are automatically re-called when a bound element changes (and the function bound elements should be the function arguments + the function context (i.e. this) - in other words, when a direct property of a function context changes, the function should be called again to determine if the function result has changed (which is not the case today) - e.g.

  // x should be reprocessed if arg1 or arg2 or c changes, 
  // or if a direct property of c changes - e.g. c.foo++;
  {let x=c.getRating(arg1,arg2)} 

I obviously believe this would be a great feature to have. Now I imagine it less restrictive than that. The current proposal implicitly gets the elements to watch (the function arguments and its context) while I'd be more in favor to let the developer explicitly state that, so that a function could be re-evaluated based on external factors that are neither arguments nor the context.

When discussing this, @benouat suggested a way to do that based on modifiers which I find kinda neat, something like this:
<div class={watch(indicator)|classGenerator(arg)}>
Don't mind the syntax, the idea is essentially to tell Hashspace which function to be called and what to watch as target for re-evaluation.

What do you think?

Is jQuery a dependency or devDependency?

So, this is about managing dependency on jQuery within #space. Currently jQuery is referenced from hashtester.js:
$=require("node_modules/jquery/dist/jquery.min.js")
https://github.com/ariatemplates/hashspace/blob/master/hsp/utils/hashtester.js

but jQuery is declared as a devDependency:
https://github.com/ariatemplates/hashspace/blob/master/package.json#L44

This means that a version installed from npm doesn't come with jQuery installed. But the way jQuery is referenced from hashtester.js means that it must be installed as a dependency of #space...

The practical implication of all this is that we can't use hashtester.js from an external project :-/

Not sure what is the best curse of action here, I can see several solutions:

  • mark jQuery as a direct dependency (probably the easiest one...), not sure what is the impact on the noder / browserify packaging, though
  • split #space into 2 projects, moving testing part into a separate project
  • change CJS processor for Karma so it can handle referencing jQuery like so:
    $=require("jquery/dist/jquery.min.js"),

Thoughts?

Missing refresh in template after value change

Plunk

Here is the plunk to reproduce this issue: http://embed.plnkr.co/YmP8EW8ARdsjIDcvk5bc
(and also http://embed.plnkr.co/9paCyklH88XvwvS1cSOk)

Go to page 2 and click on "Change value".

HSP file

var klass = require("hsp/klass");

var Tab = klass({
    $constructor : function () {
        this.changeValue();
    },
    changeValue: function () {
      this.value = new Date().getTime();
    }
});

# template displayObject(value)
  Value: {value} (through template)<br>
# /template

var Ctrl = klass({
    $constructor: function () {
        this.activeTab = new Tab();
        this.tabs = [this.activeTab,new Tab()];
    },
    clickTab: function (event,tab) {
        event.preventDefault();
        this.activeTab = tab;
    }
});

# template main using controller:Ctrl
    <ul class="nav nav-tabs">
        {foreach index,tab in controller.tabs}
            <li class="{'active': controller.activeTab === tab}">
            <a href="#" onclick="{controller.clickTab(event,tab)}">
                Page {index+1}
            </a>
            </li>
        {/foreach}
    </ul>
    Value: {controller.activeTab.value} (direct)<br>
    <#displayObject value="{controller.activeTab.value}"/><br>
    <button onclick="{controller.activeTab.changeValue()}">Change value</button>
# /template

main().render(document.body);

Expected behavior

When clicking on "Change value" on page 2, the value should be updated in both places (both in direct display and through template), just like on page 1.

Actual behavior

When clicking on "Change value" on page 2, only the direct display is updated. The value displayed through the displayObject template is not refreshed until the active tab changes.

Hashspace version

Tested with commit d692180.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.