Giter Club home page Giter Club logo

magery's Introduction

Magery

Build Status

Easy-to-use JavaScript templates that can work with server-side rendering in any language.

Aims

  • To make enhancing your multi-page website with JavaScript easier
  • To work with your choice of back end language
  • To integrate with existing components when necessary
  • To be relatively small so you can use it for little (and large) enhancements

I wrote this library to prove that you don't need a 'single page app' to build great dynamic websites. Often the best user experience is a multi-page website with thoughtful JavaScript enhancements. Today, these enhancements come with a cost: the almost-inevitable tangle of jQuery that accompanies them. Magery is an attempt to fix this using shared templates to dynamically update the page.

Download

File size

While there are no doubt smaller libraries out there, Magery is positioned on the more lightweight end of the spectrum. This is to encourage its use for relatively small improvements to server-generated pages.

A comparison with some popular minified production builds:

Angular v1.6.4:              ########################################  163 kb
React + React DOM v15.6.1:   #####################################     150 kb
jQuery v3.2.1:               #####################                      85 kb
jQuery (slim build) v3.2.1:  #################                          68 kb
Magery (compiler + runtime): ###                                        12 kb
Magery (runtime):            ##                                          6 kb

Example

Components in Magery use custom HTML5 tags defined by templates:

<template data-tagname="my-greeting">
  Hello, {{ name }}!
</template>

These templates can be rendered by your server, or compiled with JavaScript and used to dynamically update the page:

var components = MageryCompiler.compile('template');
components['my-greeting'](target, {name: 'world'});

Here's a full example:

<!DOCTYPE html>
<html>
  <head>
    <title>Example</title>
    <meta charset="utf-8">
  </head>
  <body>
    <!-- target -->
    <my-greeting></my-greeting>

    <!-- templates -->
    <template data-tagname="my-greeting">
      Hello, {{ name }}!
    </template>
        
    <!-- javascript -->
    <script src="../build/magery-compiler.min.js"></script>
    <script src="../build/magery-runtime.min.js"></script>
    <script>
         
      var components = MageryCompiler.compile('template');
      var target = document.querySelector('my-greeting');
      var data = {"name": "world"};
         
      components['my-greeting'](target, data);
         
    </script>
  </body>
</html>

You also can view this example in the browser, or see the other examples.

Future calls of components['my-greeting'](target, data) will not wipe and replace the target element but incrementally update the page as the data changes.

Template syntax

Variables

You can pass JSON data to components as a context for your templates. Properties of the context object can be inserted into the page using {{ double curly braces }}:

<template data-tagname="my-greeting">
  Hello, {{ name }}!
  <img src="{{ avatar_url }}" alt="{{ name }}'s avatar">
</template>

Variables can be expanded in both attributes and text. The inserted values are escaped so it is not possible to insert raw HTML into the page.

Booleans

Some attributes do not hold values and are either on/off depending on their presence. The checked attribute is a good example:

<input type="checkbox" checked>

For convenience, Magery allows you to use a variable, and will only insert the attribute if the variable is truthy (i.e. not 0, false, null, undefined or []).

<input type="checkbox" checked="{{ recurring_order }}">

Attributes

data-each

Loop over an array, rendering the current element for each item in the array. This attribute's value should be in the form "item in array" where item is the name to use for the current item being rendered, and array is the context property to iterate over.

Example use

Template:

<ol>
  <li data-each="user in highscores">
    {{ user.name }}: {{ user.score }} points
  </li>
</ol>

Data:

{
  highscores: [
    {name: 'popchop', score: 100},
    {name: 'fuzzable', score: 98},
    {name: 'deathmop', score: 72}
  ]
}

Result:

<ol>
  <li>popchop: 100 points</li>
  <li>fuzzable: 98 points</li>
  <li>deathmop: 72 points</li>
</ol>

If possible, combine data-each with a data-key attribute to uniquely identify each element in the loop. This enables Magery to more efficiently patch the DOM.

Template:

<ul>
  <li data-each="item in basket" data-key="{{ item.id }}">
    {{ item.title }}
  </li>
</ul>

Data:

{
  basket: [
    {id: 1000, title: 'jelly'},
    {id: 1001, title: 'custard'},
    {id: 1002, title: 'cake'}
  ]
}

Result:

<ul>
  <li>jelly</li>
  <li>custard</li>
  <li>cake</li>
</ul>

data-key

