I am using JQuery UI in an Angular application. I noticed that I was repeating the same pattern for each directive: Call the JQuery UI function with a single complex object for initialization. Rather than write separate directives for each component, I have found it easier to use a stub directive. This may not be a long term solution but it works for now.
Is there a better way to inject the attributes to make the markup more readable while still keeping the generic nature of the directive? Specifically, can I avoid using a JSON string so that it reads more like a normal angular directive.
The existing line:
<button jquiw-control='{"control":"button","options":{"label":"Hello","icons": {"primary": "ui-icon-gear","secondary": "ui-icon-triangle-1-s"}}}' ng-click="jump()"></button>
<button jquiw-control="button" jquiw-button-label="Hello" jquiw-button-icons-primary= "ui-icon-gear" jquiw-button-icons-secondary="ui-icon-triangle-1-s" ng-click="jump()"></button>
Here is a plunk of a working example of my Generic ui directive. http://plnkr.co/edit/eRoOeq
At least you can put the hardcoded JSON in the controller like this
$scope.config = {
"control": "button",
"options": {
"label": "Hello",
"icons": {
"primary": "ui-icon-gear",
"secondary": "ui-icon-triangle-1-s"
}
}
};
and then change the template to
<button jquiw-control='{{config}}' ng-click="jump()"></button>
Plunker: http://plnkr.co/edit/yY1Lc2?p=preview
Related
So, I tried to load my add-on using the about:debugging page in Firefox. But, it simply wouldn't load. Is there somewhere where an error would be logged that I could find it?
Here is my manifest.JSON code:
{
"description": "Adds a stickfigure",
"manifest_version": 2,
"name": "StickMan",
"version": "1.0",
"icons": {
"48": "icons/StickMan-48.png"
},
"applications": {
"gecko": {
"id": "extention#stick.man",
"strict_min_version": "45.0"
}
},
"permissions": [
"activeTab"
],
"background": {
"scripts": ["StickManUpdate.js"]
},
"browser_action": {
"default_icon": {
"48": "icons/StickManButton.png"
},
"default_title": "Call StickMan",
},
}
I hope that this helps other frustrated add-on creators.
Thanks in advance
The lack of loading issue is that you have multiple syntax errors in the JSON of your manifest.json file. In your manifest.json file the lines at the end of the file:
"default_title": "Call StickMan",
},
}
Should not have the extra , (which would indicate you are going to have another property in the Object):
"default_title": "Call StickMan"
}
}
If you were using the Firefox Developer Edition, the fact that you had these errors would have been obvious:
However, even if you are running Firefox 47.0.1 and had merely used the Browser Console (keyboard shortcut: Ctrl-Shift-J), as suggested in the comments, you would have seen the error:
A promise chain failed to handle a rejection. Did you forget to '.catch', or did you forget to 'return'?
See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise
Date: Sun Jul 17 2016 11:11:22 GMT-0700 (Pacific Standard Time)
Full Message: SyntaxError: JSON.parse: expected double-quoted property name at line 33 column 2 of the JSON data
Full Stack: readJSON/</<#resource://gre/modules/Extension.jsm:628:19
NetUtil_asyncFetch/<.onStopRequest#resource://gre/modules/NetUtil.jsm:128:17
While a bit cryptic, it still shows the line number of the first issue:
Full Message: SyntaxError: JSON.parse: expected double-quoted property name at line 33 column 2 of the JSON data
The error produced in the Browser Console of Firefox Developer Edition is a bit easier to parse as to what the issue is:
SyntaxError: JSON.parse: expected double-quoted property name at line 33 column 2 of the JSON data
Stack trace:
readJSON/</<#resource://gre/modules/Extension.jsm:859:19
NetUtil_asyncFetch/<.onStopRequest#resource://gre/modules/NetUtil.jsm:128:17
WebExtensions Development:
The WebExtensions API is currently in development. If you are developing a WebExtension, you should be using either Firefox Nightly, or Firefox Developer Edition in order to test your code.
More on your code:
Syntax error:
In addition to the above syntax errors, you have more issues. I did not attempt to resolve all of them, but did get sucked into fixing enough so that the add-on was functional. The next reported error, a syntax error, is in your StickManUpdate.js file on the code:
browser.tabs.sendMessage(
message: "End";
);
You have multiple issues here. Please see the tabs.sendMessage() documentation. You are missing the required tabId parameter. In addition, you appear to be mixing-up the difference between having an Object being passed as a parameter containing properties which are the information passed to the method versus a list of parameters which are other native types passed to a method. Note: It is not uncommon for there to be both a list of parameters of various native or non-native types and an Object containing properties which are data passed to the method.
Assuming browserAction is defined:
You use methods of browserAction in multiple locations where it should be browser.browserAction. browserAction by itself is not defined. Alternately, you could use browserAction as a shortcut by defining it like: var browserAction = browser.browserAction;.
Use of browserAction.getTitle() as if it is synchronous when in reality it is asynchronous:
You make a call to browserAction.getTitle() to get the value of the title. The value of the title is only available in the callback function, which you do not supply. This implies a lack of understanding of asynchronous programming. You might want to review some questions on that subject like:
Why isn't a global variable set immediately after defining a callback/listener function (asynchronous messaging, port.on)
Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
How do I return the response from an asynchronous call?
Wrong parameter type supplied to browserAction.setTitle():
This appears to, again, be confusion as to the difference between parameters of other native types and a parameter that is an Object (which may be an Object literal) which contains properties which are the information passed to the method. Admittedly, WebExtensions appear to almost arbitrarily mix using actual parameters and Objects with the properties functioning as parameters when passing information to methods. It appears that being careful as to which is being used in a particular method will be required.
Not having various functions specify the ID for the tab:
In multiple calls to various methods, you do not pass the tabId when you should. You are adding your StickMan canvas to a single tab per mouse click. You should be passing the tab ID for calls to multiple methods.
Assigning to document.body.innerHTML in stickman.js:
In general, assigning to innerHTML at any time should be avoided, if possible. It is a bad idea under most circumstances. In most instances, it may cause the entire DOM to be re-evaluated. For doing what you desire, adding HTML in text format to the DOM at the end of the HTML for an element, there is a specific function which is better/faster: insertAdjacentHTML(). Your code:
document.body.innerHTML+= '<canvas id="StickManCanvas0000000" width="100" height="200"></canvas>';
Could be written as:
document.body.insertAdjacentHTML("beforeend", '<canvas id="StickManCanvas0000000" width="100" height="200"></canvas>');
However, it is still a bad idea to use insertAdjacentHTML() here. There is a significant stigma attached to using either insertAdjacentHTML() or assigning to innerHTML. Using either will result in your add-on receiving additional scrutiny when submitted to AMO for distribution. This is mostly because there are real security issues with using either methodology for changing the DOM. The security issues are when what is being added is text that is dynamically generated from input/data which is not hard coded into your add-on. In addition, you are already mixing adding the element as text and performing changes to it using other JavaScript (e.g. assigning to canvas.style.position). You really should use one or the other. In this case, it is better to construct canvas entirely in JavaScript. It is, after all, only 4 lines to do the same thing you were doing in the two you were using for the innerHTML assignment and the getElementById() to find the canvas element.
Personally, I like using insertAdjacentHTML() in many instances with complex structures. It is generally faster to use it for inserting larget amounts of HTML. It also allows you to keep what is being inserted represented as text. Such text may be much easier to visualize the structure being added rather than figuring out what a large chunk of DOM generated using document.createElement() and setAttribute() actually looks like. However, along with the other drawbacks mentioned above, using insertAdjacentHTML() may not lend itself as easily to writing modular code.
Issues with how you insert you content script and canvas:
Every time the user clicks on your browserAction button you insert another copy of your content script into the tab. This leads to issues of errors being generated due to the consumed content scripts getting the message sent by your call to browser.tabs.sendMessage() and not being able to find the canvas. The correct solution to this is to only chrome.tabs.executeScript() the first time the button is clicked in a tab and then send a message to the content script each subsequent time the button is clicked in that tab causing the same canvas to be re-inserted into the DOM. An easy way to track if you have already loaded the StickMan into a particular tab is to use setTitle() to have the title for your button be different after the first run in that tab.
Other issues:
Note: Your code structure in stickman.js is a bit convoluted. You might want to address this.
All together
manifest.json:
{
"description": "Adds a stickfigure",
"manifest_version": 2,
"name": "StickMan",
"version": "1.0",
"icons": {
"48": "icons/StickMan-48.png"
},
"applications": {
"gecko": {
"id": "extention#stick.man",
"strict_min_version": "45.0"
}
},
"permissions": [
"activeTab"
],
"background": {
"scripts": ["StickManUpdate.js"]
},
"browser_action": {
"default_icon": {
"48": "icons/StickManButton.png"
},
"default_title": "Call StickMan",
"browser_style": true
}
}
StickManUpdate.js:
browser.browserAction.onClicked.addListener(function(tab) {
browser.browserAction.getTitle({tabId:tab.id},function(title){
if(title === 'Call StickMan') {
chrome.tabs.executeScript(tab.id, {
file: "/content_scripts/stickman.js"
});
browser.browserAction.setTitle({title:'Recall StickMan',tabId:tab.id});
} else if (title === 'Call StickMan again') {
browser.tabs.sendMessage(tab.id,"Draw");
browser.browserAction.setTitle({title:'Recall StickMan',tabId:tab.id});
}else {
browser.tabs.sendMessage(tab.id,"End");
browser.browserAction.setTitle({title:'Call StickMan again',tabId:tab.id});
}
});
});
stickman.js:
var running = true;
//document.body.insertAdjacentHTML("beforeend", '<canvas id="StickManCanvas0000000" width="100" height="200"></canvas>');
var canvas = document.createElement("canvas");
canvas.setAttribute("width",100);
canvas.setAttribute("height",200);
//var canvas = document.getElementById('StickManCanvas0000000');
canvas.style.position = 'fixed';
canvas.style.left = '0px';
canvas.style.top = (window.innerHeight-200)+'px';
canvas.style.backgroundColor = 'rgba(0, 0, 0, 0)';
canvas.style.border = '1px dashed red';
var ctx = canvas.getContext('2d');
var pos = {
x:0,
headX:50,
headY:20,
bodyX:50,
bodyY:150,
leftArmX:25,
leftArmY:90,
rightArmX:75,
rightArmY:90,
leftLegX:30,
leftLegY:200,
rightLegX:70,
rightLegY:200,
};
var setPos = function(x, y) {
canvas.style.left = x+'px';
canvas.style.top = (window.innerHeight-y-200)+'px';
};
var drawMan = function(time) {
setPos(pos.x, 0);
ctx.strokeStyle = '#000000';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.arc(pos.headX, pos.headY, 20, 0, Math.PI*2, false);
ctx.moveTo(pos.headX, pos.headY);
ctx.lineTo(pos.bodyX, pos.bodyY);
ctx.lineTo(pos.rightLegX, pos.rightLegY);
ctx.moveTo(pos.bodyX, pos.bodyY);
ctx.lineTo(pos.leftLegX, pos.leftLegY);
ctx.moveTo((pos.bodyX+pos.headX)/2, ((pos.bodyY+pos.headY)/5)*2);
ctx.lineTo(pos.rightArmX, pos.rightArmY);
ctx.moveTo((pos.bodyX+pos.headX)/2, ((pos.bodyY+pos.headY)/5)*2);
ctx.lineTo(pos.leftArmX, pos.leftArmY);
ctx.stroke();
ctx.fillStyle = '#888888';
ctx.beginPath();
ctx.arc(pos.headX, pos.headY, 20, 0, Math.PI*2, false);
ctx.fill();
if(running) {
window.requestAnimationFrame(drawMan);
}
};
drawMan();
document.body.appendChild(canvas);
browser.runtime.onMessage.addListener(function(m) {
if(m === 'End' && running === true) {
running = false;
document.body.removeChild(canvas);
} else if(m === 'Draw' && running === false) {
running = true;
document.body.appendChild(canvas);
}
});
Functionality demo [Note1: You must navigate to an actual webpage. Note2: The tooltips that pop up to tell you what the title is of your browser_action button are not captured with the program I used to create the following .gif. Note3: I added the browser_style property to the browser_action in your manifest.json file. It is new in Firefox 48. Without it, Firefox will issue a warning in the Browser Console when the add-on is loaded.]:
While exploring the angular grid from http://angulargrid.com I got stuck with an unimportant feature like having action buttons inside a cell and processing the click using a templateUrl and not an inline HTML template.
The use case is trivial:
There's a grid with some data. I want that each row contains one cell with available actions (Edit, Delete etc).
HTML:
<div ng-if="surveysGirdOptions" ag-grid="surveysGirdOptions" class="ag-fresh" style="height :400px">
</div>
AngularJS controller:
var columnDefs = [
{headerName: "Caption", field: "caption", editable: true},
{headerName: "Questions", field: "questions"},
//{headerName: "Actions", templateUrl: "views/surveys/surveyActions.html"}
{headerName: "Actions", cellRenderer: ageCellRendererFunc}
];
function ageCellRendererFunc(params) {
return '<span><a class="btn btn-sm btn-primary" ng-click="delete($index)"><span class="glyphicon glyphicon-pencil"></span></a></span><span><a class="btn btn-sm btn-primary" ng-click="delete(' + params.rowIndex + ')"><span class="glyphicon glyphicon-trash"></span></a></span>';
}
ageCellRendererFunc function behaves as expected, though there's a lot of template code in controller to refactor out and put into an html file.
But I have no clue how to access params.rowIndex (this cell's row index) in the template file. The data of the row is accessible through the data variable but I need the row index.
Any idea if it is feasible at all? And if feasible then how?
There are workarounds to achieve the same result. But iterating over a big array to find out which row should be edited, for example, based on some id from data is inefficient compared to having direct access to the row element by its index.
It looks as though the row is rendered as a whole, so it might be using something similar to ngRepeat in order to do this. One way around it is to create a directive and attach the function you need inside which will provide you the element and from there, you should be able to get the row you're after.
I used the following as a reference: http://angulargrid.com/angular-grid-angular-compiling/index.php
* UPDATE *
You can create your directive wherever you like! I tend to abstract directives away from controllers as it keeps things nice and tidy. From the docs, it looks as though the directive is attached here:
function countryCellRendererFunc(params) {
return '<country name="'+params.value+'"></country>';
}
You can create your stand alone directive like so:
var yourModule = angular.module("yourModule");
yourModule.directive('cellRender', function(){
// Whatever you need to do here:
return {
restrict: 'AE',
link: function(scope, elem, attr, ctrl){
// Attach events here...
}
}
});
//
// Inject your module into your app...
var module = angular.module("example", ["angularGrid", "yourModule"]);
module.controller("exampleCtrl", function($scope, $http) {
function countryCellRendererFunc(params) {
return '<cell-render name="'+params.value+'"></cell-render>';
}
});
I'm going to create a webapp with Polymer. For that webapp I need localization. Is there a Polymer way to do localization?
Has anyone ever done localization in a Polymer webapp?
I18n and l10n are also on my to-do list. Currently i'm porting an app from AngularJS to Polymer. The back-end is Ruby on Rails. I use the i18n-js gem which converts the Rails translation files (en.yml, de.yml and so on) into one big JavaScript file containing an I18n object with all the translations. This gem also provides a JavaScript library for performing text translations and value localizations. But there are other JavaScript libraries providing a similar functionality.
The current locale is set from the response of a HTTP request, returning the users Accept-Language header.
Nothing Polymer specific up to this point.
I then created a bunch of global Polymer expression filters that perform the various locale transformations on their input strings. This is the same method as the one that i've learned to use in an AngularJS application. The translation filter looks like follows (I18n.t is the translation function of the JavaScript library)
PolymerExpressions.prototype.i18n = function(key) {
return I18n.t(key);
};
and is used like so
<paper-button label="{{ 'action.help' | i18n }}"></paper-button>
A date localization may be written as
{{ someDate | i18n_date('short') }}
I packaged the i18n filters and additional helper functions into a Polymer element, so I can also include this element in another element and use the translation functions from within it's JavaScript code.
The i18n element is also include in my main app element where it initializes the I18n library and sets the default and current locale.
Use Polymer.AppLocalizeBehavior
https://github.com/PolymerElements/app-localize-behavior
I am using this behavior in PWA Template for locales per custom element.
https://github.com/StartPolymer/progressive-web-app-template
Not being aware of a Polymer-way doing i18n, I suggest doing that server-side.
In case the framework is Spring, I would implement the custom elements as jsp, and handle the i18n as usual with the <spring:message /> tags.
Only caveat is that switching the language of the application would require a complete reload. But as language switching is usually not done often, I don't think of this as a problem.
For Polymer 1.0 I just published a simple (heavily in development) element (see it on gitlab or read about it here). It loads the translation files asynchronously and the usage is fairly simple:
<!-- Import it in head -->
<link rel="import" href="bower_components/quaintous-i18n/quaintous-i18n.html">
<!-- initialize it in body -->
<quaintous-i18n locales-path="/locales/fa.json"></quaintous-i18n>
Now you can use it in various ways:
In computed properties: just add I18N as your element behavior and translate your data bindings, e.g. {{__('hello')}}
In global context just use I18N object, e.g. I18N.__('hello')
I implemented a different way of doing it even though there is AppLocalizeBehavior which seem to do it pretty well. I created a locale.json file with a list of languages, name of the page for which data was needed and then the data to be displayed.
{
"en": {
"loginPage" : {
"login" : "Log in",
"loginUserid" : "Enter user id",
"loginPassword" : "Enter password"
},
},
"nl": {
"loginPage" : {
"login" : "Log in",
"loginUserid" : "Voer gebruikers-ID in",
"loginPassword" : "Voer wachtwoord in"
},
},
}
I created a translate component which has the responsibility of setting the language in the local-storage (though it needs a bit of refactoring and I could use navigator object for getting the language) as well as get data from the locale.json. This looked like this:
class Translate extends
Polymer.mixinBehaviors([Polymer.AppLocalizeBehavior], Polymer.Element) {
static get is() { return 'translate'; }
static get properties() {
return {
language: {
type: String,
value: 'nl',
notify: true
},
resources: {
type: Object,
notify: true
}
}
}
ready() {
super.ready();
if (localStorage.length) {
if (localStorage.getItem('language')) {
this.language = localStorage.getItem('language');
}
else {
localStorage.setItem('language', this.language);
}
} else {
localStorage.setItem('language', this.language);
}
}
attached() {
this.loadResources(this.resolveUrl('locales.json'));
}
}
window.customElements.define(Translate.is, Translate);
Now you can utilize this custom element inside your root or app shell of the application
<translate
class="translate"
language="{{language}}"
resources="{{resources}}">
</translate>
Put an observer on your resources property and get the data from your locale.json. Now based on different pages pass only the translation values which are needed for that page. page-values being the values of the translation strings like this:
<login-form
title='Login'
name="login"
page-values="[[pageValues.loginPage]]"
login-success="[[_loginSuccess]]"
api={{apiCollection.login}}></login-form>
Hope this helps.
I created an implementation on my own. Looking what I made it's not that difficult to do.
Heres the html:
<div ng-controller="MyCtrl">
<a ng-click="open()">Open Dialog</a>
<div id="modal-to-open" title="My Title" ui-jq="dialog" ui-options="{width: 350, autoOpen: false, modal: true}">
Dialog Text
</div>
</div>
And here's the js:
function MyCtrl($scope)
{
$scope.open = function () {
$('#modal-to-open').dialog('open');
}
}
Is this the best way to go about doing this? It seems like there could be a better way of opening it without accessing the DOM but I am not sure how I would go about that. The above code works, I am just wondering if this is the way I should go about doing this. Any input is welcome.
"Best practice" is fuzzy ground here. If it's readable and it works, then you're 90% there, IMO, and it's probably fine.
That said, the "angular way" is to keep DOM manipulation out of the controller, and to use dependency injection to make sure everything is testable. Obviously the way you illustrated above would be hard to test, and puts some DOM manipulation in the controller.
I guess what I would do to get the DOM manipulation out of the controller is use a directive:
A simple directive to tie your dialog open call to a click on an element:
app.directive('openDialog', function(){
return {
restrict: 'A',
link: function(scope, elem, attr, ctrl) {
var dialogId = '#' + attr.openDialog;
elem.bind('click', function(e) {
$(dialogId).dialog('open');
});
}
};
});
And in mark up it would be used like so:
<button open-dialog="modal-to-open">Open Dialog</button>
Now, this is obviously very basic. You could get pretty advanced with this if you wanted to, adding additional attributes for different options in the dialog.
You could go even further and add a Service that opened the dialog for you, so you could inject it into your controller or even your directive, and get the call out of there that way. For example:
app.factory('dialogService', [function() {
return {
open: function(elementId) {
$(elementId).dialog('open');
}
};
}]);
And here it is in use. It seems silly because it's essentially the same thing. But that's mostly because it's a very simplistic example. But it at least leverages DI and is testable.
app.controller('MyCtrl', function($scope, dialogService) {
$scope.open = function () {
dialogService.open('#modal-to-open');
};
});
Anyhow. I hope all of that helps you decide what path you want to take. There are a thousand ways to do this. The "right" way is whatever works, allows you to do whatever you need to do (testing or anything else), and is easy to maintain.
I'm using Handlebars.js, and currently all my templates live inside script tags which live inside .html files housing dozens of other templates, also inside script tags.
<script type="text/template" id="template-1">
<div>{{variable}}</div>
</script>
<script type="text/template" id="template-2">
<div>{{variable}}</div>
</script>
<script type="text/template" id="template-3">
<div>{{variable}}</div>
</script>
...
Then I include this file on the server-side as a partial.
This has the following disadvantages:
A bunch of templates are crammed into HTML files.
Finding a given template is tedious.
I'm looking for a better way to organize my templates. I'd like each each template to live in its own file. For example:
/public/views/my_controller/my_action/some_template.html
/public/views/my_controller/my_action/some_other_template.html
/public/views/my_controller/my_other_action/another_template.html
/public/views/my_controller/my_other_action/yet_another_template.html
/public/views/shared/my_shared_template.html
Then at the top of my view, in the backend code, I can include these templates when the page loads, like this:
SomeTemplateLibrary.require(
"/public/views/my_controller/my_action/*",
"/public/views/shared/my_shared_template.html"
)
This would include all templates in /public/views/my_controller/my_action/ and also include /public/views/shared/my_shared_template.html.
My question: Are there any libraries out there that provide this or similar functionality? Or, does anyone have any alternative organizational suggestions?
RequireJS is really good library for AMD style dependency management. You can actually use the 'text' plugin of requireJS to load the template file in to your UI component. Once the template is attached to the DOM, you may use any MVVM, MVC library for bindings OR just use jQuery events for your logic.
I'm the author of BoilerplateJS. BoilerplateJS reference architecture uses requireJS for dependency management. It also provides a reference implementations to show how a self contained UI Components should be created. Self contained in the sense to handle its own view template, code behind, css, localization files, etc.
There is some more information available on the boilerplateJS homepage, under "UI components".
http://boilerplatejs.org/
I ended up using RequireJS, which pretty much let me do this. See http://aaronhardy.com/javascript/javascript-architecture-requirejs-dependency-management/.
I use a template loader that loads the template using ajax the first time it is needed, and caches it locally for future requests. I also use a debug variable to make sure the template is not cached when I am in development:
var template_loader = {
templates_cache : {},
load_template : function load_template (params, callback) {
var template;
if (this.templates_cache[params.url]){
callback(this.templates_cache[params.url]);
}
else{
if (debug){
params.url = params.url + '?t=' + new Date().getTime(), //add timestamp for dev (avoid caching)
console.log('avoid caching url in template loader...');
}
$.ajax({
url: params.url,
success: function(data) {
template = Handlebars.compile(data);
if (params.cache){
this.templates_cache[params.url] = template;
}
callback(template);
}
});
}
}
};
The template is loaded like this:
template_loader.load_template({url: '/templates/mytemplate.handlebars'}, function (template){
var template_data = {}; //get your data
$('#holder').html(template(template_data)); //render
})
there's this handy little jquery plugin I wrote for exactly this purpose.
https://github.com/cultofmetatron/handlebar-helper