Use child scope's data for parent scope's ng-repeat - javascript

Update 2 added, see below
First of all, this is the starting point of the framework I am working with (and needs to fix):
// index.html
<!doctype html>
<html ng-app="myApp">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="index.js"></script>
<body>
<div ng-controller="outerController">
<div id="copy"></div>
<hr>
<div id="src" ng-controller="innerController">
<table>
<th>Name</th>
<th>Type</th>
<tbody>
<tr ng-repeat="poke in pokemon">
<td>{{ poke.name }}</td>
<td>{{ poke.type }}</td>
</tr>
<tr>
<td>Pikachu</td>
<td>Electric</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
// index.js
var app = angular.module("myApp", []);
app.controller("innerController", function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
});
app.controller("outerController", function($scope) {
$("#copy").html($("#src").html());
});
So as you can see, the child controller will generate a table from its scope's data via ng-repeat. This step is successful. The next step is for the parent controller to copy-paste the inner HTML from src to copy. The intent is for copy to contain a copy of the complete table fully generated by angularJS inside src.
This step has failed. Only the table headers and the static Pikachu row is visible. After doing some research I am certain that this is because pokemon is inside the child controller's scope which is inaccessible by the parent controller. The HTML copied into the copy container includes the entire ng-repeat directive. This copied directive is inside the parent scope, where $scope.pokemon does not exist/contains no data, which is why the ng-repeat in copy generated nothing.
I cannot put the data inside the parent controller. In my actual application, the system uses a modular design. Each inner controller represents a module which pulls its own set of data from the server. There are multiple web pages (represented by the outer controller) which have a many-to-many relationship with the modules, and the composition of modules in each web page needs to be modifiable. That means the data used by a module must be contained within itself.
How can I rectify this?
Update 1: Redacted. I posted an example of using $emit and $on but Robert's example should be assumed as correct, since I'm still very new to this. Refer to his answer.
Update 2: While testing Alvaro Vazquez's & Robert's solutions, I've identified the specific root cause. When $("#copy").html($("#src").html()); is executed, either the copied ng-repeat executed before any data transfer to outerController occurred, or it was never executed. In the end, modifying what I originally did above makes it fully working:
var app = angular.module("myApp", []);
$(function() {
$("#copy").html($("#src").html());
});
app.controller("innerController", function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
});
app.controller("outerController", function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
});
With the location of that particular statement changed, all that is left is to transfer the data to outerController, and at this point both Alvaro's and Robert's solutions work.
As an aside, I think some have advised against using $("#copy").html($("#src").html());. As I have partly described in the comments, the actual application I'm developing consists of multiple web pages, each containing its own outerController. Each innerController is in its own separate HTML file added via an include directive into src. The outerController copies the inner HTML of src, passes it to a third party library, which pastes it into copy and controls its visual layout. $("#copy").html($("#src").html()); is actually part of the third party library's implementation, so I can't change that. Using this statement is therefore a requirement.
I'll post the above as a solution when I get home and has the convenience of a PC keyboard. In the meantime feel free to recommend better solutions to what is found if you have one, thanks!

