When using $compile on component, why is the scope passed through $parent? - angularjs

I'm trying to dynamically compile an Angular component using $compile, but the scope isn't passed to the components scope, but to the $parent scope instead.
Here is a simple component that binds to a myTitle-attribute and presents it:
app.component('myComponent', {
bindings: {
myTitle: '<'
},
template: `
<div>
<div>Doesn't work: {{ $ctrl.myTitle}}</div>
<div>Works: {{ $parent.$ctrl.myTitle}}</div>
</div>`
});
Then in the controller (or directive, etc.) I compile it using $compile:
app.controller('MainCtrl', function($scope, $element, $compile) {
var template = '<my-component></my-component>';
var bindings = {
myTitle: 'My Title'
}
var scope = angular.extend($scope.$new(true), {
$ctrl: bindings
});
var newElement = $compile(template)(scope);
$element.append(newElement);
});
When running this, it yield the result:
Doesn't work:
Works: My Title
Here's a plunker showing it in action
The question
How come the scope I create for the dynamically created component, is passed as a parent scope of the component?
Any pointer on why angular behaves like this and perhaps how to avoid it is much welcome.

As I see, you need to pass binding here var template = '<my-component></my-component>';
var template = '<my-component my-title="$ctrl.myTitle"></my-component>';
Full component may be like this:
app.controller('MainCtrl', function($scope, $element, $compile) {
var template = '<my-component my-title="$ctrl.myTitle"></my-component>';
$scope.$ctrl = {myTitle: 'My Title'};
$element.append($compile(template)($scope));
});

Related

$scope doesn't apply to compiled template in directive

I have the following directive for a popup with multiple templates:
app.directive('popup', function ($http, $rootScope, $templateCache, $compile, $parse, Popup) {
return {
link: function ($scope, $element, $attrs) {
$element.bind('click', function () {
var data = {
index: $attrs.index
}
$http.get('/partial/' + $attrs.popup + '.html', {cache: $templateCache}).success(function (tplContent) {
var mainElement = angular.element(document.getElementById('main'));
mainElement.append($compile(tplContent)($scope));
Popup.show(data);
});
});
}
}
});
I've pinned a visibility flag to $rootScope (to make the popup visible by css) along with index and item that came in data object. and the popup template looks like this:
<section class="popup" ng-class="{'show': popup.visibility}">
<h1>{{data[popup.index].title}}<h1>
<p>{{data[popup.index].message}}<p>
</section>
The directive loads the popup template and appends it to my mainElement, but doesn't apply scope to. so popup doesn't appear and no data is bound to it.
you need to pass $scope.$parent instead of $scope for compiling. because you are in the child $scope.

Using ‘this’ as scope when creating $ionicModal

