Cannot bind to rootscope in Angular 1.5 component - angularjs

I'm in the process of eliminating the "scope soup" architecture of a legacy Angular 1.5 app following this guide: http://teropa.info/blog/2015/10/18/refactoring-angular-apps-to-components.html#replace-external-reference-with-bound-input
I'm trying to remove the reference to $rootscope.taskui, so I tried to add a binding to the component. Unfortunately, taskui is now undefined. The "component" is an Angular 1.5 component (which is just a normal directive under the hood). Am I doing something wrong?
If you replace "this.taskui" with "$rootscope.taskui" (correctly injected), method prints the taskui object just fine.
export default {
bindings: {
taskui: '='
},
controller,
templateUrl: "component.html"
};
Here's the controller code:
class Controller {
constructor() {
this.method = () => console.log(this.taskui)
}
}

The problem was a misunderstanding of angularjs scope. When using isolated scope, it is not enough to just bind a variable. You must also pass the value as an attribute. See solution #3 here: https://stackoverflow.com/a/17900556/555493
The code (using the original example) should be:
// component
export default {
bindings: {
taskui: '='
},
controller,
templateUrl: "component.html"
};
// parent template
<component taskui="taskui"></component>

Related

TypeScript AngularJS component modal - this.$modalInstance.dismiss is not a function?

I've turned one of my user data entry forms into a uib-modal, but I get when I attempt to close the modal from the "cancel"button, I get this error: this.$modalInstance.dismiss is not a function. . Same thing is if use this.modalInstance.close(). Which is weird because TypeScript seems to think those methods should be there based on the code completion in VS Code.
Anyway, the basic set up is as follows:
Controller which opens the modal:
class TestModalController {
static $inject = ['$modal'];
options: ng.ui.bootstrap.IModalSettings;
myModal? : ng.ui.bootstrap.IModalInstanceService;
constructor(private $modal: ng.ui.bootstrap.IModalService) {
this.options = {
animation: true,
component: 'fringeEdit',
windowClass: 'fringe-edit',
resolve: {}
}
}
openFringeEdit() {
this.myModal = this.$modal.open(this.options);
}
}
This works fine, the modal opens as expected.
This is the modal instance itself:
class FringeEditController {
static $inject =['$uibModalInstance']
constructor(private $uibModalInstance: ng.ui.bootstrap.IModalInstanceService) {
}
dismiss() {
this.$uibModalInstance.close("closed"); //This throws error whether using dismiss or close
}
}
Registration Code:
app.controller("FringeEditController",['$uibModalInstance', FringeEditController]);
app.controller("TestModalController", ['$uibModal', TestModalController]);
app.component("fringeEdit", {
controller: "FringeEditController",
templateUrl: "/template/fringeEditTemplate.html",
bindings: {}
});
I've tried several tweaks from various posts here, but I keep getting this error, which leads me to believe that whatever is being passed in as as $uibModalInstance isn't actually a $uibModalInstance, otherwise close and dismiss would work, wouldn't it?
Not really sure what else to try.
When using the component property of the $uibModal.open method, use bindings instead of local injection:
app.component("fringeEdit", {
controller: "FringeEditController",
templateUrl: "/template/fringeEditTemplate.html",
bindings: { close: "&" }
});
Then use the bindings in the controller:
dismiss() {
this.close({$value: "closed"});
}
From the Docs:
$uibModal's open function
options parameter
component (Type: string, Example: myComponent) - A string reference to the component to be rendered that is registered with Angular's compiler. If using a directive, the directive must have restrict: 'E' and a template or templateUrl set.
It supports these bindings:
close - A method that can be used to close a modal, passing a result. The result must be passed in this format: {$value: myResult}
For more information, see
UI-Bootstrap Modal Directive API Reference

unable to get $scope form name on load

