Giter VIP home page Giter VIP logo

ampersand-dom-bindings's Introduction

ampersand-dom-bindings

Part of the Ampersand.js toolkit for building clientside applications.

Takes binding declarations as described below and returns key-tree-store of functions that can be used to apply those bindings to a DOM tree.

ampersand-view use this for declarative bindings.

The returned functions should be called with these arguments: The root element, the current value of the property, and a name for the binding types where that is relevant.

install

npm install ampersand-dom-bindings

Binding types

text

sets/maintains textContent of selected element. treats undefined, null, and NaN as ''

'model.key': {
    type: 'text',
    selector: '.someSelector' // or hook
}

class

sets and maintains single class as string that matches value of property

  • handles removing previous class if there was one
  • treats undefined, null, and NaN as '' (empty string).
'model.key': {
    type: 'class',
    selector: // or hook
}

attribute

sets the whole attribute to match value of property. treats undefined, null, and NaN as '' (empty string). name can also be an array to set multiple attributes to the same value.

'model.key': {
    type: 'attribute',
    selector: '#something', // or hook
    name: 'width'
}

value

sets the value of the element to match value of the property. works well for input, select, and textarea elements. treats undefined, null, and NaN as '' (empty string).

note: The binding will only be applied if the element is not currently in focus. This is done by checking to see if the element is the document.activeElement first. The reason it works this way is because if you've set up two-way data bindings you get a circular event: the input changes, which sets the bound model property, which in turn updates the value of the input. This might sound OK but results in the cursor always jumping to the end of the input/textarea. So if you're editing the middle of a bound text field, the cursor keeps jumping to the end. We avoid this by making sure it's not already in focus thus avoiding the bad loop.

'model.key': {
    type: 'value',
    selector: '#something', // or hook
}

booleanClass

add/removes class based on boolean interpretation of property name. name, yes, or no can also be an array of class names where all the values will be toggled. If you need the opposite effect, (false adds class, true removes class), specify invert: true.

'model.active': {
    type: 'booleanClass',
    selector: '#something', // or hook
    // to specify name of class to toggle (if different than key name)
    // you could either specify a name
    name: 'active'
    // or a yes/no case
    yes: 'active',
    no: 'not-active'
    // if you need inverse interpretation
    invert: true
}

booleanAttribute

toggles whole attribute on the element (think checked) based on boolean interpretation of property name. name can also be an array of attribute names where all the values will be toggled. If you need the opposite effect, (false adds attribute, true removes attribute), specify invert: true.

'model.isAwesome': {
    type: 'booleanAttribute',
    selector: '#something', // or hook
    // to specify name of attribute to toggle (if different than key name)
    // you could either specify a name
    name: 'checked'
    // or a yes/no case
    yes: 'data-is-awesome',
    no: 'data-is-not-awesome'
    // if you need inverse interpretation
    invert: true
}

toggle

toggles visibility (using display: none by default) of entire element based on boolean interpretation of property.

// simple show/hide of single element
'model.key': {
    type: 'toggle',
    selector: '#something' // or hook
}

// Inverse interpretation of value
'model.key': {
    type: 'toggle',
    invert: true,
    hook: 'some-element'
}

// toggle visibility property instead
'model.key': {
    type: 'toggle',
    selector: '#something', // or hook
    mode: 'visibility'
}

// show/hide where true/false show different things
'model.key': {
    type: 'toggle',
    yes: '#true_case',
    no: '#false_case'
}

switch

Toggles existence of multiple items based on value of property.

'model.activetab': {
    type: 'switch',
    cases: {
        'edit': '#edit_tab',
        'new': '#new_tab',
        'details': '#details_tab'
    }
}

switchClass

Toggles existence of a class on multiple elements based on value of property.

'model.key': {
    type: 'switchClass',
    name: 'is-active',
    cases: {
        'edit': '#edit_tab',
        'new': '#new_tab',
        'details': '#details_tab'
    }
}

