Pitfalls of the New AngularJS ng-ref Directive - angularjs

The release of AngularJS V1.7.1* introduces the new ng-ref directive. While this new directive enables users to easily do certain things, I see great potential for abuse and problems.
The ng-ref attribute tells AngularJS to publish the controller of a component on the current scope. This is useful for having a component such as an audio player expose its API to sibling components. Its play and stop controls can be easily accessed.
The first problem is the player controls are undefined inside the $onInit function of the controller.
Initial vm.pl = undefined <<<< UNDEFINED
Sample = [true,false]
For code that depends on data being available, how do we fix this?
The DEMO
angular.module("app",[])
.controller("ctrl", class ctrl {
constructor() {
console.log("construct")
}
$onInit() {
console.log("onInit", this.pl);
this.initPL = this.pl || 'undefined';
this.sample = this.pl || 'undefined';
this.getSample = () => {
this.sample = `[${this.pl.box1},${this.pl.box2}]`;
}
}
})
.component("player", {
template: `
<fieldset>
$ctrl.box1={{$ctrl.box1}}<br>
$ctrl.box2={{$ctrl.box2}}<br>
<h3>Player</h3>
</fieldset>
`,
controller: class player {
constructor() {
console.log("player",this);
}
$onInit() {
console.log("pl.init", this)
this.box1 = true;
this.box2 = false;
}
},
})
<script src="//unpkg.com/angular#1.7.1/angular.js"></script>
<body ng-app="app" ng-controller="ctrl as vm">
Initial vm.pl = {{vm.initPL}}<br>
Sample = {{vm.sample}}<br>
<button ng-click="vm.getSample()">Get Sample</button>
<br>
<input type="checkbox" ng-model="vm.pl.box1" />
Box1 pl.box1={{vm.pl.box1}}<br>
<input type="checkbox" ng-model="vm.pl.box2" />
Box2 pl.box2={{vm.pl.box2}}<br>
<br>
<player ng-ref="vm.pl"></player>
</body>

