How can you re-use a template? Consider the following javascript object:
{
MyName: '',
Address: {
Street: ''
},
MyEmployer: {
CompanyName: '',
Address: {
Street: ''
}
}
}
And a template file at /templates/myTemplate.html:
<div ng-app="someapp" ng-controller="somecontroller">
MyName: <input type="text" ng-model="MyName" />
<div ng-include="'/templates/address.html'"></div>
My Company: <input type="text" ng-model="MyEmployer.CompanyName" />
<div ng-include="'/templates/address.html'"></div>
</div>
Here is how I would imagine the address template file at /templates/address.html would look like:
<div>
Street: <input type="text" ng-model="Street" />
</div>
As you can see i'm trying to re-use the address template here. So how do you pass the proper objects to this template?
Answer
I managed to solve the problem myself and wanted to post it here for the next person who might be looking for the same treat.
My Solution
In my OP I had a model object with two addresses, one for the person's home and another for his/her employer. So taken the same object model and same address template still at /templates/address.html, we can add two additional controllers:
app.controller('personAddressController', ['$scope', function($scope) {
$scope.Street = function(newValue) { return arguments.length ? model.Address.Street = newValue : model.Address.Street; }
}]);
app.controller('companyAddressController', ['$scope', function($scope) {
$scope.Street = function(newValue) { return arguments.length ? model.MyEmployer.Address.Street = newValue : model.MyEmployer.Address.Street; }
}]);
And ng-include usage for our address template:
<div ng-include="'/templates/address.html'" ng-controller="personAddressController"></div>
<div ng-include="'/templates/address.html'" ng-controller="companyAddressController"></div>
Also we need to modify our address template:
<div>
Street: <input type="text" ng-model="Street" ng-model-options="{ getterSetter: true }" />
</div>
<!--or add a form if you have more fields like i did-->
<form ng-model-options="{ getterSetter: true }">
<div>
Street: <input type="text" ng-model="Street" />
</div>
</form>
More information on ng-model-options and setter/getter (bottom of the page): https://docs.angularjs.org/api/ng/directive/ngModel
Though watch out when adding a form since enter key will do a submission by default, but you can easily control that if you wanted to.
Lastly, you can use directives to do all of this as someone has suggested already, but you would end-up using angular directives as controllers and according to angular they aren't meant for that, more info here: AngularJS - Directives vs Controllers
ng-include share the same controller and the same data.
So a solution probably will be to think of something else. Maybe a template directive. You can pass a template and also different values to it.
// Directive
app.directive('addressTemplate', function() {
return {
templateUrl: '/templates/address.html',
scope: {
street: '='
}
};
});
// My template
<address-template street="MyEmployer.Address.Street"></address-template>
// Template
<div>
Street: <input type="text" ng-model="street" />
</div>
After struggling during months with that problem, I now use that way to avoid duplication of template:
In the controller:
$scope.myelement = {...};
$scope.myelements = [{ ...}, { ... }]
In the template myelement.html :
{{ myelement.name }} {{ myelement.anything }}
In a template:
<div ng-include="'myelement.html'"></div> <!-- $scope.myelement is used -->
<div ng-repeat="myelement in myelements track by $index"></div> <!-- $scope.myelements[$index] used myelements.length times -->
If you wanna specify another variable you can do:
<div ng-include="'myelement.html'" ng-init="myelement = SomeScopeVariable;"></div>
But in that case the ng-init is only called when the ng-include is compiled. And you won't be able to change the 'myelement' inside it, you will only be able to modify its attributes. I don't recommand to use ng-init, except in case you just want to modify / visualize a specific element in your controller. If you want to remove it or create another one, don't use ng-init.
Related
When using ng-repeat which approach has better performance? (assuming there are a large number of users)
Approach 1: Filter in Controller
<div ng-repeat="user in users | showBlocked">
<strong>{{user.id}}</strong>
<span>{{user.name}}
</div>
HTML code in template
$scope.users = [
{ id: 1, name: 'alex', isBlocked: true},
{ id: 2, name: 'john', isBlocked: true}
];
JavaScript code in Controller
showBlocked is a filter which returns a list of blocked users
.filter('showBlocked', function() {
return function(users) {
return users.filter(user => user.isBlocked);
}
});
Approach 2: Reassigns users list
<button ng-click="reassignUser(1)">reassign user</button>
<div ng-repeat="user in users">
<strong>{{user.id}}</strong>
<span>{{user.name}}
</div>
HTML code in template
$scope.reassignUser = function (userId) {
if (userId === 1) {
$scope.users = [{id: 1, name: 'alex', isBlocked: true}];
}
// in this case just assigns a single user
};
CodePen Demo: ng-repeat filter vs reassign binding
Do let me know if you need any additional information.
ng-repeat is evaluated on every $digest cycle, making it extremely slow with two-way data-binding due to $dirty checking. The best solution is to use one-way data-binding with {{:: data}} syntax.
But in your example it is indeed better to re-write the array rather than to filter it. The use of filters will work slower, due to each filter creating a sub collection of the original list. However, this can be resolved differently, by hiding the data with ng-show. Here is a post about the complex solution, but you can consider this simple example:
angular.module('myApp', []);
angular.module('myApp').controller('Ctrl', ['$scope', function($scope) {
$scope.users = [{name:"John", "id":0},
{name:"Adam", "id":1},
{name:"Ado", "id":2},
{name:"Chris", "id":3},
{name:"Heather", "id":4},
{name:"Stan", "id":5}];
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<div ng-app="myApp" ng-controller="Ctrl">
ID: <input ng-model="query" />
<ul>
<li ng-repeat="user in users" ng-show="([user.id] | filter:query).length">
{{user.name}}
</li>
</ul>
</div>
i'm wondering if there is a good practice solution for the following situation in AngularJs:
I have a html that is basically displaying a list of elements and a controller that is the view-model and handles certain events for that list.
I use this html/controller combination on different spots within my app by
using ng-include.
List.html:
<div ng-controller="listController">
<!--... display List here-->
</div>
Now I have been using events from the parent scope to fill the list controller with elements.
ListController.js
angular.module("app").controller('listController', function($scope) {
let $scope.elements = [];
$scope.$on('setElementsEvent', function(event, data) {
$scope.elements = data;
});
});
This has been working fine so far. But now I have the situation, that one parent controller has multiple of these Lists as children, but I want them to display distinct elements. With broadcasting events I would always set the elements for all child lists! Using a factory would pose the same problem, since all of the listControllers have the same controller function.
parent.html
<div ng-controller="parentController">
<!--first list-->
<div ng-include='"List.html"'></div>
<!-- second list -->
<div ng-include='"List.html"'></div>
</div>
Is there a way to get this done without having to write a new controller for every list I want to display?
Good practice is not to use standalone controllers at all. Use components, they accept arguments in the form of bindings.
Here you have an example usage:
angular
.module('exampleApp', [])
.run($rootScope => {
$rootScope.productSetA = [{
name: 'Foo'
},
{
name: 'Bar'
},
{
name: 'Baz'
}
];
$rootScope.productSetB = [{
name: 'Fiz'
},
{
name: 'Fooz'
},
{
name: 'Fez'
}
];
$rootScope.productSetC = [{
name: 'Booze'
},
{
name: 'Beeze'
},
{
name: 'Beep'
}
];
})
.component('productList', {
bindings: {products: '<'},
template: '<div ng-repeat="product in $ctrl.products" ng-bind="product.name">'
});
product-list {
display: block;
background-color: tomato;
margin: 5px;
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular.min.js"></script>
<div ng-app='exampleApp'>
<product-list products='productSetA'></product-list>
<product-list products='productSetB'></product-list>
<product-list products='productSetC'></product-list>
</div>
Best way it switch to components and set attributes or require parent controller in the child.
However if you want to go with ng-include, try ng-repeat. It creates independent scope for every iteration and set $index and variable value which are available to the child scope through $scope.$parent
so your code will look like:
<div ng-controller="parentController">
<div ng-repeat="ctrlId in ['first', 'second']" ng-include='"List.html"'></div>
</div>
Better way is to convert your template to components https://docs.angularjs.org/tutorial/step_03
well one solution in my head is to set a determinant parameter for each controller, the other solution is to use a directive for each list, you can know more about directives here , and for the first solution here's an example
HTML
<div ng-controller="listController" ng-init="ctrlNo=1"><!--here you set the determinant factor for this controller-->
<!--... display List here-->
</div>
ListController.js
angular.module("app").controller('listController', function($scope) {
$scope.ctrlNo=0;
let $scope.elements = [];
$scope.$on('setElementsEvent', function(event, data) {
if($scope.ctrlNo===1){//Here you stop the propagation to do the specific actions for the specific controller
$scope.elements = data;
}
});
});
I have an Angular factory called Model which is shared across multiple controllers.
Basically this Model encapsulate properties and helpers methods.
I'm trying now to bind this model with my view and I have a strange behaviour, The nested objects of Model and the array of objects are not bind properly.
I think this issue is caused by the fact that i'm trying to modify an object inside an other object by reference. Maybe i have lost the context for the nested elements ?
How can i solve this problem ?
Here is my app :
var app = angular.module('plunker', []);
app.factory('Blog', function() {
function Blog(id) {
this.load(id);
}
Blog.prototype = {
name: "",
description: [{
value: ""
}, {
value: ""
}, {
value: ""
}],
website: {
name: "",
url: ""
},
load: function(id) {
},
helper1: function() {
// implementation
},
helper2: function() {
// implementation
}
// many other helpers...
}
return Blog;
});
app.controller('MainCtrl', function($scope, Blog) {
$scope.model = new Blog(12);
});
And finally my view
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js#1.2.x" src="https://code.angularjs.org/1.2.22/angular.js" data-semver="1.2.22"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<p>Name <input ng-model="model.name"></p>
<div ng-repeat="line in model.description">
Description line {{$index}} <input ng-model="line.value">
</div>
<p>Website name <input ng-model="model.website.name"></p>
<p>Website url <input ng-model="model.website.url"></p>
<p>Result : {{ model | json }}</p>
</body>
</html>
To illustrate the problem, i have created a plunker example
When entering the values in the input fields, the Model is not updated with the changes. Only the field name is updated
Thanks in advance
Services are instanced by Angular and then a reference of them is injected where ever it is asked for.
Just change the code as following
var app = angular.module('plunker', []);
app.factory('Blog', function() {
return {
name: "",
description: [
{value: ""},
{value: ""},
{value: ""}
],
website: {
name: "",
url: ""
},
helper1: function() {
// implementation
},
helper2: function() {
// implementation
}
// many other helpers...
}
//return new Blog();
});
app.controller('MainCtrl', function($scope, Blog) {
$scope.model = Blog;
});
There is a slight difference in way in which services, factories and provider works. I hope you are clear on that other wise read this excellent article
http://tylermcginnis.com/angularjs-factory-vs-service-vs-provider/
NEW plunkr
http://plnkr.co/edit/FGOcPWVMeFDSyVtmv8Xd?p=preview
The factory exposes a new method using which you can create new instances of Blog.
I think its also because JSON filter is implemented you are not able to see all the values.
Because decription and website and url are in proto of the object, it's not displaying it.
I added a button for logging the model, you can see in the console that the value is changed
Edit 2
Q: I don't understand why the JSON filter is not displaying the values ?
A: I think it could be how its implemented, may be to make it light weight. Otherwise it has to walk through it's entire prototype chain
Q: Why the values are placed into the proto because of the nature of Blog ?
A: its because you add the values in the Blog's prototype and not inside Blog itself. It's how prototypical inheritence works in javascript. The advantage is think when you have to create 1000s of blog's instances. Now each instance can have the same methods and properties or to make it light weight, each instance can share the same object which is in its proto.(think of base class in OO language)
Read this for more clarity
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain
Blog here is a service that should be created by a factory. Your factory should return the new instance / a value / or a function.
You don't create an instance of the Blog that was injected to your controller, what is injected should already be the only instance available to everything that will be injected to others.
Hence, services are singleton (created by the factory).
With emberjs (1.0.0rc1) and ember-data (very recent build #36d3f1b), I am trying to setup a basic crud example. I can't figure out how to retrieve a submitted model from a view and then update/save it. Here is what my code looks like:
App = Ember.Application.create();
App.Router.map(function() {
this.resource('posts', function() {
this.route('create');
this.route('edit', {
path: '/:post_id'
});
});
});
App.PostsIndexRoute = Ember.Route.extend({
model: function() {
return App.Post.find();
}
});
App.PostsCreateView = Ember.View.extend({
submit: function () {
console.log(this.get('model')); // undefined
}
});
App.Post = DS.Model.extend({
title: DS.attr('string'),
body: DS.attr('string')
});
App.Post.FIXTURES = [{
id: 2,
title: 'a',
body: 'aa'
}, {
id: 5,
title: 'b',
body: 'bb'
}];
App.Store = DS.Store.extend({
revision: 11,
adapter: DS.FixtureAdapter.create({
simulateRemoteResponse: false
})
});
and the create template:
<script type="text/x-handlebars" data-template-name="posts/create">
{{#view App.PostsCreateView tagName="form" classNames="form-horizontal"}}
<h3>Create</h3>
<div class="control-group">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input type="text" id="title" placeholder="Title" />
{{view Ember.TextField valueBinding="title"}}
</div>
</div>
<div class="control-group">
<label class="control-label" for="body">Body</label>
<div class="controls">
<input type="password" id="body" placeholder="Body" />
</div>
</div>
<div class="control-group">
<div class="controls">
<button class="btn">Create</button>
</div>
</div>
<div>{{#linkTo 'posts'}}Back{{/linkTo}}</div>
{{/view}}
</script>
How can I access the value of the form (serialized to the model) from the submit hook? Secondly, how do I then persist this via the FixtureAdapter?
The first part of your question is tricky to answer because it's actually pretty simple, but in order for it to be simple you'll need to change the way you think about model CRUD. Your "submit" function is not needed. When you instantiate a view) it should have an instance of your model bound to it. (If you're creating a new one it will be a new, empty instance.) When you make changes to that model in the view, they are made instantly; no need for submit. (After all, what would you submit to?)
I'm not sure this actually answers your question, but maybe it puts you on a track to answering it.
I can be a lot more definite about your second question, persisting a value via the FixturesAdapter: you can't. The FixturesAdapter is just that, an adapter for loading fixtures (essentially read-only data) into the store. Changes made to models from the FixturesAdapter will only last until the app is reloaded. To persist data you will need to transition from the FixturesAdapter to a different adapter (probably the RestAdapter).
this article deals with building a small example app including a creation form (it's originally in portuguese, but fortunately, google translate doesn't garble the text too much in this case).
the code can be found here (the important part is way down the page) and a live example here.
I have the following view:
Main.Views.Login = EventQ.View.extend({
events: {
"submit form": "login"
},
template: "login",
login: function(e) {
var me = this;
$.ajax({
url: "/api/users/login",
type: 'POST',
dataType: "json",
data: $(e.currentTarget).serializeArray(),
success: function(data, status){
EventQ.app.router.navigate('dashboard', true);
},
error: function(xhr, status, e) {
var result = $.parseJSON(xhr.responseText);
me.render_with_errors(result.errors);
}
});
return false;
},
render: function(done) {
var me = this;
// Fetch the template, render it to the View element and call done.
EventQ.fetchTemplate(me.template, function(tmpl) {
me.el.innerHTML = tmpl(me.model.toJSON());
done(me.el);
});
},
render_with_errors: function(errors) {
var me = this;
// Fetch the template, render it to the View element and call done.
EventQ.fetchTemplate(this.template, function(tmpl) {
me.el.innerHTML = tmpl(errors);
});
}
});
and a simple template like this:
<form>
<input name="username" />
<input name="password" />
<button type="submit" />
</form>
what I'm looking to do is be able to re-render the template if errors are returned but keep the input's populated. An error template would like like:
<form>
<input name="username" />
<label class="error">required</label>
<input name="password" />
<button type="submit" />
</form>
Is there a way to bind the view to a model or something that I can check? Right now the render_with_errors works except for the fact that I lose all the data filled out on the form.
It's common for people to get in the mode where they only way they think that the only way they can change the page is a full re-render of a template. But rendering templates are only 1 solution to updating the page. You are still free to use traditional methods from within your backbone view. So another possible solution is for you to simply adjust the dom from your view.
So make your template be the following:
<form>
<input name="username" />
<label class="error" style="display:none">required</label>
<input name="password" />
<button type="submit" />
</form>
And then make the following change in your login function:
error: function(xhr, status, e) {
var result = $.parseJSON(xhr.responseText);
me.showLoginError();
}
showLoginError: function() {
this.$('.error').show();
}
And of course you can always add more to that, message customizations, etc.
It's just important to remember that full template renders aren't the only way for your backbone code to react to changes in the application state, and it's ok to manipulate the DOM in a method other than render.
Supposed you have a Model like this:
Main.Models.LoginModel = EventQ.Model.extend({
/* ... */
defaults: {
'username': "",
'password': ""
},
/* ... */
When your Ajax-Request successes, you can navigate to the next page.
If it fails, you can set your model to use undefined to indicate a missing value:
// assumed you did not enter your password
this.me.model.set( { 'username': textBoxValueSoFar, 'password': undefined });
Then can built up an template like this (it will be the same as on first page load):
<form>
<input name="username" value="{{username}}" />
{{#unless username}}
<label class="error">required</label>
{{/unless}}
<input name="password" value="{{password}}" />
{{#unless password}}
<label class="error">required</label>
{{/unless}}
</form>
{{unless}} checks if, the value is not false, undefined, null or []. So on first page load, it is an empty string and no error message is provided.
See http://handlebarsjs.com/ the part with "unless" for more details.
So what you do is: you use an empty string to indicate that no wrong value has been entered so far. You use undefined to check if a wrong value (in fact, nothing) has been entered.
In your template you can check for this an act appropriately.