Multi-state Forms with Angular-Formly - angularjs

I can't figure out how to do a multi-state form. The main issue is the form.$errors seem to look at the active state rather then the whole form. In other words, the submit button is meant to be disabled until ALL required questions are answered for the entire form, but it seems to become enabled when all the required questions are answered for the active state.
Here's a simplified Plunker: http://plnkr.co/edit/O49eO4uRlUjoWHlxV6Li?p=preview
And below is my actual code.
View:
<div class="row">
<div class="col-md-4">
<ul class="nav nav-pills nav-stacked">
<li role="presentation" ng-repeat="section in vm.sections[0].sections" ng-if="section.fields">
<a ui-sref="subject.items.new.section({sectionUrl: section.url})">
<span class="badge badge-warning" title="Number of unanswered required questions for {{section.name}}">1</span>
{{section.name}}
</a>
</li>
</ul>
</div>
<div class="col-md-8">
<form class="assessment-item" name="assessmentForm">
<div ui-view></div>
<hr>
<button class="btn btn-primary btn-raised" ng-disabled="assessmentForm.$invalid" ng-click="vm.createCompletedItem()">Save Changes</button>
<button class="btn btn-inverse btn-inverse-danger" back-button>Cancel</button>
</form>
</div>
</div>
Routes:
.state('subject.items.new', {
url: '/items/new/:availableItemUrl',
templateUrl: 'components/items/new.html',
controller: 'ItemNewCtrl',
controllerAs: 'vm',
resolve: {
getAvailableItemsResolve: function(DataService) {
return DataService.availableItems().getList();
},
getUser: function($cmUserData) {
return $cmUserData.getUser(1);
}
}
})
.state('subject.items.new.section', {
url: '/:sectionUrl',
template: '<div class="animated fadeIn"><formly-form model="vm.completedItem.answers" fields="vm.fields"></formly-form></div>',
controller: function($scope, $stateParams, lodash) {
var vm = this;
var _ = lodash;
vm.questions = _.filter($scope.vm.sections[0].sections, { 'url' : $stateParams.sectionUrl });
vm.fields = vm.questions[0].fields;
vm.completedItem = $scope.vm.completedItem;
},
controllerAs: 'vm'
})
itemNewCtrl:
(function() {
'use strict';
angular
.module('casemanagerApp')
.controller('ItemNewCtrl', ItemNewCtrl);
function ItemNewCtrl($stateParams, $filter, DataService, lodash, getUser, getAvailableItemsResolve) {
var vm = this;
var _ = lodash;
vm.item = DataService.completedItems().one();
vm.availableItems = getAvailableItemsResolve;
vm.sections = _.filter(vm.availableItems, { 'url' : $stateParams.availableItemUrl });
vm.completedItem = DataService.completedItems().one();
vm.completedItem.subjectId = $stateParams.subjectId;
vm.completedItem.name = vm.sections[0].name;
vm.completedItem.probationOfficer = getUser.firstName + ' ' + getUser.lastName;
vm.completedItem.label = 'Final';
}
})();

