Skip to content

Latest commit

 

History

History
1223 lines (1032 loc) · 29.3 KB

m2d2.md

File metadata and controls

1223 lines (1032 loc) · 29.3 KB

M2D2 Documentation

Outline

Installing

Installation instructions for node or the web, are explained in the main README page.

Quick Start

If you have 5 minutes, check our Quick Start Guide.

Rendering

Loading

To initialize M2D2, you have mainly two options:

The first one, is to execute some code after the DOM is ready:

m2d2.ready($ => { /* ... */ });

The variable $ can be anything you choose (any variable name allowed by Javascript). Throughout all the documentation, examples and tutorials we use $ for simplicity (please don't get confused with JQuery $).

The other option is used when you want to have access to M2D2 before the DOM is ready or after you know for a fact that it is ready (for example, if you use your scripts at the end of your HTML document, you want to implement an extension, when using together with another framework/library, etc.):

$ = m2d2.load();

DOM objects

The most basic functionality is to access to DOM elements easily:

const user = $("#user");

In this example user is an extended DOM object. In addition to all the methods and properties that a DOM element (HTMLElement or Node) possess, we are adding other methods and properties which are convenient to simplify your code, for example:

  • text : Set or Get text in Nodes
user.text = 'Hello World';

console.log(user.text);
  • html : Set or Get HTML in Nodes (useful if HTML comes from the server, not recommended otherwise)
const fromServer = `<div class='blue'>User</div>`;
user.html = fromServer;

console.log(user.html);
  • find(selector) : find the child which matches the CSS selector
  • findAll(selector) : find all children who match the CSS selector
  • sibling(selector) : find the sibling which matches the CSS selector
user.find(".delete").click();
user.findAll("li").forEach(li => { li.delete() });
user.sibling(".message");
  • index() : Get the index (position) of child within its parent node (for <option> use index property)
movies.find(".favorite").index()
  • parent() : Get parent node
  • posterior() : Get next sibling element
  • anterior() : Get previous sibling element
movies.parent()
movies.find(".favorite").posterior()

More about index, posterior and anterior in lists

Other additional methods/properties added to Nodes (explained later):

Additional events:

Short Assign

To simplify your code, M2D2 will guess which properties you are trying to set. For example, you can place your data in this way:

const user = $("#user", {
    name : {
        text : "Yoda"
    },
    age : {
        text : 900
    },
    email : {
        text : "yoda@starwars.com"
    }
});

But it is easier this way:

const user = $("#user", {
    name : "Yoda",
    age : 900,
    email : "yoda@starwars.com"
});

Depending on the element in which you are trying to set this data, M2D2 will decide which property to set it to. If, for example, age is an span element, it will set the text property, (or html if HTML is detected) but if it is an input element, it will set it as value:

<span class="age">900</span>
<input type="number" name="age" value="900" />

This is specially useful if your data comes from a server (which usually does), for example:

$.get("/user/yoda", response => {
  if(response.data) {
    const user = $("#user", response.data);
  }
});

In which, response.data is an object (from JSON) which matches your structure (or vice-versa).

NOTE: $.get is from the XHR extension

In order to achieve this functionality, M2D2 uses a Proxy object, which may not be supported in old browsers. To turn this feature OFF, you can set: m2d2.short = false at the beginning of your code.

More about how to access the elements' data when using short assignment in Linking Elements

Locating Elements

M2D2 will try its best to guess which node or property you are trying to assign, for example:

<div id="user" title="User Info">
  <span class="name"></span>
  <form>
    <label><input type="text" name="age" /></label>
  </form>
</div>
const user = $("#user", {
    title : "User : Karen McLaren",  // <--- user.title property
    name  : "Karen McLaren",         // <--- span.name element
    age   : 30,                      // <--- input[name=age] element
    label : "Age: ",                 // <--- label element
});

M2D2 will first look for <tag>, #id, [names] and .classes in that order.

Linking Elements

One of the most interesting features in M2D2 is that you don't need to represent your data exactly in the same way you have your HTML structure. For example:

<div id="user">
  <form>
    <div class="wrapper">
      <fieldset>
        <div class="name">
          <label>First Name:</label>
          <input type="text" name="first" />
          <label>Middle Name:</label>
          <input type="text" name="middle" />
          <label>Last Name:</label>
          <input type="text" name="last" />
        </div>
      </fieldset>
    </div>
  </form>
</div>

In order to set the "Last Name", you don't need to do: user.form.wrapper.fieldset.name.last (which would be painful), you can skip to the important parts:

const user = $("#user", {
  first  : "Barney",
  middle : "Waitforit",
  last   : "Stinson"
});

And later, you can access or modify it easily:

user.middle = "Legendary"
console.log(user.middle.value)

NOTE: The short assignment can only be used when setting a value into an element or property and not when accessing it.

What happens, is that M2D2 creates links in the user object that points to the child elements of our choice (so we don't have to specify each step in the DOM tree).

One advantage of doing so, is that if we redesign our HTML completely, as long as we keep the classes, names or ids in place, you won't need to change your Javascript. For example, the above user example, will work as well in this HTML (no matter the order of the elements):

<section id="user">
  <div class="last"></div>
  <div class="first"></div>
  <div class="middle"></div>
</section>

Keeping the previous example in mind, user is an HTMLElement (or DOM Node), so you can set its title property as usual:

user.title = 'Title'
// and read it in the same way: 
console.log(user.title)

But for child elements:

user.first = "Barry"
console.log(user.first)       // This will print the HTMLElement object
console.log(user.first.text)  // This will print the property "text"

You can not use short assignment in root elements, the following IS NOT CORRECT:

user = "Not Correct"  // Trying to replace variable `user`

Fortunately we set user as const, so that will never be allowed. The only way it will work is if we specify the property:

user.text = "Correct"

Double references

You can always (when needed) create different M2D2 objects using the same elements, for example:

<div id="user">
  <div class="name"></div>
  <div class="email"></div>
</div>
const basic = $("#user", {
  name : "jannete"
});
const advanced = $("#user", {
  email : "jan@example.com"
});

In this case, basic and advanced are just aliases:

console.log(basic.email.text);    // prints "jan@example.com"
console.log(advanced.name.text);  // prints "jannete"

Possible applications for these "double references" are:

  • Parts of the data comes from different data sources
  • Simplify code by referring to different parts of the layout (shallow vs deep)
  • Keep a logical reference to an element which may change over time (like: user.main in the case of having several users)

Resolving conflicts

There are times in which you may want to use a class or id of an existing property:

<div id="user" title="User Profile">
  <h1 class="title">User :</h1>
</div>
const user = $("#user", {
  title: "New Title"
});

As user.title is an existing property (which can not be replaced), M2D2 will create a link to the h1 element as: user.$title (and report it in the console logs).

In order to prevent conflicts, it is better to replace the class name or if you can't replace it (due to style declarations), add another class to access it, for example:

<div id="user" title="User Profile">
  <h1 class="title user_title">User :</h1>
</div>
const user = $("#user", {
  user_title: "New Title"
});

Other way can be to handle h1 element separately from user:

const user_title = $("#user .title");

CSS and styles

To make things easier to manage CSS class names, all M2D2 elements have the css property, which you can set in this way:

const user = $("#user", {
    css : "myclass",          // `<div class='myclass' ...
    css : ["full", "blue"],   // `<div class='full blue' ...
});
// Or:
user.css = "myclass";
user.css = ["full", "blue"];

In both of the cases, css will replace any existing classes with the ones specified. If you want to add or remove classes, you need to set an object with true (add), or false (remove) as values:

const user = $("#user", {
    css : {
        full : true,  // will add "full" to existing classes
        link : false  // will remove "link" class if exists
    }, 
});
// Or:
user.css = { full : true, link : false }

When you read the css property, it will return the classList Node property:

user.css.contains("link");
user.css.add("blue");
user.css.remove("red");
user.css.toggle("active");
user.css.length;
user.css.forEach(cls => { /* ... */ })

You can also set the style you want directly:

const user = $("#user", {
  style : {
     color : "green",  // or any HTML supported value
     backgroundColor : "#3338",  // background-color becomes backgroundColor in Javascript
     display : "inline"
  }
});
// Or:
user.style.backgroundColor = "#3338";

Show / Hide elements

M2D2 offers you a simple way to hide / show HTML elements by using the property show:

const user = $("#user", {
    show : false,  // initial state will be hidden
    text : "User"
});
// Show user when menu botton is clicked:
const menu = $("#menu", {
    user : {
        onclick : function (ev) {
            user.show = true
        }
    } 
});

M2D2 also provides an event to perform actions when an element is shown:

const user = $("#user", {
  show : false,  // initial state will be hidden
  text : "User",
  onshow : function (ev) {
      this.text = "User is now shown"
  }
});

Events

onload

After an object has been created the onload event is fired:

const user = $("#user", {
  text : "User",
  onload: function (ev) {
    this.text = "Loaded" // you can not use `user` here
  }
});

onready

After an object has been rendered the onready event is fired:

const user = $("#user", {
  text : "User",
  onready: function (ev) {
    user.text = "Ready" // you can use `user` here
  }
});

The main difference between onload and onready is that onready is executed asynchronously after the object has been created, so at that moment, the assigment has been completed and the object can be accessed. As onload is executed during the object creation it happens before onready and we can be sure that no other code has been executed at that moment, while in onready, it is possible that other parts of your code has been executed. You may want to use onready to be sure that the object has been created. Triggering events like focus() may work better in onready than onload.

Updating reference

With M2D2 you can observe changes on objects values and properties, so you don't have to check for changes constantly. These are some way in which that is useful:

Linked References

With this feature, you can update immediately one object when another changes, for example:

<div id="user">
  <span class="username"></span>
</div>
<form id="form">
  <input type="text" name="new_user_name" value="" />
</form>
const form = $("#form", {
    new_user_name : ""
});

const user = $("#user", {
    username : [form.new_user_name, "value"]
});

Whenever form.new_user_name.value changes, user.username.text will be updated automatically. You can use this feature in any property, including dataset and style :

const form = $("#form", {
  dataset : { id : 0 },
  style : { color : "blue" }
});

const user = $("#user", {
  uid : [form.dataset, "id"],
  style : { backgroundColor : [form.style, "color"] }
});

Even better, you can edit the value before assigning it:

const user = $("#user", {
  uid : {
    text: [form.dataset, "id", (val) => {
      return (val * 1) + 1000;
    }],
  }
});

When form.dataset.id is updated, we update user.uid.text with that value plus 1000.

onupdate

If you need to perform some action which is not an assigment to a property or value, you can use the onupdate event:

const form = $("#form", {
  new_user_name : {
      onupdate : function (ev) {
          console.log(ev,detail);
      }
  }
});

In console will print something like:

{
  "type"     : "string",
  "property" : "value",
  "newValue" : "user1000",
  "oldValue" : ""
}

no selector = HTML Fragment

So far we have always used M2D2 with a selector and an object ($(selector, object)), but there is one more usage:

const fragment = $({
  text : "I'm invisiblae"
})

When you don't use a selector, you are creating a Node which is not connected to the Document (an HTML Fragment). It can be used in many ways, but one useful way is to keep a shared object for references:

$.scope = $({
  user : "peter600",
  level : "moderator"
});

const user = $("#user", {
    username : [$.scope, "user"]
})

NOTE: $.scope is assigned to the M2D2 instance. More on this on General Recommendations

The main advantage of using a fragment is that it doesn't depend on whether the element exists or not.

Forms

M2D2 will help you to handle input forms easily. These are the highlights:

<form id="user">
  <input type="text" name="username" />
  <input type="checkbox" value="1" name="active" />
  <button type="submit">Send</button>
</form>

Properties

Handling properties is as simple as:

const user = $("#user", {
    username : {
        disabled : true
    },
    active : {
        checked : true
    }
})
// Changing the properties:
user.username.disabled = false
user.active.checked = false

Multiple selector

M2D2 supports using tag names or classes to update a group of elements. This is particularly useful in forms:

const user =$("#user", {
    input : {
        disable : true,
        onclick : function(ev) {
            this.css.toggle("clicked");
        }
    }
})

That way, all <input> elements will be disabled by default and all of them, will have the same onclick event function. You can use this feature to apply rules or values to several elements at once, and then set individual rules or values for them (which reduce code by eliminating repetition):

const user =$("#user", {
  input : {
    disable : true,
    onclick : function(ev) {
      this.css.toggle("clicked");
    }
  },
  username : "jennifer1",
  active : {
      checked : true
  }
})

Validating and Getting Data

const user = $("#user", {
  username : {
    requried : true,
    placeholder: "User name here",
    pattern : "[a-z0-9]+",
    title : "example: user100",
    oninput : function (ev) {
        this.css.add("active")
    },
    onblur : function (ev) {
        this.css.remove("active")
    }
  },
  // On form submit:
  onsubmit : function (ev) {
      const data = this.getData();
      // data will contain an object with the values, for example:
      /* 
      {
        username : "Matt",
        active : 1
       } 
       */
       return false;
  },
  onload : function () {
      this.username.focus();
  }
})

When using getData() it will validate the form first and then will return an object with all the visible element's data (including type='hidden'). If you want to include also fields that are not shown, set as argument: true in getData(true).

Lists

To generate a list of elements M2D2 has two properties: template and items:

const users = $("#users", {
    template : { },
    items : []
});

template is an object which will define the structure and items is an array which holds the data. You must specify at least one of the two, for example:

<ul id="list"></ul>
const users = $("#users", {
  items : [
      "one",
      "two",
      "three"    
  ]
});

it will generate:

<ul id="list">
  <li>one</li>
  <li>two</li>
  <li>three</li>
</ul>

M2D2 will try to guess which template to use. In this case, as #list is <ul> it will create <li> elements.

It is equivalent to:

const users = $("#users", {
  template : "li", // You can specify the tag name you want for your items  
  items : [
    "one",
    "two",
    "three"
  ]
});

Templates

If you want to specify a more complex structure, you have 2 ways to define templates:

  1. in HTML
  2. in Javascript

1. in HTML

Templates can be specified using standard HTML:

<div id="users">
  <template>
    <div class="user">
      <span>Name:</span>
      <span class="name"></span>
      <span>Age:</span>
      <span class="age"></span>
    </div>
  </template>
</div>
const users = $("#users", {
   items : [
     { name : "Paul", age : 23 },
     { name : "Sam", age : 72 },
     { name : "Tam", age : 44 },
   ] 
});

If <template> is specified, M2D2 will use its content to generate new items.

2. in Javascript

<div id="users"></div>
const users = $("#users", {
   template : {
       div : {
            css : "user",
            labelName : {
                tagName : "span",
                text : "Name:"
            },
            name : {
                tagName : "span",
                css : "name"
            },
            labelAge : {
                tagName : "span",
                text : "Age:"
            },
            age : {
                tagName : "span",
                css : "age"
            }
       }
   },
  items : [
     { name : "Paul", age : 23 },
     { name : "Sam", age : 72 },
     { name : "Tam", age : 44 },
   ] 
});

You can also generate HTML inside your items, for example:

const users = $("#users", {
  items : [
    {
        div : {
            css : "user",
            test : "Angeline"
        }
    },
    {
      div : {
        css : "user",
        test : "Anthony"
      },
    }
  ]
});

However, using templates is cleaner and easier to maintain (recommended way).

Events in templates

You most certainly will need to interact with your items once they are rendered. You can also specify events inside your templates:

const users = $("#users", {
  template : {
      name : {
        tagName : "span",
        css : "name",
        onclick : function (ev) {
            users.title = "Active user: " + this.text
        }
      }
  },
  items : [
    { name : "Paul" },
    { name : "Sam" },
    { name : "Tam" },
  ]
});

If you are using HTML templates you can set the events in Javascript:

<div id="users">
  <template>
    <div class="user">
      <span>Name:</span>
      <span class="name"></span>
    </div>
  </template>
</div>
const users = $("#users", {
  template : {
    name : {
      onclick : function (ev) {
        users.title = "Active user: " + this.text
      }
    }
  },
  items : [
    { name : "Paul" },
    { name : "Sam" },
    { name : "Tam" },
  ]
});

Also, you can set them in HTML (not recommended):

<div id="users">
  <template>
    <div class="user">
      <span>Name:</span>
      <span class="name" onclick="someFunction"></span>
    </div>
  </template>
</div>

Selected item

Once your items have been rendered, you can access them in many ways:

// Get total of items:
users.items.length
// Traverse all items:
users.items.forEach(item => { /* ... */ })
// Clear all items:
users.items.clear()
// Get the first item:
users.items.first()
// Get the last item:
users.items.last()

NOTE: M2D2 will automatically add dataset.id to each item.

To select one item from the list:

const users = $("#users", {
  template : {
      onclick : function (ev) {
        this.selected = true; 
      }
  }
  /* ... */
});
// You can also set it:
users.items.first().selected = true;
users.items.get(10).selected = true;

Once an item is selected, M2D2 will add the [selected] attribute to the item (you can set special CSS rules for that). Then, you can use:

users.items.selected() // get the selected item
users.items.selected().index() // get the selected item index
users.items.selected().dataset.id // get the selected item original index (this is preserved after removing or adding items)
users.items.selected().posterior().selected = true // select the next item from the selected one
users.items.selected().anterior().selected = true // select the previous item from the selected one

You can use almost all Array functions with items, like:

  • at
  • concat
  • copyWithin
  • every
  • fill
  • filter
  • find
  • findAll
  • findIndex
  • forEach
  • includes
  • indexOf
  • join
  • keys
  • lastIndexOf
  • map
  • reduce
  • reduceRight
  • reverse
  • slice
  • some
  • splice
  • values

In addition to those functions, we have added more, like:

  • clear
  • first
  • get
  • last
  • pop
  • push
  • remove
  • selected
  • shift
  • sort
  • unshift

Sorting

If you want to sort a simple list, list.items.sort() and list.items.reverse() are all you need. If you need to sort more specifically, look at this example:

<table id="list">
  <thead class="headers">
    <tr>
      <th class="fruit">Fruit</th>
      <th class="qty">Qty</th>
    </tr>
  </thead>
  <tbody class="data">
    <template>
      <tr>
        <td class="fruit"></td>
        <td class="qty"></td>
      </tr>
    </template>
  </tbody>
</table>
const list = $("#list", {
  dataset: { sortedBy: "" },
  headers : {
    th : { 
      onclick : function (ev) {
          const th    = this;
          const label = th.text;
          const prop  = th.css[0];
          const compare = ( a, b ) => {
            return a[prop].text < b[prop].text ? -1 : (a[prop].text > b[prop].text ? 1 : 0)
          }
          if(list.dataset.sortedBy === label) {
            list.data.items.reverse();
          } else {
            list.dataset.sortedBy = label;
            list.data.items.sort(compare);
          }
      }
    }
  },
  data : {
    items: [
      { fruit : "Apple",      qty: 19 },
      { fruit : "Orange",     qty: 34 },
      { fruit : "Banana",     qty: 62 },
      { fruit : "Peach",      qty: 15 },
      { fruit : "Mango",      qty: 18 },
      { fruit : "Watermelon", qty: 30 },
      { fruit : "Papaya",     qty: 14 },
      { fruit : "Pear",       qty: 22 },
      { fruit : "Strawberry", qty: 45 }
    ]
  }
});

Extending

If you want to add some functionality that you will likely use in several parts of your code (globally) or perhaps in different projects, you can do so by extending M2D2:

// Here we don't use 'ready', we use 'load' (before DOM is ready)
m2d2.load($ => {
    $.canvasEditor = () => {
        // Do something here
    }
})
// To use it:
m2d2.ready($ => {
   // Anywhere: 
   $.canvasEditor();
});

Extensions

M2D2 comes with some extensions that you may find useful. These are optional. We have also prepared bundles which contains these extensions according to functionality.

Alerts

This extension makes it easy to display alerts, confirmation, input dialogs and more. Read more about the Alerts Extension

Lang

With this extension you can handle multiple languages easily. Read more about the Language Extension

Storage

This extension provides an easy way to save and restore data into localStorage and sessionStorage. Read more about the Storage Extension

XHR

This extension handles almost any kind of HTTP request to a server (e.g., GET, POST, PUT, DELETE, etc.) Read more about the XHR Extension

Upload

This extension makes it easy to upload files to a server via XHR (included in XHR bundle). Read more about the Upload XHR Extension

WS

This extension gives you an easy-to-use WebSocket client. Read more about the WebSocket client Extension

Improving Compatibility

Most of the modern browsers should work fine, but in case you want to support a browser which is complaining about Proxy or MutationObserver objects, you can turn them off:

Short Assignment OFF

// At the beginning of your code
m2d2.short = false;

By turn this feature OFF, you won't be able to short assign values: user.name = "Kelly" you will have to write: user.name.text = "Kelly"

Updates OFF

// At the beginning of your code
m2d2.updates = false

By turning this feature OFF, you won't be able to use Linked References.

Utils

In addition to all the tools explained in this documentation, M2D2 also provides extra tools to handle nodes, types, properties, attributes, etc. Read more about Utils

Using with JQuery

In most cases you won't need JQuery at all, but there are some valid reason on why to use it together, for example, you may have your own libraries already written in JQuery, or you want to use some well-known libraries together with your code (like JQuery-UI, LightBox, Tabslet, FixText, etc.). Why having to reinvent the wheel?

To use it together with JQuery you may want to rename the $ variable (either in JQuery or in M2D2) if you want to use JQuery inside the M2D2 scope (otherwise, you may still be able to use $ variable JQuery in its own scope), for example:

To use them separately:

m2d2.ready($ => {
  // Your M2D2 code here 
  const user = $("#user", {
      show : false,
      onclick : function (ev) { /* ... */ }
  });
});

jQuery($ => {
  // Your jQuery code here 
  $("#user").toggle().click();
});

Or together (M2D2 is $):

m2d2.ready($ => {
  const user = $("#user", {
    show : true,
    onclick : function (ev) { 
        jQuery(this).toggle();
    }
  });
});

Or (JQuery is $):

const m2 = m2d2.load();
jQuery($ => {
  const user = m2("#user", {
    show : true,
    onclick : function (ev) {
      $(this).toggle();
    }
  });
});

Using with Framework7

  1. Install M2D2 using npm:
npm i m2d2
  1. Inside your app.js, add (at the top of your javascript imports):
// Import M2D2 with WS bundle
import m2d2 from 'm2d2/ws';

Read more in: Import other bundles

  1. Initialize pages:
// Initialize pages (at the end of app.js)
var app = new Framework7({
  /* ... */
});  
app.on('pageInit', (page) => {
  const $ = m2d2.load();
  if(page && pages[page.name] !== undefined) {
    pages[page.name]($, app);
  }
});
  1. Inside your pages (you don't use m2d2.ready):
export default ($, $f7) => {
  const login = $("#login", {
    form : {
      onsubmit : function(ev) {
        const data = this.getData();
        $f7.dialog.alert('Username: ' + data.username + '<br/>Password: ' + data.password, () => {
          $f7.loginScreen.close();
        });
        return false;
      }
    },
    username : "admin",
    password : "******"
  });
}