Helps Magery match up elements between page updates for improved performance. The attribute can use the normal variable {{ expansion }} syntax and its value must be unique within the parent element.

This attribute is particularly useful when combined with the data-each attribute but it can be used elsewhere too. See the data-each examples for more information.

data-if

Conditionally expands the element if a context property evaluates to true. Note that an empty Array in Magery is considered false.

Example use

Template:

<span data-if="article.published">
  Published: {{ article.pubDate }}
</span>

Data:

{
  article: {
    published: true,
    pubDate: 'today'
  }
}

Result:

<span>Published: today</span>

data-unless

This is the compliment to data-if, and will display the element only if the property evaluates to false. Note that an empty Array in Magery is considered false.

Example use

Template:

<span data-unless="article.published">
  Draft
</span>

Data:

{
  article: {
    published: false,
    pubDate: null
  }
}

Result:

<span>Draft</span>

data-embed

This is only used during server-side rendering. Adding a data-embed property to an element will include data from the current context in the output. A data-context attribute is added to the element containing JSON encoded context data.

See server-side rendering.

Processing order

It is possible to add multiple template attributes to a single element, though not all combinations make sense. The attributes will be processed in the following order:

  • data-each
  • data-if
  • data-unless
  • data-key

That means you can use the current item in a data-each loop inside the value of a data-if, data-unless or data-key attribute.

You can also use these attributes when calling another template:

<div data-template="top-articles">
  <my-article data-each="article in articles"></my-article>
</div>

Tags

template-children

Expands child nodes from the calling template, if any were provided. Note: any child nodes or attributes on this tag will be ignored.

Example use

Template:

<template data-tagname="my-article">
  <h1>{{ title }}</h1>
  <div class="main-content">
    <template-children />
  </div>
</template>
            
<template data-tagname="my-page">
  <my-article title="article.title">
    <p>{{ article.text }}</p>
  </my-article>
</template>

Data:

{
  article: {
    title: 'Guinea Pig Names',
    text: 'Popchop, Fuzzable, Deathmop'
  }
}

Result:

<my-page>
  <my-article>
    <h1>Guinea Pig Names</h1>
    <div class="main-content">
      <p>Popchop, Fuzzable, Deathmop</p>
    </div>
  </my-article>
</my-page>

template-embed

Only used during server-side rendering, ignored during client-side DOM patching. Embeds a template definition in the output.

Example use

Template:

<template data-tagname="my-tags">
  <ul>
    <li data-each="tag in tags">{{ tag }}</li>
  </ul>
</template>

<template data-tagname="my-page">
  <h1>{{ title }}</h1>
  <template-embed name="my-tags"></template-embed>
</template>

Data:

{
  "title": "Example", 
  "tags": ["foo", "bar"]
}

Result:

<my-page>
  <h1>Example</h1>
  <template data-tagname="my-tags">
    <ul>
      <li data-each="tag in tags">{{ tag }}</li>
    </ul>
  </template>
</my-page>

Components

Templates can be rendered by other templates as components. To do this, simply use the template name as a custom tag. For example, the following template:

<template data-tagname="my-greeting">
  <h1>Hello, {{name}}!</h1>
</template>

Could be rendered elsewhere using the tag <my-greeting>:

<my-greeting name="{{ user.name }}"></my-greeting>

By adding attributes to your custom tag, you can pass data to the sub-template. In the above example the context property user.name is bound to name inside the my-greeting template.

The attribute names can only be lower-cased. If you want use multi-word attribute name, you should use dash of underscore.

It is also possible to provide literal string values as context data:

<my-greeting name="world"></my-greeting>

These literals can also be useful for configuring generic event handlers (e.g. by providing a target URL to POST data to).

Events

Listeners can be attached to elements using the on* attributes (e.g. onclick). Although the templates use the attribute syntax, the event handlers will in reality be attached using addEventListener():

<template data-tagname="example">
  <p>{{ counter.name }}: {{ counter.value }}</p>
  <button onclick="incrementCounter(counter)">
    Increment
  </button>
</template>

You can pass values in the current template context to the event handler as arguments. You can also pass the event object itself by using the special event argument:

<input type="text" oninput="updateField(name, event)">

The handler name (e.g. updateField above) is matched against the provided event handlers object which can be passed into templates when patching the page.

Example

