how to access $error from nested forms - angularjs

I have one main form and another subForm. How I can access $error of the child form but without having to directly reference the subForm name?
I want to be able to display that name is required(for example) instead of just knowing the name of the form.
Here's a demo of my problem: http://plnkr.co/edit/QWZArI1UFPpJdjoK8eVn?p=preview

Ok, I wasn't 100% sure I understood your question, but I think I do, so here's a stab at it:
There are a couple ways to access the errors of a child form, but all seem to need the name of the ngForm.
Assuming this structure:
<form name="parentForm">
<ng-form name="childForm"></ng-form>
</form>
you know that you can access it via $scope.childForm.$error, but less known is that it is also attached to the parent form. You can access it with $scope.parentForm.childForm.$error, but obviously, that's no good, since you still need the name.
You could get hacky and loop through the properties on the parent form and try to tell which one is the child form and go from there.
Lastly, as we've discussed on Twitter/GitHub, I have a directive that kinda does some of this magic for you. It did have a bug that didn't handle embedded forms correctly, but I fixed it. Check out the new version of my directive that tries to simplify handling errors with Angular:
https://github.com/CWSpear/angular-form-errors-directive
I added the ability to display all the errors of all the child ngForms with a flag in v1.3.0.

I think for this particular use case, my simple getErrors method on the scope is a more robust solution than the formErrors directive proposed in the accepted answer. Have a look at this comparison of the two solutions. The formErrors directive will clear errors from the child forms if the bottom child form is filled out. Play around with data entry and you will quickly see other bugs.
The getErrors solution is not pretty, but it is very simple and one can easily see how it might be improved to provide clearer messages.
The JavaScript:
var app = angular.module('app', ['FormErrors']);
app.controller('MainCtrl', ['$scope', function ($scope) {
$scope.people = [{name: ''}, {name: ''}];
$scope.allErrors = [];
$scope.getErrors = getErrors;
function getErrors(formObject){
$scope.allErrors = [];
extractErrors(formObject)
}
function extractErrors(formObject, parent) {
for (var e in formObject.$error) {
var path = parent ? parent + "." + e + "." + formObject.$name : formObject.$name + "." + e;
var err = formObject.$error[e];
if (err instanceof Array){ // this is an array of sub-forms
err.forEach(function (subForm, i) {
extractErrors(subForm, path + "[" + i + "]");
});
} else {
if (err === true) {
path = path.replace(new RegExp("." + e, "g"), "");
$scope.allErrors.push(path + ": " + e);
}
}
}
}
}]);

Related

Programatically add fields to parsley form

I'm using parsleyjs to validate my forms client-side. I have a scenario where, on click of a checkbox, some more form fields are exposed and need validating. If the checkbox is then unclicked the form fields are hidden and validation needs removing.
Is there functionality to achieve this in parsley? I've looked through the docs but can only find details of how to validate through attributes in the html. I'm looking for a method I can call in code to add and remove fields to be validated.
I ran into this exact situation a couple of weeks ago. You do need an extra bit of js to accomplish this. I dug around and found one bit of script that came close but needed some tweaking to suit my needs: it depended on predefined field definitions. No bueno. I wanted it to duplicate fields regardless of their names/ids/whatever. Then, of course, increment each new field name. Also without cloning whatever values had been entered by the user.
Since the fields being cloned already have the necessary parsley validation, that just went right along with them.
Here's the cloning code I came up with. I'm sure it can use improvement. And here's a fiddle with a working example.
this is my first contribution after years of lurking. be gentle. ;)
$('#btnDel').prop('disabled', true);
$('#btnAdd').prop('disabled', false);
$('#btnAdd').click(function() {
var num = $('.clonedInput').length;
var newNum = new Number(num + 1);
var newElem = $('#input1').clone().val('').attr('id', 'input' + newNum);
newElem.find(':input').attr('id', function () {
return this.id + '_' + newNum
});
newElem.find(':input').attr('name', function () {
return this.name + '_' + newNum
});
newElem.find(':input').val('');
$('#input' + num).after(newElem);
$('#btnDel').prop('disabled', false);
if (newNum == 5){
$('#btnAdd').prop('disabled', true);
}
});
$('#btnDel').click(function() {
var num = $('.clonedInput').length;
$('#input' + num).remove();
$('#btnAdd').prop('disabled', false);
if (num-1 == 1){
$('#btnDel').prop('disabled', true);
}
});
$('#btnDel').prop('disabled', true);
});
Parsley is great for most validation scenarios but in the past I've struggled to get it to do what I need - the lack of ability to programmatically interact with the validation lifecycle makes it less useful for more complex validations.
I wrote a library called Okjs that works in a very similar fashion to Parsley but with the added benefit that when you need to do something like add a new validator to a field based on user interaction there's a code API to allow you to do so:
https://github.com/jamesfiltness/Okjs
To add fields on click of a checkbox in Ok would go like so:
$('.my__checkbox').focus(function() {
Ok.Form.addField('checkbox1', ['required']);
Ok.Form.addField('checkbox2', ['required']);
})