The issue is due to the way you have things set up. When I switch tabs, angular-ui-router removes the view entirely and therefore angular only sees one formly-form, therefore only one formly-form is validated at a time. I would recommend against using angular-ui-router and instead just use a directive (like angular-ui-bootstrap's tabset directive) which uses ng-show to keep both forms on the page at a time and therefore both will be taken into account when doing validation. Here's an example that might help you there.

All you need to do is set a flag, like "formComplete" in the model and then flip it on once they get to the last question...we have similar issues at work with something like this and that's how we work around it.
Alternatively you could simply move the submit button to the last page

Related

controllerAs method is not getting called when others are

I' trying out firebase 3 with my Ionic app. (I just made one to mess with - so I figure i might as well do everything right from the beginning, such as using ControllerAs notation)
everything in the controller works fine. just the logUserOut method doesnt fire - at all! I'm at a loss.
controller:
.controller('DashCtrl', function($scope, $ionicModal, DataService, AuthService) {
var ctrl = this;
ctrl.icons = [];
//// SERVICE GETS
ctrl.allIcons = DataService.icons();
ctrl.pages = DataService.pages();
////
//// FILTER INFORMATION
ctrl.loadIcons = function() {
console.log(ctrl.pages)
for (icon in ctrl.allIcons) {
var i = ctrl.allIcons[icon];
// console.log(i)
if(i.active){
ctrl.icons.push(i);
}
}
};
////
//// ACTIONS
ctrl.logUserOut = function(){
console.log('this is not being called')
// AuthService.logout();
};
////
});
html:
<ion-modal-view>
<ion-pane >
<ion-content scroll="false" >
<section class = "modal-container">
<div class="item modal-header">
<button class="button button-left button-icon light ion-android-close" ng-click="ctrl.closeProfileModal()"></button>
<div class="light text-center">
<button class="button button-clear" ng-click="controller.closeProfileModal()">
<img class="avatar-modal" src="img/mike.png">
</button>
</div>
</div>
<div class = "row modal-profile-row">
<div class = "col">
<button class="button button-clear">
<span class="title light">Personal Info</span>
</button>
</div>
</div>
<div class = "row modal-profile-row">
<div class = "col">
<button class="button button-clear" ng-click="ctrl.logUserOut()">
<span class="title light">Sign Out</span>
</button>
</div>
</div>
</section>
</ion-content>
</ion-pane>
</ion-modal-view>
app.js:
.state('tab.dash', {
url: '/dash',
views: {
'tab-dash': {
templateUrl: 'templates/tab-dash.html',
controller: 'DashCtrl',
controllerAs: 'ctrl'
}
}
})
EDIT:
It turns out the problem was with Sublime Text : for some reason, there was a weird cache issue - I was editing the file but the changes weren't recognized. I'm Glad that I asked because now I am using 'controllerAs:' in the $stateProvider and not 'controller: someController as something'. Thank you all for your help!
In your app.js change to:
.state('tab.dash', {
url: '/dash',
views: {
'tab-dash': {
templateUrl: 'templates/tab-dash.html',
controller: 'DashCtrl',
controllerAs: 'ctrl' // <-- here
}
}
})
I'll explain a little more. In your controller you set a variable to 'this'. The name you use can be anything such as wowItsAVariable = this; however when you attach your controller you can use a completely different name such as controllerAs: 'wowSomeOtherName' and that is how you reference it in your html. The name doesn't matter and I would stay away from using names such as 'controller' as that doesn't tell anyone what controller you're trying to reference. Hope that helps.

Angular Scroll on Click

I am having an issue getting Angular Scroll to work.I am trying to scroll from the landing div to another section on the page with a button click. My code formatted really strangely, so let me know if further clarification is needed.
HTML
<div class="cover">
<div class="big-logo">
<i class="fa fa-trello"></i>
<span> My Kanban</span>
<br>
<button class="arrow" ng-click="bc.toLists()" du-smooth-scroll>
<i class="fa fa-angle-double-down fa-sm animated flash infinite" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="story-board content">
<button class="add-list" ng-click="bc.addingList = !bc.addingList">
Add List
</button>
<div ng-if="bc.addingList">
<form ng-submit="bc.addList(bc.newList)">
<input style="margin-left: 5px" ng-model="bc.newList.name"/>
<button type="submit">+</button>
</form>
</div>
<div class="list" ng-repeat="list in bc.lists">
<button style="font-size: 10px;background: none;border:none; color: black" ng-click="bc.removeList(list)">x</button>
<list-component list-obj="list"></list-component>
</div>
</div>
init.js
angular.module('kanban', ['duScroll'])
app.js
angular.module('kanban')
.component('boardComponent', {
templateUrl: 'app/components/board/board.html',
controller: BoardController,
controllerAs: 'bc'
})
BoardController.$inject = ['EsService']
function BoardController(EsService) {
var bc = this;
bc.lists = EsService.getLists();
bc.addingList = false;
bc.removeList = function(list){
EsService.removeList(list.id);
}
bc.addList = function(list){
EsService.createList(list);
bc.newList = {};
}
bc.toLists = function() {
bc.cover = angular.element(document.getElementsByClassName('cover'));
bc.content = angular.element(document.getElementsByClassName('content'));
bc.cover.scrollTo(bc.content, 0, 1000);
}
}
For a JQuery free answer, you can use $anchorScroll
Create your anchor link:
<button ng-click="$ctrl.scrollTo('anid')">Scroll</button>
Create the anchor to scroll to:
<div id="anid">Land here</div>
Then your controller:
controller: function($anchorScroll) {
this.scrollTo = function(id) {
$anchorScroll(id);
}
}
I would recommend letting your controller handle the scrolling as opposed to the directives. You will have much tighter control that way and can therefore debug any issues.
Here's an example using the scrollToElement method. Once you have this logic in place you can switch it out to any method you need.
Here's a working demo
angular
.module('app', ['duScroll'])
.component('cmpExample', {
templateUrl: 'path/to/template.html',
controller: function($document) {
var vm = this;
vm.scrollTo = function(id) {
$document
.scrollToElement(
angular.element(document.getElementById(id)), 0, 1000
);
}
}
});
html
<button ng-click="$ctrl.scrollTo('target')">
<div id="target">Content further down the page</div>

Unable to make angular-formly work within a ng-template of an angular-ui-bootstrap modal

I am using angular-formly to build a form inside an angular-ui-bootstrap modal, the following code works when the form is placed outside the modal template but it doesn't when placed inside the ng-template, it just doesn't print the fields at all.
I believe this should work but I don't know how the life-cycle of angular-formly runs, so I am unable to identify how to make the fields show up inside my bootstrap modal template.
The issue is clearly related to the ng-template, it appears not to render the form even if the fields array is passed correctly.
var app = angular.module("demo", ['dndLists', 'ui.bootstrap', 'formly', 'formlyBootstrap']);
app.controller('ModalInstanceCtrl', function ($scope, $uibModalInstance, items, User) {
var vm = this;
vm.loadingData = User.getUserData().then(function(result) {
vm.model = result[0];
vm.fields = result[1];
vm.originalFields = angular.copy(vm.fields);
console.log(vm);
});
});
app.controller("AdvancedDemoController", function($scope, $uibModal){
$scope.modalOpen = function(event, index, item){
var modalInstance = $uibModal.open({
animation: true,
templateUrl: 'myModalContent.html',
controller: 'ModalInstanceCtrl',
size: 'md',
resolve: {
items: function () {
return $scope.items;
}
}
});
modalInstance.result.then(function (selectedItem) {
$scope.selected = selectedItem;
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
};
});
In my view:
<!-- Template for a modal -->
<script type="text/ng-template" id="myModalContent.html">
<div class="modal-header">
<h3 class="modal-title">I'm a modal!</h3>
</div>
<div class="modal-body">
<div ng-if="vm.loadingData.$$state.status === 0" style="margin:20px 0;font-size:2em">
<strong>Loading...</strong>
</div>
<div ng-if="vm.loadingData.$$status.state !== 0">
<form ng-submit="vm.onSubmit()" novalidate>
<formly-form model="vm.model" fields="vm.fields" form="vm.form">
<button type="submit" class="btn btn-primary submit-button">Submit</button>
</formly-form>
</form>
</div>
<ul>
<li ng-repeat="item in items">
{{ item }}
</li>
</ul>
Selected: <b>{{ selected.item }}</b>
</div>
</script>
Any ideas?
When calling $uibModal.open you need to specify controllerAs: 'vm' (that's what you're template assumes the controller is defined as).

Angular-ui router doesn't update links

I am building a basic rss reader, Ihave a basic form which uploads a series of url into a dropdown menu, when a user chooses a specific one, I call an api to access it.
My form is the following:
<div class="panel-body" ng-controller="NewsCtrl">
<form class="form-inline" role="form">
<div class="input-group">
<div class="input-group-btn">
<button class="btn btn-info" type="button">{{loadButtonText}}</button>
<button class="btn btn-info dropdown-toggle" type="button" data-toggle="dropdown"><span class="caret"></span><span class="sr-only">Toggle-dropdown</span></button>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="rss in RSSList">
{{rss.Title}}
</li>
</ul>
<input type="text" class="form-control" autocomplete="off" placeholder="This is where your feed's url will appear" data-ng-model="url">
</div>
</div>
</form>
My routes/states are defined as follows:
var myApp = angular.module('myApp',['ngSanitize','ui.router', 'myAppControllers', 'myAppFactories']);
myApp.config(['$stateProvider','$urlRouterProvider','$locationProvider',
function ($stateProvider,$urlRouterProvider,$locationProvider) {
$stateProvider
.state('home', {
url:'/home',
templateUrl: 'views/partials/partial-home.html'
})
.state('app', {
url:'/api',
templateUrl: 'views/partials/partial-news.html',
controller: 'NewsCtrl'
});
$urlRouterProvider.otherwise('/home');
$locationProvider
.html5Mode(true)
.hashPrefix('!');
}]);
And my controller is the following:
var myApp = angular.module('myAppControllers',[]);
myApp.controller('NewsCtrl', ['$scope','MyService', function($scope, Feed){
$scope.loadButtonText = 'Choose News Feed ';
$scope.RSSList = [
{Title: "CNN ",
url: 'http://rss.cnn.com/rss/cnn_topstories.rss'},
{Title: "CNBC Top News ",
url: 'http://www.cnbc.com/id/100003114/device/rss/rss.html'},
];
//Loading the news items
$scope.loadFeed = function (url, e) {
$scope.url= url;
Feed.parseFeed(url).then(function (res) {
$scope.loadButtonText=angular.element(e.target).text();
$scope.feeds=res.data.query.results.item;
});
}
}]);
My problem arised when I changed to use ui-router, I know I have to change this line
{{rss.Title}}
by using ui-sref, but just changing to <a ui-sref="loadFeed(rss.url, $event);">{{rss.Title}}</a>
still doesn't upload my urls into my NewsCtrl controller, any clues?
Bonus question: When I insert a console.log just before $scope.loadButtonText and I open the developer console, it seems to upload NewsCtrl 2 times, why is that?
ui-sref="app"
Should work.
sref stands for "state" reference, instead of the familiar "hyperlink" reference. So it will look for a state defined with .state();
Alternatively, you can go with a good old fashioned url,
href="#/api"
This would have the same effect. sref is recommended by project developers.

Display list of items and edit them via AngularJS

I try to solve a classic problem using AngularJS: I need to display list of some entities and provide ability to add, edit and view details of this entities.
I implement two controllers: ListController to iterate list of entities and ItemController to display and save entity details. This is html code:
<div ng-app="myApp">
<a class="btn" data-toggle="modal" data-target="#modal">Add new item</a>
<div ng-controller="ListController">
<h4>List</h4>
<ul>
<li ng-repeat="item in list">
{{item.name}}
<a class="btn" data-toggle="modal" data-target="#modal" ng-click="editItem(item)">Edit item</a>
</li>
</ul>
</div>
<div id="modal" role="dialog" class="modal hide fade">
<div ng-controller="ItemController">
<div class="modal-header">
Item Dialog
</div>
<div class="modal-body">
<label for="txtName" />
<input type="text" id="txtName" ng-model="item.name" />
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="saveItem()" data-dismiss="modal">OK</button>
</div>
</div>
</div>
and controllers code:
var db_list = [{ name: "Test1" }, { name: "Test2" }];
var app = angular.module('myApp', []).
controller('ListController', function($scope, $rootScope) {
$scope.list = db_list;
$scope.editItem = function(item) {
$rootScope.item = item;
}
}).
controller('ItemController', function($scope, $rootScope) {
$scope.saveItem = function() {
db_list.push($rootScope.item);
$rootScope.item = null;
}
});
Also you can find the working ptototype at http://jsfiddle.net/yoyoseek/9Qntw/16/.
The general problem in this code that I store entity to display its description using scope of the ListController (via editItem()), but I need this stored entity details in the ItemController. I use $rootScope for sharing entity to edit and it looks like hack for me. Is it a normal practice?
This code has one more drawback: $rootScope.item have to been cleared on modal dialog hide.
It looks like the main problem here is that events triggered by data-toggle happen outside of your control and it's not part of the AngularJS bindings (I am new to it so I may be wrong).
Anyway, it seems like there is no way to cross-reference controllers in Angular, and the only way to get hold of them is via inspecting the DOM. But, once you get into that, you may as well initialize the scope directly (http://jsfiddle.net/B4kAW/4/):
var db_list = [{ name: "Test1" }, { name: "Test2" }];
var app = angular.module('myApp', []);
app.controller('ListController', function($scope) {
$scope.list = db_list;
$scope.editItem = function(item) {
angular.element(document.getElementById("modal")).scope().item = item;
};
});
app.controller('ItemController', function($scope) {
$scope.saveItem = function(item) {
//db_list.push(item);
//$rootScope.item = null;
};
});
Note:
The modal dialog here has no way of knowing whether it's opened for editing, or adding a new item (I commented out push).
Since the dialog is linked with "main" item in the list, it updates it instantly (can be seen while the dialog is open, on the background). You may need to copy it instead of using a reference.
Inspired by this answer. It looks like "the Angular way" around dialogs is to convert them into services.

Resources