I seem to be having an issue creating an $ionicModal when trying to specify my scope as this instead of $scope.
Since I'm binding everything in my controller via an instance name, I'm not using $scope inside of the controller.
So, I initiate the modal as instructed here in Ionic Framework doc
and switched $scope with this
$ionicModal.fromTemplateUrl('my-modal.html', {
scope: this,
animation: 'slide-in-up'
}).then(function(modal) {
this.modal = modal;
});
When the app runs, I get the following error:
undefined is not a function
and it references the following code in ionic.bundle.js:
var createModal = function(templateString, options) {
// Create a new scope for the modal
var scope = options.scope && options.scope.$new() || $rootScope.$new(true);
I even tried assigning another variable to represent this and running it that way, but the same error prevails!
If I'm not using $scope in my controller, what would be the best way to load a modal while maintaining the usage of this? Is it just not possible or am I missing something?
EDIT-
As requested, adding more info to the original,
Template:
<div id="wrapper" ng-controller="MainCtrl as ctrl">
<button ng-click="ctrl.demo()">Demo Button</button>
</div>
Controller:
angular.module('MyDemo', ['ionic'])
.controller('MainCtrl', function ($ionicModal) {
var _this = this;
this.demo = function () {
//do demo related stuff here
}
$ionicModal.fromTemplateUrl('my-modal.html', {
scope: _this,
animation: 'slide-in-up'
}).then(function(modal) {
_this.modal = modal;
});
});
So, basically, I'm using the 1st declaration style found here:
https://docs.angularjs.org/api/ng/directive/ngController
EDIT: Changed this to _this inside of $ionicModal
As requested, here's a plunker with the code above:
http://plnkr.co/edit/4GbulCDgoj4iZtmAg6v3?p=info
Because of how AngularJs currently sets up the controller when using the "controller as" syntax, you only have whatever functions and properties that you yourself define in the controller function. In order to access the $new() function that AngularJs provides to create child scopes, you need to provide an AngularJs $scope object -- which you can still get by getting it injected into your constructor function, even when using the "controller as" syntax.
angular.module('MyDemo', ['ionic'])
.controller('MainCtrl', function ($scope, $ionicModal) {
var _this = this;
this.demo = function () {
//do demo related stuff here
}
$ionicModal.fromTemplateUrl('my-modal.html', {
scope: $scope,
animation: 'slide-in-up'
}).then(function(modal) {
_this.modal = modal;
});
});

Cannot get reference to kendo grid in angular bootstrap modal window

I am trying to get reference to a kendo grid placed within a template used by bootstrap modal. The same grid works when placed directly in the scope of the app controller, but throws undefined error from the modal instance controller.
Can somebody please tell me what I am doing wrong.
var app = angular.module('plunker', ['ui.bootstrap', 'kendo.directives']);
app.controller('MainCtrl', function($scope, $modal) {
$scope.name = 'World';
$scope.mySource = new kendo.data.DataSource({
data: [
{ColumnOne:'One', ColumnTwo:'Two'},
{ColumnOne:'Three', ColumnTwo:'Four'},
{ColumnOne:'Five', ColumnTwo:'Six'}
]
});
$scope.myGridChange = function(){
var selectedRows = $scope.myGrid.select();
console.log($scope.myGrid.dataItem(selectedRows[0]));
};
$scope.items = ['item1', 'item2', 'item3'];
$scope.open = function () {
$modal.open({
templateUrl: 'myGridTemplate.html', //'myTemplate.html',
controller: ModalInstanceCtrl,
resolve: {
items: function () {
return $scope.items;
}
}
});
};
});
var ModalInstanceCtrl = function ($scope, $modalInstance, items) {
$scope.items = items;
$scope.selected = {
item: $scope.items[0]
};
$scope.myTempSource = new kendo.data.DataSource({
data: [
{ColumnOne:'One', ColumnTwo:'Two'},
{ColumnOne:'Three', ColumnTwo:'Four'},
{ColumnOne:'Five', ColumnTwo:'Six'}
]
});
$scope.myTempGridChange = function(){
var selectedRows = $scope.myTempGrid.select();
console.log($scope.myTempGrid.dataItem(selectedRows[0]));
};
$scope.ok = function () {
$modalInstance.close($scope.selected.item);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
};
I get Cannot call method 'select' of undefined at
var selectedRows = $scope.myTempGrid.select();
Here is a link to my plnkr
http://plnkr.co/edit/GUSX6JR9HRvtdSWclvPl?p=preview
Outside of your modal the controller and grid share the same scope. However when they are inside your modal the grid's scope is nested inside the controllers scope. Not entirely sure why that is but here is why it is a problem.
When you have nested scopes the child scope will prototypically inherit from the parent scope. With prototypical inheritance when you set something directly on a child scope it will not override the parent scope. So when Kendo sets $scope.myTempGrid it only gets set on the child scope and when you try to access it from your controller it is not there.
This can get pretty confusing but fortunately if you always avoid binding things directly to the scope you shouldn't run into this sort of problem. For example if you put the grid in some container object from the parent scope then you won't run into this problem.
for example: http://plnkr.co/edit/0VFJfp?p=preview
Controller:
...
$scope.container = {};
...
HTML
...
<div kendo-grid="container.myTempGrid">
...
A better solution is to always use the 'controller as' syntax: http://plnkr.co/edit/Pmjend?p=preview

Directive - controller data binding in AngularJS

I'm struggling with this for hours now.
var testApp = angular.module('testApp', []);
testApp.directive('test', function() {
return {
restrict: 'E',
transclude: true,
template: '<div ng-transclude>Hello World</div>',
link: function(scope) {
}
}
});
testApp.controller('testCtrl', function ($scope) {
$scope.user = "";
});
Here's JSFiddle: http://jsfiddle.net/2bKPj/
Now, all I need is for an input embedded in directive to be able to reflect user model directly in testCtrl controller.
I'm confused on how this beast works since I taught that scopes are shared in this case, no?
ngTransclude creates a new child scope which protorypically inherits from it's parent scope.
When you use a primitive on the child scope it shadows the parent scope's variable.
It's been already said thousand times: use the dot notation!
controller:
testApp.controller('testCtrl', function ($scope) {
$scope.data = { user : "Hello World" };
});
html:
<input type="text" ng-model="data.user"/><br />
Directive model:<span>{{ data.user }}</span>
Check my other answers for description:
bound element inside ngIf does not update binding
Directives inside ng-include

AngularJS - How can I create a new, isolated scope programmatically?

I want to create an AlertFactory with Angular.factory.
I defined an html template like follow
var template = "<h1>{{title}}</h1>";
Title is provided by calling controller and applied as follow
var compiled = $compile(template)(scope);
body.append(compiled);
So, how I can pass isolated scope from controller to factory?
I'm using in controller follow code
AlertFactory.open($scope);
But $scope is global controller scope variable. I just want pass a small scope for factory with just title property.
Thank you.
You can create a new scope manually.
You can create a new scope from $rootScope if you inject it, or just from your controller scope - this shouldn't matter as you'll be making it isolated.
var alertScope = $scope.$new(true);
alertScope.title = 'Hello';
AlertFactory.open(alertScope);
The key here is passing true to $new, which accepts one parameter for isolate, which avoids inheriting scope from the parent.
More information can be found at:
http://docs.angularjs.org/api/ng.$rootScope.Scope#$new
If you only need to interpolate things, use the $interpolate service instead of $compile, and then you won't need a scope:
myApp.factory('myService', function($interpolate) {
var template = "<h1>{{title}}</h1>";
var interpolateFn = $interpolate(template);
return {
open: function(title) {
var html = interpolateFn({ title: title });
console.log(html);
// append the html somewhere
}
}
});
Test controller:
function MyCtrl($scope, myService) {
myService.open('The Title');
}
Fiddle
Followings are the steps:
Add your HTML to the DOM by using var comiledHTML =
angular.element(yourHTML);
Create a new Scope if you want var newScope = $rootScope.$new();
Call $comile(); function which returns link function var linkFun =
$compile(comiledHTML);
Bind the new scope by calling linkFun var finalTemplate =
linkFun(newScope);
Append finalTemplate to your DOM
YourHTMLElemet.append(finalTemplate);
check out my plunkr. I'm programmatically generating a widget directive with a render directive.
https://plnkr.co/edit/5T642U9AiPr6fJthbVpD?p=preview
angular
.module('app', [])
.controller('mainCtrl', $scope => $scope.x = 'test')
.directive('widget', widget)
.directive('render', render)
function widget() {
return {
template: '<div><input ng-model="stuff"/>I say {{stuff}}</div>'
}
}
function render($compile) {
return {
template: '<button ng-click="add()">{{name}}</button><hr/>',
link: linkFn
}
function linkFn(scope, elem, attr) {
scope.name = 'Add Widget';
scope.add = () => {
const newScope = scope.$new(true);
newScope.export = (data) => alert(data);
const templ = '<div>' +
'<widget></widget>' +
'<button ng-click="export(this.stuff)">Export</button>' +
'</div>';
const compiledTempl = $compile(templ)(newScope);
elem.append(compiledTempl);
}
}
}
I assume when you are talking about an isolate scope you are talking about a directive.
Here is an example of how to do it.
http://jsfiddle.net/rgaskill/PYhGb/
var app = angular.module('test',[]);
app.controller('TestCtrl', function ($scope) {
$scope.val = 'World';
});
app.factory('AlertFactory', function () {
return {
doWork: function(scope) {
scope.title = 'Fun';
//scope.title = scope.val; //notice val doesn't exist in this scope
}
};
});
app.controller('DirCtrl', function ($scope, AlertFactory) {
AlertFactory.doWork($scope);
});
app.directive('titleVal',function () {
return {
template: '<h1>Hello {{title}}</h1>',
restrict: 'E',
controller: 'DirCtrl',
scope: {
title: '='
},
link: function() {
}
};
});
Basically, attach a controller to a directive that has defined an isolate scope. The scope injected into the directive controller will be an isolate scope. In the directive controller you can inject your AlertFactory with wich you can pass the isolate scope to.

Resources