<!DOCTYPE html>
<html>
  <head>
    <title>Events</title>
    <meta charset="utf-8">
  </head>
  <body>
    <template data-tagname="say-hello">
      <button onclick="sayHello(name)">click me</button>
    </template>
        
    <say-hello></say-hello>

    <script src="../build/magery-compiler.min.js"></script>
    <script src="../build/magery-runtime.min.js"></script>
    <script>

      var components = MageryCompiler.compile('template');
      var target = document.querySelector('say-hello');

      var data = {
        name: 'testing'
      };

      // handlers for template
      var handlers = {
        sayHello: function (name) {
          alert('Hello, ' + name + '!');
        }
      };

      // events are bound on first patch
      components['say-hello'](target, data, handlers);

    </script>
  </body>
</html>

View this in your browser, or see the examples directory for other ways to use events.

Handler functions can also be nested in order to namespace handlers for specific components e.g. myApp.sayHello(name).

API

MageryCompiler.compile(target)

Find and compile Magery templates in the current HTML document.

Arguments

  • target - a DOM element, or the CSS selector for elements, containing zero or more templates

Return value

Returns an object containing Template objects, keyed by template name (taken from their data-template attributes).

Example

var templates = MageryCompiler.compile('.magery-templates');
var templates = MageryCompiler.compile('#myTemplates');
var templates = MageryCompiler.compile('template');

var node = document.getElementById('#myTemplates'));
var templates = MageryCompiler.compile(node);
        
// access the returned Template() objects using template[name]

State management

Magery only handles updating the DOM and binding event handlers. Deciding when to update the page and managing your application's state is up to you. Thankfully, Magery works well with many popular state management libraries available from the React ecosystem.

Here's an example using Redux.js:

<!DOCTYPE html>
<html>
  <head>
      <title>Redux example</title>
      <meta charset="utf-8">
  </head>
  <body>
      <!-- target element -->
      <my-counter></my-counter>
      
      <!-- our templates -->
      <template data-tagname="my-counter">
        <p>
          Clicked: <span id="value">{{ count }}</span> times
          <button onclick="increment()">+</button>
          <button onclick="decrement()">-</button>
        </p>
      </template>

      <!-- dependencies -->
      <script src="./redux.min.js"></script>
      <script src="../build/magery-runtime.js"></script>
      <script src="../build/magery-compiler.js"></script>
      
      <!-- application code -->
      <script>
       var components = MageryCompiler.compile('template');
       
       // create a store
       var store = Redux.createStore(function (state, action) {
           if (typeof state === 'undefined') {
               return {count: 0};
           }
           switch (action.type) {
               case 'INCREMENT':
                   return {count: state.count + 1};
               case 'DECREMENT':
                   return {count: state.count - 1};
               default:
                   return state;
           }
       });
       
       var target = document.querySelector('my-counter');
       var handlers = {};
       
       function render() {
           components['my-counter'](target, store.getState(), handlers);
       }
       
       // add event handlers using Magery
       handlers.increment = function () {
           store.dispatch({type: 'INCREMENT'});
       };
       handlers.decrement = function () {
           store.dispatch({type: 'DECREMENT'});
       };
       
       // update the page when the store changes
       store.subscribe(render);
       
       // initial render
       render();
      </script>
  </body>
</html>

Now, you can benefit from the extensive ecosystem and dev tools built around Redux.

Server-side rendering

Magery has been designed to work with server-side rendering in any language. If you'd like to create a new server-side library then you can use the cross-platform Magery test suite to get you started. If your library passes the tests, you can send a pull request to include it here.

Tests

You can run the current test suite in your browser. Please report any failures as GitHub issues and be sure to state the browser version and operating system you're running on.

If you're working on the Magery source code, you can run these tests by executing npm run build-test and then visiting test/index.html in your browser. Or, you can run npm test to use a headless browser (slimerjs) and see test results in your console. For this to work you'll need to install xvfb:

sudo apt-get install xvfb

If you're not on Linux then you may need to run slimerjs directly without xvfb (just remove xvfb-run from the test script line in package.json). Running without xvfb means you'll see a small slimerjs window temporarily pop up.

Benchmarks

You can run the current benchmarks in your browser. They're very simple tests of performance so don't read too much into them.

Live editor

This allows you to play around with Magery syntax without installing it, and immediately see the results in your browser.

Open the editor

magery's People

Contributors

benzen avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar  avatar

magery's Issues

Data not passed the same way i expect

It seams to me that groovy-magery and js-magery doesn't render the same way for this kind of exemple.
Don't know we is wrong.