switchAttribute

Sets attribute(s) on matching elements based on the value of a property matching the case.

'model.key': {
    type: 'switchAttribute',
    selector: 'a', // or hook
    name: 'href',  // name defaults to the property name (e.g. 'key' from 'model.key' in this example)
    cases: {
        value1: '/foo',
        value2: '/bar'
    }
}

You can also specify multiple attributes by using an object as the case value. The object keys are used instead of the name option.

'model.key': {
    type: 'switchAttribute',
    selector: 'a', // or hook
    cases: {
        value1: { href: '/foo', name: 'foo' },
        value2: { href: '/bar', name: 'bar' }
    }
}

innerHTML

renders innerHTML, can be a string or DOM, based on property value of model

'model.key': {
    type: 'innerHTML',
    selector: '#something' // or hook
}

custom functions

type can also be a function. It will be run for each matching el with the value and previousValue of the property. The function is bound to the view declaring the bindings, so this refers to the view.

'model.key': {
    type: function (el, value, previousValue) {
        // Do something custom to el
        // using value and/or previousValue
    },
    selector: '#something', // or hook
}

Handling multiple bindings for a given key

If given an array, then treat each contained item as separate binding

'model.key': [
    {
        type: 'booleanClass',
        selector: '#something', // or hook
        name: 'active' // (optional) name of class to toggle if different than key name
    },
    {
        type: 'attribute',
        selector: '#something', // or hook
        name: 'width'
    }
]

The attribute, booleanAttribute and booleanClass types also accept an array for the name property (and yes/no for booleanClass). All the values in the array will be set the same as if each were bound separately.

'model.key': {
    // Also works with booleanAttribute and booleanClass
    type: 'attribute',
    selector: '#avatar',
    // Both height and width will be bound to model.key
    name: ['height', 'width']
}

binding using data-hook attribute

We've started using this convention a lot, rather than using classes and IDs in JS to select elements within a view, we use the data-hook attribute. This lets designers edit templates without fear of breaking something by changing a class. It works wonderfully, but the only thing that sucks about that is the syntax of attribute selectors: [data-hook=some-hook] is a bit annoying to type a million types, and also in JS-land when coding and we see [ we always assume arrays.

So for each of these bindings you can either use selector or hook, so these two would be equivalent:

'model.key': {
    selector: '[data-hook=my-element]'
}

'model.key': {
    hook: 'my-element'
}

handling simplest cases: text

'model.key': '#something' // creates `text` binding for that selector and property

// `type` defaults to `text` so we can also do
'model.key': {
    hook: 'hook-name'
}

real life example

var View = require('ampersand-view');
var templates = require('../templates');


module.exports = View.extend({
    template: templates.includes.app,
    bindings: {
        'model.client_name': {
            hook: 'name'
        },
        'model.logo_uri': {
            type: 'attribute',
            name: 'src',
            hook: 'icon'
        }
    }
});

other benefits

Previously after having given views the ability to have their own properties (since view inherits from state) it was awkward to bind those to the DOM. Also, for binding things that were not just this.model the syntax had to change.

Now this is fairly simple/obvious:

module.exports = View.extend({
    template: templates.includes.app,
    props: {
        activetab: 'string',
        person: 'state',
        meeting: 'state'
    },
    bindings: {
        // for the property that's directly on the view
        'activetab': {
            type: 'switch',
            cases: {
                'edit': '#edit_tab',
                'new': '#new_tab',
                'details': '#details_tab'
            }
        },
        // this one is for one model
        'person.full_name': '[data-hook=name]',
        // this one is for another model
        'meeting.subject': '[data-hook=subject]'
    }
});

firstMatchOnly

As an option you can add firstMatchOnly: true to the binding declaration. It will cause the selector matcher to grab only the first match.

Useful for cases when a view renders a collection with several elements with the same class/data-hook.