populate a form with a bookmarklet doesn't work in angularjs

I have a bookmarklet that just does some simple form input population but the angular form is still in an invalid state. Like nothing was ever changed.
I tried calling el.onchange() but that doesn't seem to do anything.
javascript:populate();
function populate(){
var name = document.querySelector('input[name=name]');
name.value = 'Fred';
name.click();
name.onchange();
}
Since I already have jQuery loaded, I was able to fix the issue by triggering the input event.
$('input[ng-model]').trigger('input');
I used something similar to the code below to create my bookmarklet.
Couple of things to consider.
Write your function (see example below)
Before creating your bookmarklet, test your function in the chrome console
Function tested, now remove the endlines (e.g. "replace all" in intellij (with regex enabled), replace "\n" with ""
create a new bookmark in the browser
edit the url, the url should begin with "javascript:", paste your one line url after that... you'll end up with something like "javascript:(function(){..." etc...
//nb: This function is not written to work in this page
(function () {
//get the scope object for your controller
var sc=angular.element("[ng-controller=myController]").scope();
//grab your model
var m=sc.mymodel;
//some 'dodgy' date strings :)
var today=new Date();
var todayAsString=today.toLocaleDateString("en-GB");
today.setDate(today.getDate()-183);
var sixMonthsAgoStr=today.toLocaleDateString("en-GB");
today.setDate(today.getDate()+365);
var sixMonthsLaterStr=today.toLocaleDateString("en-GB");
//pick a random oz state
var states=["WA", "VIC","SA", "NT","NSW", "ACT", "QLD"];
var ozState=states[Math.floor(Math.random()*(6)+1)];
//update the model with some semi random values
m.actionDate=todayAsString;
m.ausstralianState=ozState;
m.streetNum=Math.floor((Math.random() * 1000) + 1);
m.ID="CR" + Math.floor((Math.random() * 10000) + 1);
m.manufactureYear="" + Math.floor(Math.random()*(2016-1960+1)+1960); //approx... between 1960 and 2016...
m.endDate=sixMonthsLaterStr;
m.startDate=sixMonthsAgoStr;
//MUST call $scope.apply() at the end.
sc.$apply();
})();

How use set method on model Backbone to change a data?

I'm new with Backbone and I think I misunderstand the use of get/set with the model. I made a really simple example with IPython widget :
class Automaton(widgets.DOMWidget):
from IPython.display import Javascript
_view_name = traitlets.Unicode('AutomatonView', sync=True)
nodes = traitlets.List(sync=True)
%%javascript
require(['widgets/js/widget'], function(WidgetManager){
var AutomatonView = IPython.DOMWidgetView.extend({
render: function(){
var n_nodes = this.model.get("nodes").slice();
n_nodes[0] += 1;
this.model.set("nodes", n_nodes);
return this;
},
});
WidgetManager.register_widget_view("AutomatonView", AutomatonView);
});
So now I can call the widget like that:
a = Automaton(nodes=[1])
What I understand here is: nodes is passed to the model and now in the model nodes = [1], then I create a new node [2] that I'm setting to the model, so I'm expected for the nodes value be [2] now, but If I ask
a.nodes
It's say:
[1]
Sorry if I don't understand something really basic, if you can explain whats is happening here it will be really useful for me. I checked if the n nodes is [2] with a console log and it's fine so it's really with "this.model.set" the problem.
Are you sure you're rendering the view? Try to put this code into an initialize method instead, and everything should work just fine. By the way, this should work even without a set method call:
initialize: function() {
var n_nodes = this.model.get("nodes");
n_nodes[0] += 1;
return this;
},
Maybe that can help someone but I fixed my error with adding
this.touch();
I found my answer on this example : http://nbviewer.ipython.org/github/ipython/ipython/blob/2.x/examples/Interactive%20Widgets/Custom%20Widgets.ipynb
" it is very important that we call this.touch() to let the widget machinery know which view changed the model "

advice on how to design an extendable backbone view

I have the following backbone application.
It's a generic crud view, with the following template:
<div id="<%= crudId %>">
<div class="header-view"></div>
<div class="table-view"></div>
<div class="form-view"></div>
</div>
You can see the crud live here: http://bbbootstrap.com.ar/index.html#Wine
The view itself has subviews, to be rendered in the table-view and the form-view.
The thing is I want it to be a base crud view, and to be easily entendable, adding new subviews, for example, adding a new panel to issue some bulk operations.
These are the possible solutions I came out with so far
1- inheritance: create a new CrudBulkView inheriting from CrudView, modify the template to have a bulk-view place holder.
pro: inheritance can provide quite an elegant and simple solution
cons: it's a bit limiting, I'd like to just be able to compose the BulkView and add it to the CrudView.
2- add a method to crudview like addView(view, place) with place being something like 'beforeForm', 'afterForm', 'beforeTable', etc... (it's much too hardcoded...
cons: too hardcoded
3- pass a function with each subview I want to add, that takes care of creating the dom and attaching to it, right after CrudView has rendered the container. the method could be called setEl and return the newly created el.
pro: really flexible
cons: adds some complexity to the process of attaching the subview to the dom
4-modify the crudView template and then attach to it, something like this:
<div id="<%= crudId %>">
<div class="header-view"></div>
<div class="table-view"></div>
<div class="form-view"></div>
<div class="bulk-view"></div
</div>
then bulkView.el would be '.bulk-view'
pro: simple approach
cons: have to mess around with strings, instead of dealing with the dom
I think it's not so strange what I'm trying to achieve. I just want to add a view to a container view, being as much decoupled as possible, and being able to establish where it should be rendered.
After reading your response to my previous answer I went through and modified my example to hopefully give you an idea of how you can implement a system with named views that allows you to control the ordering as you desire. Let me know if this helps or if you have any questions about how it works.
var viewCtor = Backbone.View.prototype.constructor;
// Assuming we have a reference to the subviews already
var BaseCrudView = Backbone.View.extend({
// This is null for an important reason, see comment in constructor
subViews: null,
// Override the constructor instead of initialize since this is meant to be a base object, so things that
// inherit don't have to remember to call the parent inialize every time.
constructor: function() {
viewCtor.apply(this, arguments);
// It is important this is initialized when instantiating the view rather than in the prototype.
// Backbone's extend() will "copy" the prototype properties of the parent when extending, which really
// just performs an assignment. If this were initialized above in the prototype then all children
// that inherit from that prototype would share the exact same instance of the array/object. If a child
// adds something to the array, it would be changed for all instances that inherit from the parent.
this.subViews = {
header: new HeaderView(),
table: new TableView
};
this.subViewOrder = [
'header',
'table'
];
},
addBefore: function(subView, name, beforeView) {
this.subViews[name] = subView;
var viewLoc = this.subViewOrder.indexOf(beforeView);
if(viewLoc == -1) {
viewLoc = 0;
}
this.subViewOrder.splice(viewLoc, 0, name);
},
addAfter: function(subView, name, afterView) {
this.subViews[name] = subView;
var viewLoc = this.subViewOrder.indexOf(afterView);
if(viewLoc == -1) {
viewLoc = this.subViewOrder.length - 1;
}
this.subViewOrder.splice(viewLoc + 1, 0, name);
},
moveBefore: function(name, beforeView) {
this.addBefore(this.subViews[name], name, this.subViewOrder.splice(this.subViewOrder.indexOf(name), 1));
},
moveAfter: function(name, afterView) {
this.addAfter(this.subViews[name], name, this.subViewOrder.splice(this.subViewOrder.indexOf(name), 1));
},
render: function() {
var that = this;
_.each(this.subViewOrder, function(viewName) {
// Assumes the render() call on any given view returns 'this' to get 'el'
that.$el.append(this.subViews[viewName].render().el);
});
return this;
}
});
var BulkCrudView = BaseCrudView.extend({
inialize: function() {
// Skipping the last parameter causes it to insert at the end
this.addAfter(new BulkView(), 'bulkView');
}
});
With this you could easily extend the BulkCrudView and modify its subViews array in initialize to add/insert whatever you want. Though, it'd work just as well to instantiate a BaseCrudView and work with the view methods. Just whatever feels cleaner and/or floats your boat.

How to use Backbone.Marionette.ItemView with Mustache

The following code works fine using Backbone.Marionette.ItemView but not Mustache.
Backbone.Marionette.ItemView - no Mustache
I would like to use the same code but loading the template varaible using Mustache.
Here is my code:
Backbone.Marionette.ItemView - with Mustache
Any idea why my code does not work and why?
Thanks
I'd like to update the answer here a bit as I was just struggling with this, and I was using this answer as a reference.
Here are my findings:
The answer here is a bit out of date with the current version of Mustache (which is understandable as it's pretty old)
Mustache.to_html is now deprecated, but still exists as a simple wrapper around Mustache.render for backwards compat. Check out this link.
Additionally, I found overriding Marionette.Renderer.render, as in the accepted answer above, completely bypasses the Marionette.TemplateCache layer which may not be the desired behavior.
Here's the source for the Marionette.Renderer.render method:
render: function(template, data){
if (!template) {
var error = new Error("Cannot render the template since it's false, null or undefined.");
error.name = "TemplateNotFoundError";
throw error;
}
var templateFunc;
if (typeof template === "function"){
templateFunc = template;
} else {
templateFunc = Marionette.TemplateCache.get(template);
}
return templateFunc(data);
}
Source
As you can see it accesses the Marionette.TemplateCache.get method and the above answer does nothing to maintain that functionality.
Now to get to my solve (note: the above answer is not wrong necessarily; this is just my approach to maintain the Marionette.TemplateCache layer):
As the comments suggest above, override compileTemplate instead:
Marionette.TemplateCache.prototype.compileTemplate = function(rawTemplate) {
// Mustache.parse will not return anything useful (returns an array)
// The render function from Marionette.Renderer.render expects a function
// so instead pass a partial of Mustache.render
// with rawTemplate as the initial parameter.
// Additionally Mustache.compile no longer exists so we must use parse.
Mustache.parse(rawTemplate);
return _.partial(Mustache.render, rawTemplate);
};
Here's a working JSFiddle as proof.
In the fiddle, I've also overridden Marionette.TemplateCache.loadTemplate to demonstrate that it's only called once. The body of the function only adds some debug output and then re-implements most of the original functionality (minus error handling).
Marionette assumes the use of UnderscoreJS templates by default. Simply replacing the template configuration for a view isn't enough. You also need to replace how the rendering process works.
In your simple example, you only need to override the Marionette.Renderer.render function to call Mustache, and then set the template of your views to the string template that you want:
Backbone.Marionette.Renderer.render = function(template, data){
return Mustache.to_html(template, data);
}
var rowTemplate = '{{ username }}{{ fullname }}';
// A Grid Row
var GridRow = Backbone.Marionette.ItemView.extend({
template: rowTemplate,
tagName: "tr"
});
Note that your JSFiddle still won't work even when you put this code in place, because the GridView is still using a jQuery selector/string as the template attribute. You'll need to replace this with the same type of template function to return mustache.
http://jsfiddle.net/derickbailey/d7qDz/

Resources