Passing non-string attributes to a $compile function - dom

I made an html-renderer directive which accepts a string and turns it into html elements:
.directive('htmlRenderer', function($compile) {
var linker = function(scope, element, attrs) {
scope.$watch('template', function() {
element.html(scope.template);
$compile(element.contents())(scope);
console.log("Template", scope.template);
});
};
return {
restrict: 'E',
replace: true,
link: linker,
scope: {
template: '#'
}
};
});
My problem is that I get a 'DOM Exception 5' when passing objects as attributes:
<html-rendered template='<foo x-data={{ neededData }}/>'/>
And in my controller I have:
function Controller($scope) {
$scope.neededData = { text: 'Foo needs this for something' };
}
Here's a fiddle of it: http://jsfiddle.net/9U7CJ/2/
I've come across this question: Getting INVALID_CHARACTER_ERR: DOM Exception 5
which pretty much means I'm doing something wrong. And sadly, I don't know much about DOM soo I'm pretty much stuck.
I've read and re-read the Angular Docs for $compile and about the link/compile functions but it didn't get any clearer for me. Thanks in advance! :)

Related

AEM multifield data-sly-repeat ${item} not working

This has been driving me nuts - hoping someone can help me.
I have a multifield component called 'books' with a single textfield: 'title'.
Everything seems to be working; the dialog box contains the multifield then I add two title fields then enter 'title1' and 'title2'.
then in the HTML itself I go:
<div data-sly-repeat="${properties.books}">
<p>${item}</p>
<p>${itemList.index</p>
<p>${item.title}</p>
</div>
What I don't get is, ${item} correctly gives me:
{"title": "title1"} {"title": "title2"}
and ${itemList.index} correctly gives me: 0 1
but ${item.title} keeps coming up blank. I also tried ${item["title"]} and that comes up blank too.
What am I doing wrong here? In my desperation I contemplated using
<div data-title="${item}"></div>
and then using JS to process the JSON object but I don't really want to do that.
Someone help, please!
It looks like your books property is either a JSON array string or a multivalued property with each value being a JSON object string;
The easiest way to parse the property is via a JS model like the following:
You could simplify this script to match your specific case, I made it general to multi-value and non-multi-value string properties.
/path/to/your-component/model.js:
"use strict";
use(function () {
// parse a JSON string property, including multivalued, returned as array
function parseJson(prop){
if(!prop) return [];
var result =[];
if(prop.constructor === Array){
prop.forEach(function(item){
result.push(JSON.parse(item));
});
}
else {
var parsed = JSON.parse(prop);
if(parsed.constructor === Array){
result = parsed;
}
else result = [parsed];
}
return result;
}
var $books = properties.get("books", java.lang.reflect.Array.newInstance(java.lang.String, 1));
var books = parseJson($books);
return {
books: books
}
});
/path/to/your-component/your-component.html:
<sly data-sly-use.model="model.js"/>
<div data-sly-repeat="${model.books}">
<p>${item}</p>
<p>${itemList.index</p>
<p>${item.title}</p>
</div>

How do I transpile an *expression* with babel?

Given the following:
require('babel-core').transform('3').code
Is there a way to get this to return 3 (an expression) instead of 3; (a statement)?
I've tried:
Searching the web and various sites for 'babel expression', 'babel transpile expression', and the like.
Passing (3), but that also was transformed into 3;.
Poking the babel internals to figure out what's going on, but I don't know enough about how it works to sort this out.
Passing the option minified: true, which claims to remove the trailing semicolon but doesn't actually seem to do anything.
Right now I'm using .replace(/;$/, ''), which works but seems absurd and error-prone.
#loganfsmyth provided the missing link on BabelJS.slack -- parserOpts.allowReturnOutsideFunction. Here's the code I came up with:
const babel = require('babel-core');
let script = 'return ((selector, callback) => Array.prototype.map.call(document.querySelectorAll(selector), callback))("#main table a.companylist",a => a.innerText)';
let transform = babel.transform(script, {
ast: false,
code: true,
comments: false,
compact: true,
minified: true,
presets: [
['env', {
targets: {
browsers: ['> 1%', 'last 2 Firefox versions', 'last 2 Chrome versions', 'last 2 Edge versions', 'last 2 Safari versions', 'Firefox ESR', 'IE >= 8'],
}
}]
],
parserOpts: {
allowReturnOutsideFunction: true,
}
});
let code = `return function(){${transform.code}}()`;
Output:
return function(){"use strict";return function(selector,callback){return Array.prototype.map.call(document.querySelectorAll(selector),callback)}("#main table a.companylist",function(a){return a.innerText});}()
My input script looks a bit funny just because of how I generated it, but you can see that it transformed those arrow expressions into regular functions and the whole thing is still an expression.
N.B. you may want to modify that last line to something like
let code = `(function(){${transform.code}})()`;
depending on your needs. I needed mine to be returned.

Cloud9 & Meteor.js undefined global var

I'm trying to create the Leaderboard on Cloud9. But I get the error: PlayersList not defined... in the editor. The app is working, but then it's code in editor underlining all 'not defind PlayersList'
The code:
PlayersList = new Mongo.Collection('players');
if(Meteor.isClient){
Template.leaderboard.helpers({
'player': function(){
return PlayersList.find({}, {sort: {score: -1, name: 1}});
},
'selectedClass': function(){
var playerId = this._id;
var selectedPlayer = Session.get('selectedPlayer');
if(selectedPlayer === playerId){
return 'selected';
}
},
'showSelectedPlayer': function(){
var selectedPlayer = Session.get('selectedPlayer');
return PlayersList.findOne(selectedPlayer);
}
});
Cloud9's editor uses ESLint, and using foo = 22 makes it think that there's a missing statement like var foo; somewhere. You can either choose to ignore this, or fix it as follows:
Add /*eslint-env meteor */ to the top so it doesn't give warnings about Meteor globals, and maybe you'll also need to add /* globals Player */ added too in case the error still stays. (I haven't tested this out, please let me know how it goes so I can improve the answer)
I solved the problem with a little workaround. I added coffeescript and then I used the # symbol on the global like you should, when you define a collection with coffeescript. And that was solving it for me. It works fine. As I opened the app in an new browser window, posts were available in the console.
Example:
#Posts = new Mongo.Collection('posts')

approach for validated form controls in AngularJS

My teammates and I are learning AngularJS, and are currently trying to do some simple form field validation. We realize there are many ways to do this, and we have tried
putting input through validation filters
using a combination of controller and validating service/factory
a validation directive on the input element
a directive comprising the label, input and error output elements
To me, the directive approach seems the most "correct". With #3, we ran into the issue of having to communicate the validation result to the error element (a span sibling). It's simple enough to do some scope juggling, but it seemed "more correct" to put the span in the directive, too, and bundle the whole form control. We ran into a couple of issue, and I would like the StackOverflow community's input on our solution and/or to clarify any misunderstandings.
var PATTERN_NAME = /^[- A-Za-z]{1,30}$/;
module.directive("inputName", [
function () {
return {
restrict: "E",
require: "ngModel",
scope: {
fieldName: "#",
modelName: "=",
labelName: "#",
focus: "#"
},
template: '<div>' +
'<label for="{{fieldName}}">{{labelName}}</label>' +
'<input type="text" ng-model="modelName" id="{{fieldName}}" name="{{fieldName}}" placeholder="{{labelName}}" x-blur="validateName()" ng-change="validateName()" required>' +
'<span class="inputError" ng-show="errorCode">{{ errorCode | errorMsgFltr }}</span>' +
'</div>',
link: function (scope, elem, attrs, ngModel)
{
var errorCode = "";
if (scope.focus == 'yes') {
// set focus
}
scope.validateName = function () {
if (scope.modelName == undefined || scope.modelName == "" || scope.modelName == null) {
scope.errorCode = 10000;
ngModel.$setValidity("name", false);
} else if (! PATTERN_NAME.test(scope.modelName)) {
scope.errorCode = 10001;
ngModel.$setValidity("name", false);
} else {
scope.errorCode = "";
ngModel.$setValidity("name", true);
}
};
}
};
}
]);
used as
<form novalidate name="addUser">
<x-input-name
label-name="First Name"
field-name="firstName"
ng-model="firstName"
focus="yes"
model-name="user.firstName">
</x-input-name>
<x-input-name
label-name="Last Name"
field-name="lastName"
ng-model="lastName"
model-name="user.lastName">
</x-input-name>
...
</form>
First, because both form and input are overridden by AngularJS directives, we needed access to the ngModel API (ngModelController) to allow the now-nested input to be able to communicate validity to the parent FormController. Thus, we had to require: "ngModel", which becomes the ngModel option to the link function.
Secondly, even though fieldName and ngModel are given the same value, we had to use them separately. The one-way-bound (1WB) fieldName is used as an attribute value. We found that we couldn't use the curly braces in an ngModel directive. Further, we couldn't use a 1WB input with ngModel and we couldn't use a two-way-bound (2WB) input with values that should be static. If we use a single, 2WB input, the model works, but attributes like id and name become the values given to the form control.
Finally, because we are sometimes reusing the directive in the same form (e.g., first name and last name), we had to make attributes like focus parameters to be passed in.
Personally, I would also like to see the onblur and onchange events bound using JavaScript in the link function, but I'm not sure how to access the template markup from within link, especially outside/ignorant of the larger DOM.

Is it possible to add angularized html tags and attributes in a directive based on model data in Angularjs?

I want to build a directive that will build form inputs based on a nested object of settings that include the input type, model to bind, and html attributes. I have been pounding my head and, perhaps, am close to concluding that this is something Angular is not equipped to do. I would like to build a directive that could take an array of objects like:
[{
"label":"When did it happen",
"model": $scope.event.date,
"element":"input",
"type": "date",
"options":{
"class":"big",
"required": true,
},
},{
"label":"How did it happen?",
"model": $scope.event.cause,
"element":"textarea",
"options":{
"cols":45,
"rows":55,
},
},{
"label":"How many times did it happen?",
"model": $scope.event.times,
"element":"input",
"options":{},
}],
I have struggled through many different aspects of directives. I keep running across a few issues.
Neither the template nor the controller directive functions have access to any sort of scope that could reach any sort of data--most especially my array I've made. This means that I can't decide how to build my DOM (i.e. tag types and attributes) until later.
All compiling is done before the linking function. I am able to manipulate the DOM in the linking function but none of it is angularized. This means if I add a required attribute angular's ngValidate is not aware of it. If I try and change the tag type, I reset lose my model binding etc.
It seems that this is just the way angular runs. Is there no good way to affect the DOM tag types and attributes based on model data without specifying everything out specifically?
How about something like this:
$scope.event = {
date: new Date(),
cause:'It was the colonel in the kitchen with the revolver',
time:5, //...
$metadata: data
};
Where data here is simply the array you have already shown. (apart from the model property which would just be a string representing the property of the event, like so:
{
"label":"When did it happen",
"model":'date',
"element":"input",
"type": "date",
"options":{
"class":"big",
"required": true,
}
}
Then your directive would simply access the property given it on the parent scope. (event) And process the metadata into a usable template. Which could then get compiled.
Here is such a directive, and the directive...
myApp.directive('contentForm',function($compile,$interpolate){
var template = "<span class='lbl'>{{label}}</span><{{element ||'input'}} {{options}}"+
" ng-model='{{root+'.'+model}}'></{{element}}>";
function linkerFunction(scope,element,attrs){
if(!scope[attrs.contentForm])return;
var metadata = scope[attrs.contentForm].$metadata || false;
if(!metadata)return;
element.html(getHtml(metadata,attrs.contentForm));
$compile(element.contents())(scope);
}
return {
restrict: 'A',
replace: true,
link:linkerFunction
};
//interpolate the template with each set of metadata
function getHtml(metadata,root){
var interpolation = $interpolate(template);
var html = '';
if(angular.isArray(metadata)){
for(var i = 0; i < metadata.length; i++){
metadata[i].root = root;
metadata[i].options = processOptions(metadata[i].options);
html += interpolation(metadata[i]) + '</br>'
}
}else{
html = interpolation(metadata);
metadata.options = processOptions(metadata.options);
}
return html;
}
// parse object into html attributes
function processOptions(options){
var result = '';
for(var key in options){
if(options.hasOwnProperty(key)){
result += ' '+key+"='"+options[key]+"'"
}
}
return result.trim();
}
});
You'd probably want to change the template. I seem to have forgotten to put in a type. But that should be fairly straight forward. I hope this helps!