I'm trying the retrieve the form name inside my angularjs component while the form is being loaded as I wanted to set the form state to dirty based on some data validations that were resolved in to the component. I'm able to access the form name once the form is completely loaded say inside a submit, however i'm unable to do that on the load how can I do that. I'm using ui.router hence the controller name is being set based on the state.
<form class="form-horizontal" name="detail.myForm">
<button ng-click="detail.submit">
</form>
app.component('myDetail', {
bindings: {
alldetails: '<'
},
templateUrl: '/app/detail.html',
controllerAs: 'detail',
controller: function ($state, $transitions, $scope) {
var detail=this;
/*validateData in the alldetails here */
$scope.detail.myForm.$setDirty(); // issue here saying undefined
detail.submit = () =>{
$scope.detail.myForm.$setPristine() //works without any issue
}
}
This happens since the DOM isn't ready on your controller's construction. You have to use the $onInit callback instead. From AngularJS docs:
$onInit() - Called on each controller after all the controllers on an element have been constructed and had their bindings initialized (and before the pre & post linking functions for the directives on this element). This is a good place to put initialization code for your controller.
Also, it'd be better to inject the ngFormController by using the require object instead of assigning it to your model.
Here's a fiddle with a working example. The relevant code is:
.component('myDetail', {
template: '<h1>Details Component</h1>',
controllerAs: 'detail',
// By requiring the form controller, angular will
// create a 'formCtrl' property on your controller with the
// ngFormController instance of the parent form.
require: {
formCtrl: '^form'
},
controller: function() {
// We can't just acces the formController here, couse it will be
// undefined, since the dom isn't ready yet. So we have to use the
// $onInit callback that will be executed by angularjs.
this.$onInit = function() {
/*validateData in the alldetails here */
this.formCtrl.$setDirty();
}
}
});

AngularJS 1.5 - How to Set Two Way Bindings on Component

Directives in Angular 1.X are set to have two way binding by default. Components have isolated scopes by default. I have a view that looks like:
<div class="my-view">
{{controllerVariable}}
</div>
If I have the above set up as a directive, the controllerVariable loads correctly in the following situation:
<div ng-controller="myController">
<my-view></my-view>
</div>
But if I have it set up as a component using the following:
myApp.component('myView', {
templateUrl: '/path/to/view',
bindings: '='
});
then the variable value isn't displayed. I have tried adding $ctrl to the variable:
<div class="my-view">
{{$ctrl.controllerVariable}}
</div>
but this doesn't display the value either.
What am I missing here?
You need to pass the value from the directive into the component:
<my-view passed-var='ctrl.passedVar'></my-view>
and in the component:
myApp.component('myView', {
templateUrl: '/path/to/view',
bindings: {
passedVar: '='
},
controller: function () {
var vm = this;
console.log(vm.passedVar);
}
});
then you will be able to access in the component as in the example
There are a few other ways to do it, such as using a service to handle the information or using require which would give your component access to the controller of the directive. You can find the above method and others here: https://docs.angularjs.org/guide/component.
I had to explicitly state the variable I wanted to bind:
myApp.component('myView', {
templateUrl: '/path/to/view',
bindings: {
controllerVariable: '#'
}
});
Also, since controllerVariable is a string, I had to use the # sign binding.

angularJS 1.5 nested components

I am trying to build a parent view (with a controller) that has a angularjs component. The parent view delivers the data to the component by two way binding (it is a result of an Web-Service, so the data is a JSON Object). A part of the data is again delivered from the component to another nested component.
Now when I change the data in the nested component by a textfield, following exception occurs:
angular.js:13424 Error: [$compile:nonassign] Expression 'undefined' in attribute 'attributDetailDto' used with directive 'catAuswertungsparameterBearbeitung' is non-assignable!
JS of Component
angular.module('catApp').component('catAuswertungsparameterBearbeitung', {
controller : CatAuswertungsparameterBearbeitungController,
templateUrl : 'resources/js/konfiguration/auswertungsparameter/catAuswertungsparameterBearbeitung.html',
bindings : {
attributDetailDto : '='
}
});
function CatAuswertungsparameterBearbeitungController($translate) {
var ctrl = this;
}
HTML of component
...
<cat-textfeld min=1 max=50 wert="$ctrl.attributDetailDto.bezeichnung"></cat-textfeld>
...
JS of nested component cat-textfeld
angular.module('catApp').component('catTextfeld', {
controller : MinMaxTextfeldController,
templateUrl : 'resources/js/fwk/catTextfeld.html',
bindings : {
wert : '=',
min : '#',
max : '#'
}
});
function MinMaxTextfeldController($translate) {
var ctrl = this;
HTML of nested component
<input type="text" class="textfeld" name="textfeld" ng-model="$ctrl.wert">
Do you have any ideas? Many thanks :)
Oh man, you couldn't help me because I totally failed in calling the first component.
This didn't work for sure:
<cat-auswertungsparameter-bearbeitung attributDetailDto="attributDetailDto"></cat-auswertungsparameter-bearbeitung>
Because angular also divides camel-case attributes:
<cat-auswertungsparameter-bearbeitung attribut-detail-dto="attributDetailDto"></cat-auswertungsparameter-bearbeitung>
sorry for sapping your time
When using bindings in Angular 1.5, you are able to declare the binding parameteres as optional, like this: wert: '=?'. This way, using the catAuswertungsparameterBearbeitung component will not force using all of its bindings attributes.
a small advice, start using a pattern like this
app.component("someComponent", {
bindings: {
something: '='
},
templateUrl: "app/templates/layer-list-component.html",
controllerAs: "model",
controller: function () {
var model = this;
}
});
and then
<input type="text" class="textfeld" name="textfeld" ng-model="model.wert">
to avoid confusion when using the $ symbol

How to call angular controller scope methods from nested directives in Typescript

Given following common setup:
CtrlA (page level controller)
|- directiveAA (component e.g. button bar)
|- directiveAAA (sub-component e.g. button)
I would like to call CtrlA.methodA() from directiveAAA by passing the methodA down the chain using directive attributes - CtrlA -> directiveAA -> directiveAAA. So for example my directiveAAA "Save" button can call controller method "Save". Components directiveAA and directiveAAA are dumb components and only know about their environment given their attribute settings.
Before Typescript I would make use of inherited scope down the chain to call controller method $scope.save() from directiveAAA.
How would this work with Typescript? Would we still have to make use of injected scope into our controller, directive controller classes or can this be done without using scope, based on class inheritance?
So here's my question in code - its probably not perfect but gives the gist - the nub of the problem is marked with comment "this is where i need help":
module app.page {
class CtrlPage {
private ctrlPageBtnActions: string[] = ['goPrev', 'goNext'];
goPrev(){
console.log('go previous');
}
goNext(){
console.log('go next');
}
}
function pageDirective(){
return {
restrict: 'E',
replace: true,
template: '<button-bar actions="CtrlPage.ctrlPageActions"></button-bar>',
controller: CtrlPage,
controllerAs: 'ctrlPage',
bindToController: true
}
}
angular.module('app')
.directive('page', pageDirective);
}
module app.page.directives {
class CtrlBtnBar {
private actions: string[];
}
function buttonBar() {
return {
restrict: 'E',
replace: true,
template: '<div class="buttonBar"><btn action="CtrlBtnBar.actions[0]"></btn><btn action="CtrlBtnBar.actions[1]"></btn></div>'
controller: CtrlBtnBar,
controllerAs: 'CtrlBtnBar',
bindToController: {
actions: '='
}
}
}
angular.module('app')
.directive('buttonBar', buttonBar);
}
module app.page.directives {
class CtrlBtn {
private action: string;
handleClick(){
if (action === 'goNext'){
CtrlPage.goNext(); /// Here is where I need help
}
}
}
function btnDirective(){
return {
restrict: 'E',
replace: true,
template: '<input type="button" value="someCaption" ng-click="CtrlBtn.handleClick()"/>',
controller: CtrlBtn,
controllerAs: 'ctrlBtn',
bindToController: {
action: '#'
}
}
}
angular.module('app')
.directíve('btn', btnDirective);
}
If you run the code in http://www.typescriptlang.org/Playground you will see that typescript understandably objects to CtrlPage reference from within btnDirective controller CtrlBtn, because within this context CtrlPage does not exist. Must we use angular $scope to access the "goNext" method, given that the btnDirective is dumb and is not aware of its parents controllers and only receives inputs from its attributes? Taking radim's tip into consideration I guess the answer is yes.
Typescript with AngularJS (ver 1) does NOT bring any change to angular's architecture/design. So scopes are scopes, and they will be inherited as they did (via .$new())
Also, any Typescript class inheritance has no impact on $scope inheritance. And that won't change even with Angular 2. If some component (a bit like controller class in Typescript today) will be using code from its parent (derive from it) - in runtime it will has no effect on its context.
So, use the angular as you did, just profit from strongly typed language support.
Check these Q & A with working examples with directives:
How can I define my controller using TypeScript?
How To bind data using TypeScript Controller & Angular Js
Angular Ui-router can't access $stateParams inside my controller

Resources