Short Question:
How to create a <input type="text"> which contains a custom-format string serialization of an object in a way that editing the string updates the model and vice versa?
I think AngularJS’ directives are the way to go, but i can’t get it pinned down.
Long Question:
Prequel
I have a object which is my application’s “master model”. it can be serialized to a string of a specific format:
it has 2-3 attributes, whose serializations are joined by “;” (no trailing “;” if the third is missing)
attributes 2 and 3 are lists of objects, and serialized by joining those with “,”.
the serialization of the objects is just one of their string attributes, or two ow them with “x” between.
so i have a constructor (accepting a spec string), and a toString function. Following; the latter for clarity:
World.prototype.toString = function() {
var spec = [];
spec[0] = this.version;
spec[1] = this.layers.map(function(layer) {
var c = (layer.c > 1) ? layer.c + 'x' : '';
return c + layer.id; //e.g. 'T' or '2xT'
}).join(',');
//spec[2] in python: ','.join(option.id for option in options if option.checked)
var options = this.options.filter(function(option) {
return option.checked;
});
if (options.length > 0)
spec[2] = options.map(function(option) {
return option.id;
}).join(',');
return spec.join(';');
};
The directive i tried to use looks thusly, but the $watch only fires once.
angular.module('layersApp', []).directive('spec', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch('world', function(val) {
element.val(val.toString());
console.log('object updated', element.val());
}, true);
element.blur(function(e) {
scope.world = new World(element.val());
});
}
};
});
Actual long question
What i want is an easy way to make this work,
<input type="text" data-ng-model="theWorld" spec>
where spec is the custom directive shown above, setting up the two-way binding
Outlook
it would be awesome if this could result in a generic “serialization” directive used like that:
<input type="text" data-serialization="string2Foo, foo2String" data-ng-model="foo">
Which would look up the object foo, and the functions string2Foo and foo2String to setup custom (de)serialization.
I think you can use of $parsers and $filters of ngModel controller.
Here is the simplest example of doing it.
http://plnkr.co/edit/13PJN2
It should be easy to add validation, too.
I tried to make it accept custom serializer from parent scope, but failed to do so. Not sure about it.
Related
I try to implement a custom directive that list all available plans and allow user to choose one.
When plan is selected, the parent scope must be updated with selected object (it is linked with two way binding)
It should behave exactly the same as angular ng-options does, but I have hard time fighting the Javascript object replacement.
What I have right now works (with some clutter removed):
In HTML:
<choose-plan ng-model='plan' plans='plans' choose-plan-title='Premium plans' />
In Controller:
$scope.plan = {}
Plans.get (resource) ->
$scope.plans = resource.plans
return
It does not work with $scope.plan = undefined obviously, but I look for the solution that does.
In JS (Coffeescript) directive:
angular.module('tv-dashboard').directive 'choosePlan', [
'lodash'
(lodash) ->
'use strict'
restrict: 'E'
scope:
plan: '=ngModel'
plan_collection: '=plans'
title: '#choosePlanTitle'
link: (scope, element, attrs) ->
# HACK two way binding does not replace the value. Investigate
scope.choosePlan = (available_plan) ->
# scope.plan = available_plan # Does NOT update the parent scope binded ng-model
angular.copy available_plan, scope.plan
return
scope.isSelected = (available_plan) ->
return unless available_plan?
available_plan.id == scope.plan.id
presentPlans = (collection) ->
angular.copy(collection).map (resource_plan) ->
price_parts = resource_plan.interval_price.split '.'
resource_plan['integer_price'] = price_parts[0]
resource_plan['decimal_price'] = price_parts[1]
resource_plan
chooseDefaultPlan = (collection) ->
scope.choosePlan lodash.last collection
unWatchCollection = scope.$watch 'plan_collection', (collection) ->
return unless collection? && collection.length > 0
scope.plans = presentPlans collection
chooseDefaultPlan scope.plans
unWatchCollection()
return
return
templateUrl: 'form/choose_plan.html'
]
But if you take a look on the isSelected function, you notice that i have to compare objects using the id field. Comparison by == (=== in JS) does not return true.
Is there a way I can replace the parent scope plan without dancing around with angular.copy available_plan, scope.plan and forced to use the id field comparison?
You should use require: 'ngModel' to inject the model attached to the element or its parent element on which the directive is bound to. Here is a demo.
One way to solve this is to share an object and write into its properties
$scope.plan = {
// value: set in directive
}
scope.choosePlan = (available_plan) ->
scope.plan.value = available_plan
return
scope.isSelected = (available_plan) ->
return unless available_plan?
available_plan == scope.plan.value
Your code as jsfiddle: http://jsfiddle.net/ht6tmhfu/1/
Fixed code as jsfiddle: http://jsfiddle.net/ht6tmhfu/3/
I want to convert lenghts from mm to ft and inches or vice versa. User can input either mm or ft&in.The case is I always want to save data in mm to database but use angular to see it in both format.
I have already created a solution for it. http://plnkr.co/edit/thgx8vjsjxwfx6cLVVn1?p=preview
But it is using ng-change everytime to convert the values.
I was wondering if some angular expert has better idea of doing similar stuff. Please note that I am only planning to save single value $scope.lengthmm
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
// $scope.lengthtodb = 0;
$scope.mmToFtIn =function(){
toInch = $scope.lengthmm * 0.0393701;
toFt = toInch/12;
OutputFT = Math.floor(toFt);
OutputInch = (toFt - Math.floor(toFt))*12;
$scope.lengthft = OutputFT;
$scope.lengthin = OutputInch;
$scope.Result1 = OutputFT + "ft" + OutputInch + "Inches";
};
$scope.FtInTomm =function(){
tomm = (($scope.lengthft *12)+ $scope.lengthin)*25.4;
$scope.lengthmm = tomm;
$scope.Result2 = tomm;
};
});
In addition, as there will be lots of fields using the the same logic maybe method mmToFTIn needs to split in two methods to bind ft and inches separately. But I am really looking forward to see a smarted solution.
Formatting the Output onto the view is best done with filters.
JS:
app.filter('inchFilter', function() {
return function(input) {
return Math.floor(input * 0.0393701);
};
});
HTML:
<input name="mm" type="text" value="{{lengthmm | inchFilter}}">
Edit:
For a more complete and powerful solution I extended the plunker with a directive to now allow two-way-binding on the non-metric fields aswell.
app.directive('enhancedInput', function($parse){
return {
restrict: 'A',
require: 'ngModel',
link : function(scope, element, attr, ngModelCtrl){
ngModelCtrl.$parsers.unshift(scope.$eval(attr.fromMm));
ngModelCtrl.$formatters.unshift(scope.$eval(attr.toMm));
}
};
});
This is achieved by first "requiring" the ngModelController and then using its $parsers and $formatters to intercept communication in between model and view.
plunker
One option is to create a $watch on the millimeter variable and then update the inches model. This is probably a better option than ng-change because the $watch will fire any time the variable is changed within the scope, not just when the input to your text box changes.
I have created an example for you. While you state you only wish to use one variable, I believe using a second variable in this manner is really the better approach. Alternatively you could use an object or an array to maintain a single variable while storing two values.
For this example I defined the variables as follows
$scope.inches = 0;
$scope.mm = 0;
Then we just create a watch on both variables. To reiterate, this function gets called any time there is a change to the mm variable which allows you to ensure that the relationship between mm and inches is maintained.
$scope.$watch('mm', function(newVal, oldVal) {
$scope.inches = newVal / 25.4;
});
You could also create a custom filter as follows... this might even be a better solution if you only need to display mm in a formatted manner (as opposed to using it for calculations or something else).
angular.module('myFilters', []).filter('mmToInches', function() {
return (function (input) {
return (input / 25.4);
});
});
Then later on do...
Inches Filter : {{ mm | mmToInches }}
You can add arguments to filters to a single filter can do both conversions.
angular.module('myFilters', []).filter('convert', function() {
return (function (input, type) {
if (type == 'mm2inch')
{
return (input / 25.4);
} else if (type == 'inch2mm') {
return (input * 25.4);
}
});
});
Inches Filter : {{ mm | convert:'mm2inch' }}
MM Filter : {{ inches | convert:'inch2mm' }}
Here is an improvement I made to Christoph Hegemann's answer.
The directive only uses one attribute, which points to a scope object inchesShowFeet (you can call it anything you want).
<input type="text" ng-model="data.beamLength"
ng-enhanced-input="inchesShowFeet"
ng-model-options="{ debounce: 500 }" />
The directive itself, as he mentioned, uses the ng-model attribute's parsers
app.directive('ngEnhancedInput', function($parse){
return {
restrict: 'A',
require: 'ngModel',
link : function(scope, element, attr, ngModelCtrl){
ngModelCtrl.$parsers.unshift(scope.$eval(attr.ngEnhancedInput).store);
ngModelCtrl.$formatters.unshift(scope.$eval(attr.ngEnhancedInput).show);
}
};
});
The object, usually set in the controller, has a show and a store function. The name is funny, but it's the best I could think of. The one returns the value to show in the text box, the other one returns the value to store in the model.
$scope.inchesShowFeet = {
show: function(val){ return val / 12; },
store: function(val){ return val * 12; }
};
So, say I have 25 inches stored in my model. The show function get's called with a val of 25. It divides it by 12 and returns it, which get's displayed in the text box. When you type in 4, the store function get's called with a val of 4. It multiplies it by 12 and returns 48, which get's stored in the model.
Let us explain the question with an example. I have a text box. The textbox (every textbox) has a property called 'value'. I want to over ride that textbox.value and comeup with and
new thing. When the text in textbox is 'ranjan' then the textbox.VALUE property returns 'ranjan'. Now I want to thus overwrite this so that when you type textbox.VALUE you get a different thing say for example, RaNjAn or say, Mr. Ranjan or whatever.
We can over ride methods using Object.PROTOTYPE property. But how can we do it for non-function objects inside object for example the 'value' property in this case.
If i need to make the question more clear, please mention.
Regards - Ranjan.
You can define custom properties for your element using Object.defineProperty
If you have a case where you need to get the value of an element as Mr. <value> for example, then this approach will be useful. Overriding standard properties may not be such a good idea.
Demo: http://jsfiddle.net/zvCGw/2/
Code:
var foo = document.getElementById('foo');
Object.defineProperty(foo, "xvalue", {
get: function() {
return 'Mr. ' + foo.value;
},
set: function(_newValue) {
foo.value = _newValue;
}
});
foo.xvalue = 'Hello';
alert(foo.xvalue);
What you are trying to do is called type augmentation. In javscript there are types of things, such as the object type, array type, etc.
You can use the prototype to augment these built in types, for example, adding a new method that can be called on any object that is of the type array:
Array.prototype.myNewMethod = function() {
//the method logic
}
Then you can call your method on any array:
[0,1,2].myNewMethod();
There is no INPUT type in JavaScript, DOM elements are classed as Objects. But you could jerry-rig something together that kind of does what you need, like this
Object.prototype.changeValue = function(el) {
if (el.tagName === "INPUT") {
return "Mr " + el.value;
}
}
var testEl = document.getElementById("test");
document.write(testEl.changeValue(testEl))
Used in conjunction with this textbox:
<input id="test" value="Dan" />
You would then get the output 'Mr Dan'
However, this is not great, it's just to illustrate the point and is just something to get you started...
I made a fiddle so you can play around with it
You can redeclare value but it will do no good ;)
This example would do that if test is a textbox
var input = document.getElementById("test");
Object.defineProperty(input, "value", {
get : function () {
return "'" + this["value"] + "'";
},
set : function (val) {
this["value"] = val;
}
});
input.value = "Hello World";
alert(input.value);
Unfortunately, "this.value" will reference the getter causing infinite recursion.
Once redefined, the original value will no longer exist so you will have crippled the element object.
At least as far as I have been able to test.
If the property you're trying to override can also be represented by an HTML attribute (e.g. an input's value), then you can use getAttribute and setAttribute.
Object.defineProperty(myInputElement, 'value', {
get: function () {
return myInputElement.getAttribute('value');
},
set: function (value) {
myInputElement.setAttribute('value', value);
}
});
Note, however, that this override itself cannot be overridden without re-implementing it.
I want to have a Backbone model with float attributes in it but without worrying too much about variable types.
I would like to encapsulate the value parsing right there in the model so I am thinking of overriding the set function:
var Place = Backbone.Model.extend({
set: function(attributes, options) {
if (!_.isEmpty(attributes.latitude)){
attributes.latitude == parseFloat(attributes.latitude);
}
if (!_.isEmpty(attributes.longitude)){
attributes.longitude == parseFloat(attributes.longitude);
}
Backbone.Model.prototype.set.call(this, attributes, options);
}
});
However this seems cumbersome, since I would have a similar logic in the validate method and potentially repeated across multiple models. I don't think the View should take care of these conversions.
So what is the best way of doing it?
Use a validation plugin for your model so that you can validate the input in a generic fashion.
There are several out there including one that I have written:
Backbone.Validator
Backbone.Validation
Then you don't worry about performing data validation anywhere else - your model does it and sends out and error message you can listen for and provide appropriate feedback.
Also, a lat/lng pair can, in rare circumstances, be an integer, such as Greenwich England: 0,0 or the north pole: 90,180. And since JavaScript only has "number" any valid input for parseFloat is also valid for parseInt.
But parseFloat will always return a float.
My solution was to replace Backbone.Model.prototype.set with a preprocessor proxy:
/**
* Intercept calls to Backbone.Model.set and preprocess attribute values.
*
* If the model has a <code>preprocess</code> property, that property will be
* used for mapping attribute names to preprocessor functions. This is useful
* for automatically converting strings to numbers, for instance.
*
* #param Backbone
* the global Backbone object.
*/
(function(Backbone) {
var originalSet = Backbone.Model.prototype.set;
_.extend(Backbone.Model.prototype, {
set: function(key, val, options) {
if(!this.preprocess) {
return originalSet.apply(this, arguments);
}
// If-else copied from Backbone source
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
for(attr in this.preprocess) {
if(_.has(attrs, attr)) {
attrs[attr] = this.preprocess[attr](attrs[attr]);
}
}
return originalSet.call(this, attrs, options);
},
});
})(Backbone);
After this, models with a preprocess property will use it to map attribute names to preprocessor functions. For instance, preprocess: { age: parseInt } means that whenever the age attribute is set, the value will be passed through parseInt before actually setting it. Attributes with no corresponding preprocess entry will not be affected.
Example usage:
var Thing = Backbone.Model.extend({
preprocess: {
mass: parseInt,
created: function(s) { return new Date(s); },
},
});
var t = new Thing({
label: '42',
mass: '42',
created: '1971-02-03T12:13:14+02:00',
});
console.log(t.get('label')+3); // 423
console.log(t.get('mass')+3); // 45
console.log(t.get('created').toLocaleString('ja-JP', { weekday: 'short' })); // 水
Pros
The functionality is available in all models without needing to duplicate code
No need to send { validate: true } in every call to set
No need to duplicate preprocessing in validate, since this happens before validate is called (this might also be a con, se below)
Cons
Some duplication of Backbone code
Might break validation since preprocessing happens before validate is called. JavaScript parsing methods usually return invalid values instead of throwing exceptions, though (i.e. parseInt('foo') returns NaN), so you should be able to detect that instead.
Given an arbitrary HTML element with zero or more data-* attributes, how can one retrieve a list of key-value pairs for the data.
E.g. given this:
<div id='prod' data-id='10' data-cat='toy' data-cid='42'>blah</div>
I would like to be able to programmatically retrieve this:
{ "id":10, "cat":"toy", "cid":42 }
Using jQuery (v1.4.3), accessing the individual bits of data using $.data() is simple if the keys are known in advance, but it is not obvious how one can do so with arbitrary sets of data.
I'm looking for a 'simple' jQuery solution if one exists, but would not mind a lower level approach otherwise. I had a go at trying to to parse $('#prod').attributes but my lack of javascript-fu is letting me down.
update
customdata does what I need. However, including a jQuery plugin just for a fraction of its functionality seemed like an overkill.
Eyeballing the source helped me fix my own code (and improved my javascript-fu).
Here's the solution I came up with:
function getDataAttributes(node) {
var d = {},
re_dataAttr = /^data\-(.+)$/;
$.each(node.get(0).attributes, function(index, attr) {
if (re_dataAttr.test(attr.nodeName)) {
var key = attr.nodeName.match(re_dataAttr)[1];
d[key] = attr.nodeValue;
}
});
return d;
}
update 2
As demonstrated in the accepted answer, the solution is trivial with jQuery (>=1.4.4). $('#prod').data() would return the required data dict.
Actually, if you're working with jQuery, as of version 1.4.3 1.4.4 (because of the bug as mentioned in the comments below), data-* attributes are supported through .data():
As of jQuery 1.4.3 HTML 5 data-
attributes will be automatically
pulled in to jQuery's data object.
Note that strings are left intact
while JavaScript values are converted
to their associated value (this
includes booleans, numbers, objects,
arrays, and null). The data-
attributes are pulled in the first
time the data property is accessed and
then are no longer accessed or mutated
(all data values are then stored
internally in jQuery).
The jQuery.fn.data function will return all of the data- attribute inside an object as key-value pairs, with the key being the part of the attribute name after data- and the value being the value of that attribute after being converted following the rules stated above.
I've also created a simple demo if that doesn't convince you: http://jsfiddle.net/yijiang/WVfSg/
A pure JavaScript solution ought to be offered as well, as the solution is not difficult:
var a = [].filter.call(el.attributes, function(at) { return /^data-/.test(at.name); });
This gives an array of attribute objects, which have name and value properties:
if (a.length) {
var firstAttributeName = a[0].name;
var firstAttributeValue = a[0].value;
}
Edit: To take it a step further, you can get a dictionary by iterating the attributes and populating a data object:
var data = {};
[].forEach.call(el.attributes, function(attr) {
if (/^data-/.test(attr.name)) {
var camelCaseName = attr.name.substr(5).replace(/-(.)/g, function ($0, $1) {
return $1.toUpperCase();
});
data[camelCaseName] = attr.value;
}
});
You could then access the value of, for example, data-my-value="2" as data.myValue;
jsfiddle.net/3KFYf/33
Edit: If you wanted to set data attributes on your element programmatically from an object, you could:
Object.keys(data).forEach(function(key) {
var attrName = "data-" + key.replace(/[A-Z]/g, function($0) {
return "-" + $0.toLowerCase();
});
el.setAttribute(attrName, data[key]);
});
jsfiddle.net/3KFYf/34
EDIT: If you are using babel or TypeScript, or coding only for es6 browsers, this is a nice place to use es6 arrow functions, and shorten the code a bit:
var a = [].filter.call(el.attributes, at => /^data-/.test(at.name));
Have a look here:
If the browser also supports the HTML5 JavaScript API, you should be able to get the data with:
var attributes = element.dataset
or
var cat = element.dataset.cat
Oh, but I also read:
Unfortunately, the new dataset property has not yet been implemented in any browser, so in the meantime it’s best to use getAttribute and setAttribute as demonstrated earlier.
It is from May 2010.
If you use jQuery anyway, you might want to have a look at the customdata plugin. I have no experience with it though.
As mentioned above modern browsers have the The HTMLElement.dataset API.
That API gives you a DOMStringMap, and you can retrieve the list of data-* attributes simply doing:
var dataset = el.dataset; // as you asked in the question
you can also retrieve a array with the data- property's key names like
var data = Object.keys(el.dataset);
or map its values by
Object.keys(el.dataset).map(function(key){ return el.dataset[key];});
// or the ES6 way: Object.keys(el.dataset).map(key=>{ return el.dataset[key];});
and like this you can iterate those and use them without the need of filtering between all attributes of the element like we needed to do before.
You should be get the data through the dataset attributes
var data = element.dataset;
dataset is useful tool for get data-attribute
or convert gilly3's excellent answer to a jQuery method:
$.fn.info = function () {
var data = {};
[].forEach.call(this.get(0).attributes, function (attr) {
if (/^data-/.test(attr.name)) {
var camelCaseName = attr.name.substr(5).replace(/-(.)/g, function ($0, $1) {
return $1.toUpperCase();
});
data[camelCaseName] = attr.value;
}
});
return data;
}
Using: $('.foo').info();
You can just iterate over the data attributes like any other object to get keys and values, here's how to do it with $.each:
$.each($('#myEl').data(), function(key, value) {
console.log(key);
console.log(value);
});
I use nested each - for me this is the easiest solution (Easy to control/change "what you do with the values - in my example output data-attributes as ul-list) (Jquery Code)
var model = $(".model");
var ul = $("<ul>").appendTo("body");
$(model).each(function(index, item) {
ul.append($(document.createElement("li")).text($(this).text()));
$.each($(this).data(), function(key, value) {
ul.append($(document.createElement("strong")).text(key + ": " + value));
ul.append($(document.createElement("br")));
}); //inner each
ul.append($(document.createElement("hr")));
}); // outer each
/*print html*/
var htmlString = $("ul").html();
$("code").text(htmlString);
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/prism.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/themes/prism-okaidia.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h1 id="demo"></h1>
<ul>
<li class="model" data-price="45$" data-location="Italy" data-id="1234">Model 1</li>
<li class="model" data-price="75$" data-location="Israel" data-id="4321">Model 2</li>
<li class="model" data-price="99$" data-location="France" data-id="1212">Model 3</li>
</ul>
<pre>
<code class="language-html">
</code>
</pre>
<h2>Generate list by code</h2>
<br>
Codepen: https://codepen.io/ezra_siton/pen/GRgRwNw?editors=1111
One way of finding all data attributes is using element.attributes. Using .attributes, you can loop through all of the element attributes, filtering out the items which include the string "data-".
let element = document.getElementById("element");
function getDataAttributes(element){
let elementAttributes = {},
i = 0;
while(i < element.attributes.length){
if(element.attributes[i].name.includes("data-")){
elementAttributes[element.attributes[i].name] = element.attributes[i].value
}
i++;
}
return elementAttributes;
}
If you know the name of keys you can also use object destructuring to get values like this
const {id, cat, cid } = document.getElementById('prod').dataset;
You can also skip keys you don't need and get the ones you need like this
const { cid, id } = document.getElementById('prod').dataset;
100% Javascript no jQuery ;)
DOMStringMap :
console.log(document.getElementById('target-element-id').dataset);
or custom variable :
var data = {};
Object.entries(document.getElementById('target-element-id').dataset).forEach(([key, val]) => {
data[key] = val;
});
console.log(data);