I think you should make use of angular services.
Declaring a service
First of all, you should declare a service which would 'serve' the data to the rest of your application. For the sake of simplicity, I will only show a method which returns a predefined array, but you could get the data from the server here.
app.service('pokemonService', function () {
return {
getPokemon: function () {
return [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
}
};
});
Using the service in your controller
Then, you can use the service on any of your controllers, injecting it as any other predefined angular service:
app.controller('innerController', function($scope, pokemonService) {
$scope.pokemon = pokemonService.getPokemon();
});
app.controller('outerController', function($scope, pokemonService) {
$scope.outerPokemon = pokemonService.getPokemon();
});
Showing the data in your view
Finally, you can list all your pokémon in any template/part of the template you want:
<!doctype html>
<html ng-app="myApp">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="index.js"></script>
<body>
<div ng-controller="outerController">
<div id="copy">
<!-- Here you can also list the pokémon from your outerController, maybe this time in a list -->
<ul>
<li ng-repeat="poke in pokemonOuter">
{{ poke.name }} - <span class="type">{{ poke.type }}</span>
</li>
</ul>
</div>
<hr>
<div id="src" ng-controller="innerController">
<table>
<th>Name</th>
<th>Type</th>
<tbody>
<tr ng-repeat="poke in pokemon">
<td>{{ poke.name }}</td>
<td>{{ poke.type }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
Wrap up
As you can see, there is no need of messing with the DOM at all. If you use AngularJS, you should do things the Angular way, and working directly with the DOM is not the Angular way at all. Instead, you should put all your data and business logic into services, then use those services in your controllers to retrieve that data and pass it to the view.

Scopes in Angular uses prototypal inheritance, so the child scope will have access to the parent properties but the parent will not have access to the child controller scope properties.
You can use a service to share data or use $emit to send events upwards (upwards until the root scope).
I created a plnkr for you to show you how to use emit (you can find it here)
var app = angular.module("myApp", []);
app.controller("outerController", ['$scope', function($scope) {
console.log('aici');
$scope.$on('pokemonEvent', function(event, mass) { console.log(mass); });
}]);
app.controller("innerController", ['$scope', function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
$scope.$emit('pokemonEvent', $scope.pokemon);
}]);

Related

requiring and then compiling html in angularjs

I have an array of templates in an AngularJS controller which display depending on which array item is selected like so:
function controller() {
vm.templates = [
{ name: 'form1200intro.html', url: require('html-loader!../html/europct/europct.form1200.intro.tpl.htm')},
{ name: 'form1200questions.html', url: require('html-loader!../html/europct/europct.form1200.questionnaire.tpl.htm')},
{ name: 'form1200generated.html', url: require('html-loader!../html/europct/europct.form1200.generated.tpl.htm')}
];
}
$scope.form1200Template = vm.templates[0].url;
And in the html I am displaying them using the ng-bind-html directive, like so:
<div data-ng-bind-html="form1200Template">
</div>
The HTML is loading but none of the expressions are compiling.
Question
How do I display the html with all expressions compiled?

String Interpolation not getting updated after change to object in Viewmodel in Aurelia

I have an element in my View in Aurelia that is not getting updated when an object from its Viewmodel is getting updated. I've seen the documentation about Pub/Sub and Event Aggregators, however this seems a little heavy-handed for what I want to do, since I am not trying to communicate between two different resources, but rather just within a View and its Viewmodel.
When a change occurs to the object in the Viewmodel, I don't know how to correctly update (or trigger an update to) the string interpolation in the View.
My code is as follows
myview.html
<h1>My List</h1>
<ul>
<li repeat.for="group of modelObject.groups">
<span>${group.id}</span>
<span repeat.for="state of group.region">${state}</span>
</li>
<ul>
<button click.delegate(editModelObject())>Edit</button>
myviewmodel.js
constructor()
{
this.modelObject = {
<other stuff>,
"groups": [
{
"id": "default",
"regions" : ["NY", "CT", "NJ"]
},
{
"id": "west",
"regions" : ["CA", "OR"]
}
],
<more stuff>
}
}
editModelObject() {
<something that updates both the id and regions of a group in this.modelObject>
}
For some reason, the states are correctly changing in the view, but the id's are not. Do I need to use something like Pub/Sub to get the two-way binding to work correctly? Or is there a simple thing that I am missing or doing wrong?
This works if you change a property of one of the array's objects. But this doesn't work if you assign one of the array's index because this would require dirty-checking. See https://github.com/aurelia/binding/issues/64
To solve your problem you should use splice() instead of indexed assignment. For instance:
const newItem = { id: 77, name: 'Test 77', obj: { name: 'Sub Name 77' } };
//instead of this.model.items[0] = newItem; use below
this.model.items.splice(0, 1, newItem);
Running example https://gist.run/?id=087bc928de6532784eaf834eb918cffa

Optimal way to use Angular Directives and JSON

I want an efficient way to factor an Angular Directive that is written to display a chart.
After reading other answers here, I found a nice way to create a directive that displays a single chart without any problem.
How do I reuse the same directive to display different charts? Each chart needs a JSON object that has settings and data in order to render.
I don't want to pollute my Angular View by typing 100-150 lines of JSON and passing it in via the directive.
Details:-
Each chart has some common key/value pairs that I can leave in the directive.
How do I infuse chart specific key & value pairs in each directive?
Eg:- Say I want one chart to have green bars and the other chart to have red lines.
Angular Directive
(function () {
'use strict';
angular
.module("analytics")
.directive("angularDirectiveAmcharts", angularDirectiveAmcharts);
function angularDirectiveAmcharts() {
var directive = {
link: link,
restrict: 'A',
replace: true,
scope: {
chartdata: '=',
type: '=',
customstyle: '#',
chartsettings: '=',
chartid: '#'
},
template: '<div id="{{ chartid }}" style="{{ customstyle }}"></div>'
};
return directive;
function link(scope, elem, attrs) {
AmCharts.makeChart(scope.chartid, {
"type": "serial",
"categoryField": "date",
"autoMarginOffset": 10,
"marginRight": 20,
"marginTop": 20,
//I've deleted lots of keys and values for the sake of brevity
"dataProvider": scope.chartdata
});
}
}
})();
View
<div class="chartarea" ng-controller="pcController as vm">
<div angular-directive-amcharts chartid="chartdiv" chartdata="vm.chart_data"></div>
</div>
I am particular about maintainability because a lot of changes are going to made after I'm done with my internship.
Parts of the given code in this answer are based on another answer
You could use a service to provide a standard configuration to all of your chart directives. In this service you can define this standard configuration once and merge it with a specific configuration each time, a directive is created. This way you only have to declare minor changes in your controller.
Nonrequired but possible config binding into directive:
<div ng-controller="myCtrl">
<my-chart></my-chart>
<my-chart config="conf"></my-chart>
</div>
Specific configuration in controller:
myapp.controller('myCtrl', function ($scope) {
$scope.conf = {
graphs: [{ type: 'column' }]
};
});
Service for default configuration (using jQuerys way to deep merge objects):
myapp.service('chartService', function () {
this.defaultConfig = {
"type": "serial",
// reduced object for readabilty
};
this.getConfig = function (mergeObj) {
return $.extend(true, {}, this.defaultConfig, mergeObj);
}
});
The data is get through another service, and added to the configuration after the merge:
var config = chartService.getConfig(scope.config || {});
config.dataProvider = dataProvider.getData();
chart = AmCharts.makeChart(element[0], config);
I've prepared a fiddle, so you can take a look into an example.

Accessing array of objects in an object using Angular

I am trying to have an array of 30 recipes shown on my view with data from an API call.
// app.js
angular.module('recipeApp', [])
.controller('RecipesCtrl', ['$scope', '$http', function ($scope, $http) {
$scope.mainview = [];
$http.get('/API')
.then(function(response) {
$scope.mainview = response.data;
});
// index.html
<html lang="en" ng-app="recipeApp">
<body ng-controller="RecipesCtrl">
{{mainview}} //this outputs the same data shown on the API call.
// when I try 'ng-repeat' nothing shows up at all
// data from API call (this is just a sample of the data. there is really an array of 30 objects in "recipes")
{
"count": 30,
"recipes": [
{
"publisher": "Closet Cooking",
"f2f_url": "http://food2fork.com/view/35382",
"title": "Jalapeno Popper Grilled Cheese Sandwich",
"source_url": "http://www.closetcooking.com/2011/04/jalapeno-popper-grilled-cheese-sandwich.html",
"recipe_id": "35382",
"image_url": "http://static.food2fork.com/Jalapeno2BPopper2BGrilled2BCheese2BSandwich2B12B500fd186186.jpg",
"social_rank": 100,
"publisher_url": "http://closetcooking.com"
},
{
"publisher": "The Pioneer Woman",
"f2f_url": "http://food2fork.com/view/47024",
"title": "Perfect Iced Coffee",
"source_url": "http://thepioneerwoman.com/cooking/2011/06/perfect-iced-coffee/",
"recipe_id": "47024",
"image_url": "http://static.food2fork.com/icedcoffee5766.jpg",
"social_rank": 100,
"publisher_url": "http://thepioneerwoman.com"
},
When I have {{mainview}} in the html, it shows the same as above, but how can I have it so all 30 recipes are looped in the view? I looked into ng-repeat, but I am very new to Angular and couldn't figure it out. Any information on this would be appreciated.
you can use ng-repeat like this:
<body ng-controller="RecipesCtrl">
<ul>
<li ng-repeat="recipe in mainview.recipes">{{recipe}}</li>
</ul>
</body>
It will generate a li element for every recipe in your array. You can access the properties of a recipe using . as you would in javascript.
{{recipe.publisher}}
Note: ng-repeat works with any elements, I used ul and li for show purposes only.
Something like this may help you:
<ul>
<li ng-repeat="recipe in mainview.recipes">
{{recipe.title}}
<br />
- {{recipe.publisher}}
</li>
</ul>
I think you're looking for a view fragment like:
<div data-ng-repeat="item in mainview.recipes">
<div>
<label>Publisher</label><span>{{item.publisher}}</span>
<div>
<div>
<label>Title</label><span>{{item.title}}</span>
<div>
...
</div>
where ... is whatever else you want to display in the view. Documentation at: https://docs.angularjs.org/api/ng/directive/ngRepeat (though I know you've read it (: )

How to load partials / views / templates dynamically in Ember.js

So I have the following setup.
On the main page, a list of generators is being displayed based on a list coming from a model using fixture data.
Now when one of the generator links is clicked, a new page is shown with some input fields that are dynamically generated based on that fixture data.
Until this point everything works perfectly.
Now when I change the input field's value in the generator page (after selecting one of the generators) to see the changes being updated in some sort of a preview div just below my input fields, it is easy. I can use {{generatorFields.0.value}} to bind the first input field, .1., and so on until I bind all of them.
But as you can imagine, each generator has its own format and its own input fields, and I want to create a new .hbs file for each and every one of them and then pass that file into the generator page to show the preview.
I solved 0.1% of the problem with a partial. In the generator.hbs file I entered {{partial "generator-1"}} and this loads my _generator-3.hbs file that contains that {{generatorFields.0.value}} bind, and it works. But that partial is not dynamic; I need to load a different partial each time I use a different generator. How can I achieve this?
How can I pass the partial name dynamically or load a template based on the model data that I have?
The code used so far is below:
idex.hbs looks like this:
<table class="table table-hover">
<thead>
<tr>
<th>#</th>
<th>Generator name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{{#each model}}
<tr>
<td>{{id}}</td>
<td>{{title}}</td>
<td>{{#linkTo 'generator' this classNames="btn btn-mini pull-right"}}Create file{{/linkTo}}</td>
</tr>
{{/each}}
</tbody>
</table>
generator.hbs
{{#each generatorFields}}
<div class="row-fluid">
<div class="span4">{{name}}</div>
<div class="span8">{{view Ember.TextField valueBinding='value' class='span12' placeholder='Type value here…'}}</div>
</div>
{{/each}}
{{partial "generator-1"}}
_generator-1.hbs
<h1>Project: {{generatorFields.0.value}}</h1>
app.js
App.Store = DS.Store.extend({
revision: 13,
adapter: 'DS.FixtureAdapter'
});
App.Router.map(function () {
this.resource('index', { path: '/' });
this.resource('generator', {path: '/generator/:generator_id'});
});
App.IndexRoute = Ember.Route.extend({
model: function () {
return App.Generator.find();
}
});
App.Generator = DS.Model.extend({
title: DS.attr('string'),
templateName: DS.attr('string'),
generatorFields: DS.attr('generatorFields')
});
// Fixture data
DS.RESTAdapter.registerTransform('generatorFields', {
serialize: function(serialized) {
return Em.none(serialized) ? {} : serialized;
},
deserialize: function(deserialized) {
return Em.none(deserialized) ? {} : deserialized;
}
});
App.Generator.FIXTURES = [{
id: 1,
title: "test 1",
generatorFields: [
{id: 1, name: "name 1", value: ""}
],
templateName: "generator-1"
}, {
id: 2,
title: "test 2",
generatorFields: [
{id: 1, name: "name 1", value: ""},
{id: 2, name: "name 2", value: ""},
],
templateName: "generator-2"
}];
You can create a dynamic partial helper that uses the passed in name to render with the {{partial}} helper.
Ember.Handlebars.helper('dynPartial', function(name, options) {
return Ember.Handlebars.helpers.partial.apply(this, arguments);
});
Then use this dynamic partial, {{dynPartial}} instead.
{{#each item in controller}}
{{dynPartial item.templateName}}
{{/each}}
For a generator with templateName of generator-1. This would render with the partial _generator-1. Note that the name of the template's id/data-template-name must begin with an underscore.
You should be able to simply place your dynamic partial variable within the partial helper.
{{#each item in controller}}
{{partial item.templateName}}
{{/each}}
As #darshan-sawardekar pointed out if you have a generator with templateName of generator-1it would render the partial _generator-1.
While #Darshan's answer is simpler than the below and will work in many cases, I just ran into an issue where transitioning to a same route with a different model causes the partial to not re-render if the second model's partial name is the same as the first's (bug in ember?). Setting up a view that watches the model fixes this.
App.FooDynamicLayout = Ember.View.extend
rerenderOnModelChange: (->
#rerender()
).observes('model')
And call it with:
view App.FooDynamicLayout templateName=dynamicTemplateName model=model
#KamrenZ already mentioned this but I figured I'd cite chapter and verse for those looking into this. More recent versions of Ember gracefully accept bound property names and use them in the partial helper:
http://ember-doc.com/classes/Ember.Handlebars.helpers.html#method_partial
BOUND TEMPLATE NAMES The parameter supplied to partial can also be a
path to a property containing a template name, e.g.:
{{partial someTemplateName}}
The above example will look up the value of someTemplateName on the
template context (e.g. a controller) and use that value as the name of
the template to render. If the resolved value is falsy, nothing will
be rendered. If someTemplateName changes, the partial will be
re-rendered using the new template name.

Categories