<template data-tagname="my-app">
  <app-title></app-title>
  oups
  <app-button text="minus" onclick="decrement()"></app-button>
  {{counter}}
  <app-button text="plus" onclick="increment()"></app-button>
</template>

<template data-tagname="app-button">
  <button>{{text}}</button>
</template>

<template data-tagname="app-title">
  <h1>{{title}}</h1>
</template>
{
    "name": "World",
    "counter": 0,
    "title": "abc"
}

Here is the render I've got (using the example editor app).
capture d ecran 2018-01-15 a 11 29 52

Here is what i expected, which is also what groovy-magery produce
capture d ecran 2018-01-15 a 11 30 59 1

Since groovy magery is more or less a direct translation of python-magery I assume python-magery will produce the same output.

Miss behavior when attribute is "newT"

Here is an exemple

<template data-tagname="my-app">
    <todo-input newT="{{newT}}"></todo-input>
</template>

<template data-tagname="todo-input">
    <input type="text" value="{{newT}}"/>
</template>
{
    "newT":"alpha"
}

Am I missing something ?

I've made a little exemple using groovy-magery and js-magery to demonstrate the combination of both of them.
I also use redux, maybe it's the source of my problem, but I'm not sure.

What happend?
The templates are correctly rendered by the backend (at least the way I expected), when the patch the dom with js-magery most of the dom is removed.

Here is the dom before patch

<app-root>
  <app-title>
  <h1>Sake</h1>
</app-title>
  oups
  <app-button>
  <button>minus</button>
</app-button>
  0
  <app-button>
  <button>plus</button>
</app-button>
</app-root>

Here is the dom after patch

<app-root>
  <app-title></app-title>
  oups
  <app-button></app-button>
  0
  <app-button></app-button>
</app-root>

Can you look at the template and app.js, I assume there is something that i didn't get, but it's unclear what it is.

Terminology: pages vs. components

Magery is designed around custom HTML5 template tags that called 'components' (though sometimes confusingly referred to as templates in our docs). On the server-side these need to be placed into a full HTML document to be useful, on the client-side only the components are used.

I'm proposing we call all current templates using custom tags 'components', and we introduce a concept of 'pages' for use with server-side implementations.

A page might look something like this:

<!DOCTYPE html>
<html>
  <head>
    <title>{{ page.title }}</title>
  </head>
  <body>
    <h1>Profile</h1>
    <my-component user="{{ user }}"></my-component>
  </body>
</html>

Which makes use of a component defined like this:

<template data-tagname="my-component">
  Hello, {{ user.name }}!
</template>

In the server-side library you would call separate functions/methods/whatever to load components and pages, for example:

magery.load_components("./templates/components")
magery.load_pages("./templates/pages")

Components would be rendered using their tag name:

magery.render_component("my-component", data)

And pages would be rendered using the path to the template file:

magery.render_page("project/example.html", data)

Exactly how those server-side APIs look and how page paths are constructed will be up to server-side implementors - they can do whatever makes sense in their language.

About redux / connect

Being able to use redux super cool.
But I think we need an equivalent to redux-connect in order to get all the benefice of redux.

Do you plan on giving a more advanced exemple ?
If not do you know it well enought to guide someone to do so?

Discuss: handling form input

Controlled

Currently, all elements are considered 'controlled' (in the React sense). That means, to update a form element you must intercept oninput or similar events then update the data and re-patch the page. This gives the most control over these elements using only template data. However, it is also quite invasive as it effectively disables all inputs and instead requires re-patching on every keypress or button click. This is the only way to reliably control the real (displayed) value on a text input using only template data.