Getting ref to components controller isn't new, directives back in the day allowed it and that wasn't a problem at all, it's necessary to have such feature, ng-ref is just a helper for you to do this from the template side (the same way angular 2+ does).
Nevertheless, if you need the child components ready you should use $postLink() instead of $onInit. $postLink is called after the component is linked with his children, which means, the ng-ref will be ready when it gets called.
So all you have to do is change your onInit like so:
̶$̶o̶n̶I̶n̶i̶t̶(̶)̶ ̶{̶
$postLink() {
console.log("onInit", this.pl);
this.initPL = this.pl || 'undefined';
this.sample = this.pl || 'undefined';
this.getSample = () => {
this.sample = `[${this.pl.box1},${this.pl.box2}]`;
}
}
$postLink() - Called after this controller's element and its children have been linked. Similar to the post-link function this hook can be used to set up DOM event handlers and do direct DOM manipulation. Note that child elements that contain templateUrl directives will not have been compiled and linked since they are waiting for their template to load asynchronously and their own compilation and linking has been suspended until that occurs. This hook can be considered analogous to the ngAfterViewInit and ngAfterContentInit hooks in Angular. Since the compilation process is rather different in AngularJS there is no direct mapping and care should be taken when upgrading.
Ref.: Understanding Components
The full working snippet can be found bellow (I removed all console.log to make it clearer):
angular.module("app",[])
.controller("ctrl", class ctrl {
constructor() {
//console.log("construct")
}
$postLink() {
//console.log("onInit", this.pl);
this.initPL = this.pl || 'undefined';
this.sample = this.pl || 'undefined';
this.getSample = () => {
this.sample = `[${this.pl.box1},${this.pl.box2}]`;
}
}
})
.component("player", {
template: `
<fieldset>
$ctrl.box1={{$ctrl.box1}}<br>
$ctrl.box2={{$ctrl.box2}}<br>
</fieldset>
`,
controller: class player {
constructor() {
//console.log("player",this);
}
$onInit() {
//console.log("pl.init", this)
this.box1 = true;
this.box2 = false;
}
},
})
<script src="//unpkg.com/angular#1.7.1/angular.js"></script>
<body ng-app="app" ng-controller="ctrl as vm">
Initial vm.pl = {{vm.initPL}}<br>
Sample = {{vm.sample}}<br>
<button ng-click="vm.getSample()">Get Sample</button>
<br>
<input type="checkbox" ng-model="vm.pl.box1" />
Box1 pl.box1={{vm.pl.box1}}<br>
<input type="checkbox" ng-model="vm.pl.box2" />
Box2 pl.box2={{vm.pl.box2}}<br>
<player ng-ref="vm.pl"></player>
</body>

Parent controller initialization happens before the initialization of the player controller, so that is why we have initPL as undefined in the first $onInit.
Personally, I would prefer to define and load the data that should be passed down to the nested components on the parent controller initialization instead of setting the parent's initial data from its child's. But still if we will need this we can do it on the child's component initialization using bindings and callbacks. Probably it looks more like a dirty workaround, but it could work in such scenarios, here is the code:
angular.module("app",[])
.controller("ctrl", class ctrl {
constructor() {
console.log("construct")
}
$onInit() {
console.log("onInit", this.pl);
this.getSample = () => {
this.sample = `[${this.pl.box1},${this.pl.box2}]`;
}
this.onPlayerInit = (pl) => {
console.log("onPlayerInit", pl);
this.initPL = pl || 'undefined';
this.sample = `[${pl.box1},${pl.box2}]`;
}
}
})
.component("player", {
bindings: {
onInit: '&'
},
template: `
<fieldset>
$ctrl.box1={{$ctrl.box1}}<br>
$ctrl.box2={{$ctrl.box2}}<br>
<h3>Player</h3>
</fieldset>
`,
controller: class player {
constructor() {
console.log("player",this);
}
$onInit() {
console.log("pl.init", this)
this.box1 = true;
this.box2 = false;
if (angular.isFunction( this.onInit() )) {
this.onInit()(this);
}
}
},
})
<script src="//unpkg.com/angular#1.7.1/angular.js"></script>
<body ng-app="app" ng-controller="ctrl as vm">
Initial vm.pl = {{vm.initPL}}<br>
Sample = {{vm.sample}}<br>
<button ng-click="vm.getSample()">Get Sample</button>
<br>
<input type="checkbox" ng-model="vm.pl.box1" />
Box1 pl.box1={{vm.pl.box1}}<br>
<input type="checkbox" ng-model="vm.pl.box2" />
Box2 pl.box2={{vm.pl.box2}}<br>
<br>
<player ng-ref="vm.pl" on-init="vm.onPlayerInit"></player>
</body>

Related

AngularJs views and the DOM (Leaflet and ag-grid)

I have an AngualrJS app which is currently single page. It will display either a Leaflet map or two Ag-grid, using ng-show/hide on a boolean value, to show only map or grids at a time.
I was thinking that it would be better to add routing, using ui-router, and have 2 views, one for the Leaflet map & one for the two ag-grid.
I had some problems with the grids, probably because it is necessary to do something like
// wait for the document to be loaded, otherwise
// ag-Grid will not find the div in the document.
document.addEventListener("DOMContentLoaded", function() {
// lookup the container we want the Grid to use
var eGridDiv = document.querySelector('#myGrid');
// create the grid passing in the div to use together with the columns & data we want to use
new agGrid.Grid(eGridDiv, gridOptions);
I am not asking to solve my coding problem, which I hope to figure out by myself.
Instead, I am asking you to help me understand how AngularJs ui-router views work.
Are they always bound to the DOM, and hidden until the appropriate state is entered, or are they added to/removed from the DOM as the stata changes?
Is there anything else that I need to know, in order to understand how it works under the hood?
If I understand the requirements correctly, you could first define some conditions and then make the transition to the appropriate view.
In the example code, you can change the checked attribute for inputs for changing view displayed.
var myApp = angular.module('helloworld', ['ui.router'])
.config(function($stateProvider) {
var helloState = {
name: 'hello',
url: '/hello',
template: '<h3>hello world!</h3>'
}
var aboutState = {
name: 'about',
url: '/about',
template: '<h3>Its the UI-Router hello world app!</h3>'
}
$stateProvider.state(helloState);
$stateProvider.state(aboutState);
})
.directive('selectView', selectView);
let state;
let transitions;
function selectView($state, $transitions) {
state = $state;
transitions = $transitions;
return {
restrict: 'A',
link: selectViewLinkFn
}
}
function selectViewLinkFn($scope, $element) {
const triggers = document.querySelectorAll('input[type="radio"]');
transitions.onSuccess({}, () => {
console.log('onSuccess: ', document.querySelector('h3').innerHTML);
});
transitions.onStart({}, () => {
const findedInView = document.querySelector('h3');
console.log(`onStart: ${findedInView ? findedInView.innerHTML : 'nothing found'}`);
});
setView($scope);
for (const trigger of triggers) {
trigger.addEventListener('change', () => setView($scope, true))
}
function setView(scope, needDigest) {
// Check some conditions
let currentRoute;
for (const trigger of triggers) {
if (trigger.checked) {
currentRoute = trigger.value;
}
}
state.go(currentRoute);
if (needDigest) {
scope.$digest();
}
}
}
selectView.$inject = ['$state', '$transitions'];
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<script src="//unpkg.com/#uirouter/angularjs/release/angular-ui-router.min.js"></script>
<body ng-app="helloworld" select-view>
<label for id="selectHello">Hello</label>
<input name="selectorForView" type="radio" id="selectHello" value="hello" checked>
<label for id="selectAbout">About</label>
<input name="selectorForView" type="radio" id="selectAbout" value="about">
<h2 ng-bind="selectorForView">
<h2>
<ui-view></ui-view>
</body>

Function used to filter in AngularJS 1.7 doesn't have "this" context

I am in the process of swapping out some controllers to components. I have a controller that has a custom filter function:
function MyController($scope, $filter) {
$scope.customFilter = function(item) {
$filter('filter')([item], $scope.search);
}
}
in my template:
<input type="text" ng-model="search">
<div ng-repeat="item in (filtered = (items | filter:customFilter))" >
This works great when I have access to $scope. My filter function is much more complex, but a one off. I don't really need to define it as a true filter for the app as its not used anywhere else. Hence just a custom function in the controller itself.
However I am moving my controller over to component and don't have access to $scope.
class MyComponent {
constructor($filter) {
this.$filter = $filter;
this.search = '';
}
customFilter(item) {
this.$filter('filter')([item], this.search);
}
onCustomClick() {
// if this is called from ng-click
// I can access 'this' here, why not in filter
}
}
Template:
<input type="text" ng-model="$ctrl.search">
<div ng-repeat="item in ($ctrl.filtered = (items | filter:$ctrl.customFilter))">
The customFilter function is called as before, however it has no context bound. The 'this' variable is undefined. Am I doing something wrong, or should I be able to access the context of my component in the filter function?
If I call a function in ng-click the 'this' context is bound correctly, is this a limitation of calling a function of the component to filter?
I am not sure but I think when filter calls your function it creates own context and for this reason you have a this of undefined.
A fast and alternative solution can be this sample, you can also pass parameters to filter function you defined and it will return an anonymous function
class MyComponent {
constructor() {
this.search = '';
}
customFilter(searchText) {
return function (item) {
$filter('filter')([item], searchText);
}
}
onCustomClick() {
// if this is called from ng-click
// I can access 'this' here, why not in filter
}
}
Template:
<input type="text" ng-model="$ctrl.search">
<div ng-repeat="item in ($ctrl.filtered = (items | filter:$ctrl.customFilter($ctrl.search)))">

is it possible to be notified when an angular component finishes loading?

I am using angular 1.5's new component feature to compartmentalize various things; in particular I have a sidenav slide-out menu.
The sidenav needs to run its initialization code after other components are finished loading. So far I cannot find anything that helps me break this logic apart. At the moment, I am accomplishing this with a messy hack, like this.
assume html body such as this;
<body>
<container>
<navigation></navigation>
<sidenav></sidenav>
</container>
</body>
navigation needs to finish rendering before the sidenav can execute correctly. So in my component files, I am doing this (pseudo code);
SideNav Component
bindings = {};
require = { Container: '^container' };
SideNav Controller
$postLink = function() {
Container['Ready']();
}
Navigation Component
bindings = {};
require = { Container: '^container' };
Navigation Controller
$postLink = function() {
if(Container['Ready'])
Container['Ready']();
}
Container Component
transclude = true;
Container Controller
pending = 2; // controls that must finish loading
Ready = function() {
pending--;
if( pending > 0 )
return;
// code to initialize sidenav via jQuery
}
so basically, there is a counter on the container, and each component that I need to be loaded calls a function on the parent that decrements the counter. If that causes the counter to be 0, then the sidenav is initialized.
This feels really caddywhompus, though. Is there any other way to get some form of notification or behavior that can allow me to initialize the sidenav when it is truly the right time?
You can probably think of a better place to hang these items, but the idea I had with the watch is to do something like this:
<html ng-app="myApp">
<body ng-controller="appController as AppCtrl">
<div>
<component1/></component1>
<component2></component2>
<component3></component3>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script>
angular.module("myApp",[])
.controller("appController", function($rootScope,$scope) {
var ctrl=this;
ctrl.readyForAction = readyForAction;
ctrl.letsParty = letsParty;
$scope.$watch("$rootScope.gotBeer && $rootScope.gotPizza && $rootScope.gotHockey",ctrl.readyForAction)
function readyForAction() {
if ($rootScope.gotBeer && $rootScope.gotPizza && $rootScope.gotHockey) {
ctrl.letsParty()
}
else
{
console.log("Not yet!")
}
};
function letsParty() {
alert("Let's go Red Wings!")
};
})
.component("component1", {
template:"<h1>Beer</h2>",
controller: function($rootScope) {
$rootScope.gotBeer=true;
}
})
.component("component2", {
template: "<h1>Pizza</h1>",
controller: function($rootScope) {
$rootScope.gotPizza = true;
}
})
.component("component3", {
template: "<h1>Hockey</h1>",
controller: function($rootScope) {
$rootScope.gotHockey = true;
}
})
</script>
</body>
</html>
I'm just setting the flags when the controllers are created, but obviously you could set them anywhere. So then you just watch an expression that consists of all of your flags and then when they all evaluate to true you go about your business.

AngularJS - Why is the DOM not updating

So, what I am trying to do is add a "stack" - a basic JS object - into an array called cardStacks, declared the stackCtrl function. I've read that the controller in AngularJS is not supposed to do very much, nor do you want the manipulation of DOM elements done the controller. So what I have done is create a "save" directive which, as you could probably guess, adds a new "stack" into $scope.cardStacks (if the object is not being edited)
Everything seems to work ok, up until when the template is supposed to update. console.log() reveals that objects are going into an array, but because the template is not being updated, I can only guess it is not $scope.cardStacks.
Can somebody give the following a look and tell me why the template is not listing the "stacks" in the template?
Consider the following:
Template:
<div ng-controller="stackCtrl">
<stackeditor></stackeditor>
<p>Stacks:</p>
<ul class="stack-list" ng-repeat="stack in cardStacks">
<li>{{stack.title}}
<span class="stack-opts"> (Edit | Delete)</span>
</li>
</ul>
</div>
Template for the stackeditor tag/directive:
<div>
<span class="button" ng-click="show=!show">+ New</span>
<form ng-show="show" novalidate>
<input type="text" ng-model="stackTitle" required/>
<button class="button" save>Save</button>
<button class="button" reset>Cancel</button>
<div ng-show="error"><p>{{error}}</p></div>
</form>
</div>
Controller:
function stackCtrl($scope) {
$scope.cardStacks = [];
$scope.stackTitle = "";
$scope.addStack = function(title) {
var newStack = {};
newStack.title = title;
$scope.cardStacks.push(newStack);
}
$scope.removeStack = function($index) {
console.log("removing Stack...");
}
$scope.editStack = function(element) {
console.log("editing Stack...");
}
}
Directive:
fcApp.directive('save', function() {
var linkFn = function($scope, element, attrs) {
element.bind("click", function() {
if (typeof $scope.stackTitle !== 'undefined' && $scope.stackTitle.length > 0) {
if ($scope.edit) {
$scope.editStack(element);
} else {
$scope.addStack($scope.stackTitle);
}
} else {
$scope.error = "Your card stack needs a title!";
}
});
});
return {
restrict: "A",
link: linkFn
}
}
});
Try using $apply:
$scope.cardStacks.push(newStack);
$scope.$apply(function(){
$scope.cardStacks;
}
Rendering might be the problem..... Hope it helps.
The save function would be better in stackCtrl and then use ng-click in the template to call it.
You are right that manipulating the DOM in the controller is bad practice, but you are just updating an object in the controller - angular is sorting out the DOM which is fine.

Angularjs toggle image onclick

I'm trying to toggle a button image when a user clicks it. I prefer to use angularjs instead of jquery if possible. Right now I have a working version which toggles the image when clicked, the only problem is it changes ALL the images on click. How do I reduce the scope or pass in the src attribute for the img element?
<div ng-repeat="merchant in merchants">
<div class="followrow">
<a ng-click="toggleImage()"><img id="followbutton" ng-src="{{followBtnImgUrl}}" /></a>
</div>
</div>
app.controller('FollowCtrl', function CouponCtrl($scope) {
$scope.followBtnImgUrl = '/img1'
$scope.toggleImage = function () {
if ($scope.followBtnImgUrl === '/img1.jpg') {
$scope.followBtnImgUrl = baseUrl + '/img2.jpg';
} else {
$scope.followBtnImgUrl = 'img1.jpg';
}
}
});
Can I pass in the img src attribute the function like toggleImage(this.img.src) or similar?
<div ng-repeat="merchant in merchants">
<div class="followrow">
<a ng-click="toggleImage(merchant)"><img id="followbutton" ng-src="{{merchant.imgUrl}}" />
</a>
</div>
</div>
.
app.controller('FollowCtrl', function CouponCtrl($scope) {
$scope.followBtnImgUrl = '/sth.jpg'
$scope.merchants = [{imgUrl: "img1.jpg", name:"sdf"},
{imgUrl: "img2.jpg", name: "dfsd"}];
$scope.toggleImage = function(merchant) {
if(merchant.imgUrl === $scope.followBtnImgUrl) {
merchant.imgUrl = merchant.$backupUrl;
} else {
merchant.$backupUrl = merchant.imgUrl;
merchant.imgUrl = $scope.followBtnImgUrl;
}
};
});
What you want is a new scope for each followrow. As your code stands, there's only one scope that all of the followrows are referencing.
A simple answer is to create a new controller that you attach to each followrow:
<div class="followrow" ng-controller="ImageToggleCtrl">...</div>
And then move the image toggling logic to that new controller:
app.controller('ImageToggleCtrl', function($scope) {
$scope.followBtnImgUrl = '/img1';
$scope.toggleImage = function() { /* the toggling logic */ };
});
Now, a new scope will be instantiated for each row, and the images will toggle independently.
I just added two clickable images:
<div ng-app="FormApp" ng-controller="myController" max-width="1150px;" width="1150px;" >
<input ng-show="ShowDown" type="image" style="width:250px; height:40px;" src="~/Content/Images/contactShow.png" ng-click="ShowHide()"/>
<input ng-show="ShowUp" type="image" style="width:250px; height:40px;" src="~/Content/Images/contactHide.png" ng-click="ShowHide()" />
</div>
They toggle eachothers visibility. At page load one is visible, one is not, and both clickable images call the same function:
<script type="text/javascript">
var app = angular.module('FormApp', [])
app.controller('myController', function ($scope) {
$scope.ShowDown = true;
$scope.ShowUp = false;
$scope.ShowHide = function () {
$scope.ShowDown = $scope.ShowDown ? false : true;
$scope.ShowUp = $scope.ShowUp ? false : true;
}
});
</script>

Resources