module.exports = View.extend({
  template: '<div><span data-hook="foo"></span><span data-hook="foo"></span>',
  props: {
    someText: 'string'
  },
  initialize: function(){
    this.someText = 'hello';
  },
  bindings: {
    'someText': {
      type: 'text',
      hook: 'foo',
      firstMatchOnly: true
    }
  }
});
// will render <div><span data-hook="foo">hello</span><span data-hook="foo"></span></div>

changelog

  • 3.3.1 - Fix issues with yes/no handling in boolean class. Add lots of tests.

license

MIT

ampersand-dom-bindings's People

Contributors

bear avatar cgastrell avatar chesles avatar dhritzkiv avatar henrikjoreteg avatar kylefarris avatar latentflip avatar lukekarrys avatar pgilad avatar remixz avatar remko avatar sansthesis avatar spencerbyw avatar wraithgar avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ampersand-dom-bindings's Issues

Visibility binding types

Something i came across a few times is wanting to change the visibility of elements based on a boolean attribute. I'm currently using booleanClass for this, but it would be handy if i didn't need to create a class.

In some cases, i need 'display: none' to be turned on, in some cases 'visibility: hidden'.

Update input value on blur

I understand that type:value works 1-way.
I understand that it does not update the .value of the input when the control is focused.
This leads to situations where model has already new value, but this change is still not propagated to the DOM because user "blocks" it by keeping cursor on the field.
I expected the DOM to get updated once user moves the focus out of the input, but actually nothing happens leading to inconsitency between the model and the view.

A motivating example is this: I have a textbox where user can type in a number or an expression (like "2+2"). I have "change input" event handler which sets the model.x to the value of the expression (in this case 4). If the user types in "2+2" and click outside the textbox then everything works fine, because the "change" event fires once the focus is already outside the box, so the new value can be propageted to DOM. On the other hand if the user press the enter key on the keyboard, which also triggers the "change input", then the story is much different.
The model.x gets updated to 4, but the since the cursor is still in the inputbox, the DOM is not updated, and still shows "2+2". Now, the bad state is permanent: even if the user clicks outside of the input box.

I guess that another use case would be in situation where the change to the model happens "from the outside" - asynchronous timer, XHR, or some other event can cause model to change while the focus is on the input box.

One possible solution would be to add a "blur input" handler and refresh the binding.
There are some problems with that:

  • one has to guess that this is necessary (which is not obvious from the spec)
  • one has to figure out how to use blur event correctly (there are some problems with bubbling of the blur event)
  • one has to figure out how to "refresh the binding" (the spec does not mention how to force changes in the model to be flushed to the DOM)

Add yes/no support to booleanAttribute

I'd like to be able to use yes/no like booleanClass supports when using booleanAttribute bindings.

My use case would be something like this:

'model.isEnabled':
  type: 'booleanAttribute'
  no: 'disabled'
  hook: 'submitButton'

Attribute `indeterminate` is not correctly toggled

'model.checkAllState': {
    type: 'switchAttribute',
    selector: '[data-hook=check_all] input[type=checkbox]',
    cases: {
        checked: {checked: null},
        unchecked: {},
        indeterminate: {indeterminate: null}
    }
}

It correctly adds the indeterminate attribute to the input checkbox, however, it seems you must use javascript to set the property el.indeterminate = true otherwise it does not correctly trigger indeterminate state. Does ampersand need some notion of attribute vs. property (like jquery does with .prop and .attr)?