{{#define example}}
  <input type="text" value="{{description}}" oninput="updateDescription" />
  <button onclick="save">Save</button>
{{/define}}
container.dispatch = function (name, event, context) {
    if (name == 'updateDescription') {
        context.description = event.target.value;
    }
    if (name == 'save') {
        saveData(context.description);
    }
    patch();
};

Uncontrolled

The alternative is to leave all elements uncontrolled. That means we do not intercept input events or force the user to re-patch to update them. When uncontrolled, the form elements would become a secondary store of state in the application (as their value property may differ from their value attribute). You would not be able to update the value property using template data. So effectively you can't change the live/displayed value using only template data and patching. However, this is less invasive and less likely to subvert normal browser behaviour (e.g. clearing a textbox after a user types data into an input in the gap between page load and javascript running). For this to be workable we'd need a way to refer to elements on the page without traversing the DOM. This gives us a light touch with the browser, which is desirable, and I'd be interested in your ideas on how this could work.

{{#define example}}
  <form>
    <input type="text" name="desc" value="{{description}}" />
    <button onclick="save">Save</button>
  </form>
{{/define}}
container.dispatch = function (name, event, context) {
    if (name == 'save') {
        saveData(event.target.form.desc.value);
    }
    patch();
};

Hybrid

The third option is to try and do both. React allows you to choose between these approaches depending on whether a value attribute is present. We can't do this easily since some form elements (e.g. checkbox) use a boolean attribute to set their value. Omitting that attribute means it may be unchecked, for example. We can't allow assigning checked={false} as when rendered server-side it would be interpreted differently. So we need to either always have controlled elements, always have uncontrolled elements, or introduce some custom attributes which let us choose. I'd like to keep element customization to a minimum where possible so I'm not as keen on this approach.

Notes

Whatever we choose needs to make sense in a server context too. Adding a custom attribute that sets the checked property on a form element might require waiting for JavaScript to load before it displays correctly. If possible, the initial server-side render should exactly match the initial JS patch.

Discuss

I'd like your feedback on which approach makes sense. So far the everything is controlled approach works, but is invasive. I want to hear your ideas on how we might improve this.

Error handling and reporting

My philosophy so far has been to avoid runtime errors based on the shape of the context data wherever possible. Otherwise, it's very easy to take out large portions of a website when the context data is updated.

However, I think errors we can detect during compile time should be reported. In the JavaScript library, some of these might be best reported as warnings (or even console.errors for developers), to avoid breaking user interaction wherever possible. The server-side implementations should, however, produce exceptions in their normal way.

I'm opening this ticket in part to document my thoughts, but also to invite comment. If you think this is the wrong approach, let's discuss it here.

Should components define their 'arguments'?

I like to think of components as functions accepting named arguments (e.g. title="{{ article.title }}"). Should the <template data-tagname="my-template"> tag also require/allow you to define the context it expects to receive?

How do you call call components between them

I'm trying to build a magery compiler for Java environement.
But I don't get how do you call a component from another one (test 0701).

I've looked at the JS implementation and at the python implementation but I still don't get it.
Thanks for your help

Add conversion in attribute format

Here is an exemple in which it's unclear how things are transformed.

<template data-tagname="my-app">
    <app-entry my-attr="{{myAttr}}">
</template>
<template data-tagname="app-entry">
    {{my-attr}}
</template>
{
    "myAttr": "def"
}

This will render def.
As explained previously it's required by html (#15 (comment)).

It seams pretty natural to me to use lower-dashed-case for attribute (like normal attribute).
What is really weird to me is the fact that my data will change it's shape ().

What if the compiler did the job of converting back and forth the attributes and value path in data depending of the context ?

render outside of target

Here is an exemple

<html>
  <head>
    <script src="/js/magery-compiler.js"></script>
    <script src="/js/magery-patcher.js"></script>
    <script src="/js/redux.js"></script>
    <template data-tagname="app-title">
      <h1>{{text}}</h1>
    </template>
  </head>
  <body>
    <app></app>

    <script>
      var components = MageryCompiler.compile('template');

      // create a store
      var store = Redux.createStore(function (state, action) {
          if (typeof state === 'undefined') {
            return {
              title: "fuck mennn",
              count: 0
            };
           }
           switch (action.type) {
            case 'INCREMENT':
              return {count: state.count + 1};
            case 'DECREMENT':
              return {count: state.count - 1};
            default:
              return state;
           }
       });

       var target = document.querySelector('app');
       var handlers = {};

       function render() {
          console.log(store.getState())
          components['app-title'](target, store.getState(), handlers);
       }

       // add event handlers using Magery
       handlers.increment = function () {
           store.dispatch({type: 'INCREMENT'});
       };
       handlers.decrement = function () {
           store.dispatch({type: 'DECREMENT'});
       };

       // update the page when the store changes
       store.subscribe(render);

       // initial render
       render();
    </script>
  </body>
</html>

I've noticed that in the produced dom, the tree rendered by outside ouf the target

<html><head>
...
    <template data-tagname="app-title">
      <h1>{{text}}</h1>
    </template>
  </head>
  <body>
    <app-title>
      <h1></h1>
    </app-title><app></app>
    <script>
...

</body></html>

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.