Obvious workaround (and what I've done) is a custom binding function, but it be nice to have the above working correctly to keep the code succinct.

For anyone curious. Here's my custom model and binding:

session: {
    checkAllState: {
        type: 'string',
        values: ['checked', 'indeterminate', 'unchecked'],
        default: 'unchecked'
    }
}

'model.checkAllState': {
    selector: '[data-hook=check_all] input[type=checkbox]',
    type: function (el, value, previousValue) {
        if (value === 'indeterminate') {
            el.indeterminate = true;
            el.checked = false;
        } else if (value === 'unchecked') {
            el.indeterminate = false;
            el.checked = false
        } else if (value === 'checked') {
            el.indeterminate = false;
            el.checked = true;
        }
    }
}

Tested in the latest version of Chrome.

new binding type: switchAttribute

Similar to switchClass, this binding would set/unset the attribute based on whether the case matches, rather than the literal value of the property:

bindings: {
    'model.prop': {
      type: 'switchAttribute',
      selector: 'a', // or hook
      name: 'href',
      cases: {
        'value1': '/foo',
        'value2': '/bar'
      }
    }
}

It would also be useful to be able to set multiple attributes at once with something like:

cases: {
  'value1': { href: '/foo', name: 'foo' },
  'value2': { href: '/bar', name: 'bar' }
}

Binding with innerHTML didn't fire script tag

Possible edge case here ๐Ÿ˜‰

We were getting a Tweet's embed code form the Twitter API which sends back some HTML you can put in your page. When we using the innerHTML binding with a view, the HTML was placed as expected, but the script tag contained in the HTML didn't fire.

Example HTML sent back from Twitter:

<blockquote class="twitter-tweet" align="center" width="350">
<p>From Madrid to the Int&#39;l Space Station, eyes turn to the Supermoon on Sunday - PHOTOS: <a href="http://t.co/HJ25f9KrUV">http://t.co/HJ25f9KrUV</a> <a href="http://t.co/Om3ujf4ldn">pic.twitter.com/Om3ujf4ldn</a></p>&mdash; ABC News (@ABC) <a href="https://twitter.com/ABC/statuses/499065279360430080">August 12, 2014</a>
</blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

I ended up creating a session property on the view and listeningTo it for changes, and then setting the HTML content with jQuery via $(...).html(...), which worked.

I quickly checked the source in this repo and in ampersand-dom and didn't see anything that would cause this. Maybe I missed it.

type:attribute, name:value doesnt work for selects

The following works in plain JS to visually change the selected option:

document.getElementById('select').value = 'name';

But, with a binding like this:

{
  type: 'attribute',
  name: 'value',
  selector: '#select'
}

it doesn't work. I think this is because behind the scenes it uses setAttribute('value', value) which doesnt change the select visually in the browser.

Only apply bindings if element is not currently focused

'value' bindings occur only when element being set is currently not focused. I understand that the check is enforced to avoid cyclic loops. However, it leaves the state value and the DOM element value in an inconsistent state. This is not good.
In this situation, is it possible to trigger an event to let the developer handle it in any relevant manner? I think it will provide a means to bring the state and DOM element back to a consistent state. My quarter cent.

Can't select root element

If you try to select the root element in the template (even if you've assigned a role to the root element), you have to specify selector: '' to make it work.

"falsy" values for attribute bindings act as true for certain attributes

For certain html attributes (e.g. disabled, hidden, etc.) the presence of the attribute's key is enough to consider it true and enable it on the element.

For example, say we have a model with properties that look like:

{
    props: {
        disabled: "boolean"
    }
}

and bindings on a view that look like:

{
    bindings: {
        "model.disabled": {
            type: "attribute",
            name: "disabled",
            hook: "button"
        }
    }
}

the [data-hook=button] element will be disabled if model.disabled == true or model.disabled == false, since the ampersand-com-bindings module leaves the attribute on the element no matter what.

model.disabled == true

<button data-hook="button" disabled="true">I should be disabled</button>

model.disabled == false

<button data-hook="button" disabled>I shouldn't be disabled, but I am</button>

Is there any way to add special cases to the attribute binding type? Or remove the attribute all together when falsy?

Type: class to support some kind of mapping or parse function

I came across an issue where my model has a "status" property that can be "phase1", "phase2" or "phase3" and my classes are equal but prepend "widget-". When I try to use the bindings to change the class of the view element there is no way to prepend, append or parse the class name.

As a work around I had to poison my models with class properties. In my opinion models should not contain any CSS information so i propose the following syntax to make my usage possible without modifying the models:

bindings: {
            'model.status' : {
                type: 'class'
                parse: function(status){
                        return "widget-" + class;
                }
            }
}

arrays for booleanAttributes would be nice

This syntax would be helpful in keeping certain things not quite so verbose:

'model.isDisabled': [{
    type: 'booleanAttribute',
    name: ['disabled', 'readOnly'],
    role: 'latitude'
}, {
    type: 'booleanAttribute',
    name: ['disabled', 'readOnly'],
    role: 'longitude'
}]

Style properties

When working with ampersand-dom-bindings I came accros an issue where I want to change the width of a block element. I had to generate a style like string "width: " + percentage + "%;" to tackle the use case. It would be handy to have ampersand-dom-bindings to also be able to change CSS aka Style properties directly.

I am not sure if this should be covered in the ampersand-dom repo.

class binding should support an array of classes

The Issue

booleanClass and switchClass can both take an array of class names to apply, but if you provide an array of names in a type: class you end up with a comma-separated string (i.e. the default array toString).

As an example:

session: {
  fixed: 'boolean',
  classes: { type: 'array', default: function() { return ['class1', 'class2'] } }
},
bindings: {
  fixed: { type: 'booleanClass', name: ['fixed', 'fixed-top'] }
  classes: { type: 'class' }
}

The binding for fixed works as expected, setting both classes when the property is true. The classes binding ends up with the class attribute set to class="class1,class2". Alternatively, if the classes property were a string with spaces, the browser throws an exception:

...
  classes: { type: 'string', default: 'class1 class2'}
...

Uncaught InvalidCharacterError: Failed to execute 'add' on 'DOMTokenList': The token provided ('c1 c2') contains HTML space characters, which are not valid in tokens. ampersand-dom.js:10

Currently as far as I can tell it is impossible to set multiple classes based on an array attribute using a type='class' binding. A binding with type='attribute', name='class' works, but will overwrites any other classes, which isn't ideal.

The motivation

My use-case for this is a reusable view, where classes on some of the elements are customizable and can in some cases be arrays. The view takes a state object that has session properties used to customize text, classes and other attributes of the view.

Changes needed

From what I can tell, this is actually a change that can be made in ampersand-dom - if switchClass supports arrays (via addClass and removeClass supporting arrays), this should just work without any changes to ampersand-dom-bindings.

Binding with non-dot-friendly properties.

I noticed that when trying to bind something like this doesn't work:

...
bindings: {
  'model.something["not-dot-friendly"]': '...'
}
...

This however did work:

...
bindings: {
  'model.something.not-dot-friendly': '...'
}
...

"Toggle" type requires both "yes" and "no" options

I noticed that when using the toggle binding type, and when specifying yes/no options, you must specify both yes AND no options. Would it be possible to only supply yes OR no? I find it useful to show specific content when a value is falsy.

I found a work around (below) that works by passing a fake selector to the yes option.

"model.boolean": [
    {
        type: "toggle",
        no: "#incomplete",
        yes: "#fake"
    }
]

Custom binding types

Hi,

Much like dataTypes for ampersand-state, it would be awesome to be able to specify a bindingTypes property in order to add custom types.
In my use case I would like something similar to booleanClass but for an attribute (i.e. booleanAttribute).
With the current API I'm unable to customize this.

What do you think about adding such a feature?
Thanks!

Binding to a derived property does not update the DOM when the derived value changes

From the me.js model:

    props: {
        id: ['string'],
        email: ['string', true, '']
    },
    derived: {
        loggedIn: {
            cache: false,
            fn: function () {
                return !!$.trim(app.me.email);
            }
        }
    }

From the main.js view:

    bindings: {
        'model.loggedIn': {
            type: 'switch',
            cases: {
                false: '[data-hook=nav-login]',
                true: '[data-hook=nav-logout]'
            }
        }
    }

From the body.jade template:

        li(data-hook="nav-login"): a(href="/login") Login
        li(data-hook="nav-logout"): a(href="/logout") Logout
  • The view renders the DOM correctly if I refresh the page, but updates to loggedIn do not update the DOM
  • The DOM updates as expected if I bind to a regular prop suchas email and then use app.me.set("email", true/false) to test in the browser console.
  • I've tried setting cache: false to the derived property but it does not change the results of my testing.
  • I've tried using me.loggedIn instead of model.loggedIn, but that causes the DOM to hide both links instead of switching. In fact, I'm still confused as to why I've had partial success with model.loggedIn when me.loggedIn makes more sense to me. I assume I lack some basic understanding, but reading the documentation hasn't helped clear it up for me.

I've done my best to narrow things down, but I am not an Ampersand wizard. If my approach is poor, please let me know.

Borrow features from Backbone.Epoxy

Been working on a Backbone app for the last year, and Ampersand looks like it solves a lot of the issues I've run into. I've been using the Backbone.Epoxy plugin, which has enhanced Model and View classes.

Epoxy.View has a lot of very useful abilities, and I think it would be helpful if Ampersand-Dom-Bindings borrowed a few of those. The biggest ones I see are:

  • More binding types. Epoxy includes several binding types (which it refers to as "handlers") for form fields, checkboxes, and option dropdowns, as well as a "css" handler. Note that several of Epoxy's handlers are trivial implementations using jQuery. Also, one none-Epoxy binding type suggestion: a binding that will swap classes based on the value of a model attribute, not just its truthiness. I see a "switchClass()" function in Ampersand-Dom, so that would seem pretty simple to implement.
  • Epoxy also has "binding filters", which allow various formatting of values after they are retrieved from a model.
  • Most importantly, EXPANDABILITY. Epoxy comes with a variety of handlers and filters included, but custom handlers and filters can be defined either locally in your Epoxy.View subclass, or added to the global list. Being able to define your own custom binding handlers is extremely helpful.
  • Finally, Epoxy has a custom mini-language for defining bindings. Example:
<a data-bind="text:linkText,attr:{href:linkUrl,title:linkText}"></a>

If you look way down under the hood, it's actually using the JS "with()" syntax to do dynamic lookup of model fields, which is probably a little too magical. Still, the shorthand syntax is kind of nice.

pass binding context through to custom binding functions

This is a follow up to the solution implemented for #22.

Any chance we could get the binding context passed through to the custom binding function too? I need to do something like the following:

var bindingToggleFormField = function(el, value, previousValue, bindingContext) {
  var fieldName = el.getAttribute('data-hook');
  if (this._fieldViews[fieldName]) {
    var newValue = value && bindingContext.showWhen;
    this._fieldViews[fieldName].set('isVisible', newValue);
  }
}
...
bindings: {
  isFoo: {
    type: bindingToggleFormField,
    hook: 'barField',
    showWhen: [true || false]
  }
}

props: {
  isFoo: 'boolean'
}

FWIW in the short term I implemented this kind of thing:

var bindingToggleFormField = {
  showWhenTrue: function(el, value, previousValue) {
    var fieldName = el.getAttribute('data-hook');
    if (this._fieldViews[fieldName]) {
      this._fieldViews[fieldName].set('isVisible', value);
    }
  },
  showWhenFalse: function(el, value, previousValue) {
    var fieldName = el.getAttribute('data-hook');
    if (this._fieldViews[fieldName]) {
      this._fieldViews[fieldName].set('isVisible', !value);
    }
  }
}
...
bindings: {
  isFoo: {
    type: bindingToggleFormField.showWhenTrue || bindingToggleFormField.showWhenFalse,
    hook: 'barField'
  }
}

... which gets the job done, but is less elegant.

Thanks!

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.