How to avoid stacking navigation history in tab-bar states - angular-ui-router

Tab A - Tab B - Tab C
States like below;
tabs.a, tabs.b, tab.c
I want to close app like there is no navigation history when switching in each of tab states
For example: I was in Tab A then I clicked to Tab B and then I clicked to Tab C from now on if user pushes back button the app should close. In normal behaviour navigation history stacks up and if I push back button I'll go to Tab B from Tab C. How to avoid this behaviour
Below is my codes;
.state('tabs', {
url: "/tab",
abstract: true,
templateUrl: "templates/tabs.html"
})
.state('tabs.a', {
url: "/a",
views: {
'a-tab': {
templateUrl: "templates/a.html",
controller: 'AController'
}
}
}).state('tabs.b', {
url: "/b",
views: {
'b-tab': {
templateUrl: "templates/b.html",
controller: 'BController'
}
}
}).state('tabs.c', {
url: "/c",
views: {
'c-tab': {
templateUrl: "templates/c.html",
controller: 'CController'
}
}
});
<ion-tabs class="tabs-royal tabs-striped">
<ion-tab title="A" href="#/tab/a">
<ion-nav-view name="a-tab"></ion-nav-view>
</ion-tab>
<ion-tab title="B" href="#/tab/b">
<ion-nav-view name="b-tab"></ion-nav-view>
</ion-tab>
<ion-tab title="C" href="#/tab/c">
<ion-nav-view name="b-tab"></ion-nav-view>
</ion-tab>
</ion-tabs>

You can intercept the back button in each of your controllers.
From the documentation:
registerBackButtonAction(callback, priority, [actionId]) Register a
hardware back button action. Only one action will execute when the
back button is clicked, so this method decides which of the registered
back button actions has the highest priority.
For example, if an actionsheet is showing, the back button should
close the actionsheet, but it should not also go back a page view or
close a modal which may be open.
The priorities for the existing back button hooks are as follows:
Return to previous view = 100 Close side menu = 150 Dismiss modal =
200 Close action sheet = 300 Dismiss popup = 400 Dismiss loading
overlay = 500
Your back button action will override each of the above actions whose
priority is less than the priority you provide. For example, an action
assigned a priority of 101 will override the 'return to previous view'
action, but not any of the other actions.
In your controllers you can register a listener for the back button and exit if it has been pressed:
.controller('AController', function($scope, $ionicPlatform){
var deregister = $ionicPlatform.registerBackButtonAction(
function () {
ionic.Platform.exitApp();
}, 100
);
$scope.$on('$destroy', deregister)
})
.controller('BController', function($scope, $ionicPlatform){
var deregister = $ionicPlatform.registerBackButtonAction(
function () {
ionic.Platform.exitApp();
}, 100
);
$scope.$on('$destroy', deregister)
})
.controller('CController', function($scope, $ionicPlatform){
var deregister = $ionicPlatform.registerBackButtonAction(
function () {
ionic.Platform.exitApp();
}, 100
);
$scope.$on('$destroy', deregister)
});
NOTES:
Your last tab (C TAB) should have the name: c-tab:
<ion-tab title="C" href="#/tab/c">
<ion-nav-view name="c-tab"></ion-nav-view>
</ion-tab>

I have done it with a simple way
Added a function in controller scope of tabs state
.state('tabs', {
url: "/tab",
abstract: true,
templateUrl: "templates/tabs.html",
controller: function($scope, $ionicTabsDelegate, $ionicHistory) {
$scope.rt = function(e, index) {
$ionicTabsDelegate.select(index);
//e.preventDefault();
$ionicHistory.nextViewOptions({historyRoot:true});
}
}
})
and added ng-click on each ion-tab directive in tabs.html template like below;
<ion-tab ng-click="rt($event, 0)" title="A" href="#/tab/a">
The second parameter of rt function is the index of tab
I used $ionicHistory.nextViewOptions({historyRoot:true});
From the documentation
nextViewOptions()
Sets options for the next view. This method can be
useful to override certain view/transition defaults right before a
view transition happens. For example, the menuClose directive uses
this method internally to ensure an animated view transition does not
happen when a side menu is open, and also sets the next view as the
root of its history stack. After the transition these options are set
back to null.
Available options:
disableAnimate: Do not animate the next transition.
disableBack: The next view should forget its back view, and set it to null.
historyRoot: The next view should become the root view in its history stack.
That way I achieved what I want

Related

ExtJS 6 Make ViewController talk to each other

I have two ViewController.
The first one fires an event if I select an item in a treepanel :
// treeController
onItemSelection: function(treeview, record) {
var me = this,
controller = me.getView().getController();
...
controller.fireEvent('myEvent', record);
}
The second one is listening to this event.
The controller is responsible for uploading a file to a specified url.
This url is set by the onMyEvent-function.
// uploadController
...
listen: {
controller: {
'*': {
myEvent: 'onMyEvent'
}
}
},
defaultUrl: 'foo/bar/{id}',
currentUrl: null,
onMyEvent: function(record) {
var me = this;
me.currentUrl = me.defaultUrl.replace('{id}', record.getId());
},
onUploadClick: function (form) {
var me = this;
if (form.isValid()) {
form.submit({
url: me.currentUrl,
...
});
}
},
...
What I want to achieve:
I select an item in the treepanel
-> the event is fired, the onMyEvent-function has been executed.
I click on the button to open the uploadView (the view which is connected to the controller). After that I'll click on the fileupload-button, select a file and click on the uploadbutton.
After the uploadbutton has been pressed, the controller should call the onUploadClick-function and use the previous placed url (currentUrl) for the upload.
The problems I'm facing:
Selecting an item in the treepanel fires the event, but the uploadController is not executing the onMyEvent-function.
When I open the uploadView first and select afterwards a node in the panel, the onMyEvent-function is executed.
When I use the second approach and try to upload the file, I get an error which tells me I haven't specifed the url (its null).
How can I accomplish the process without using the mentioned workaround for 1.?
Thanks in advance.
Your event myEvent is in UploadController. However, controller.fireEvent('myEvent', record); would try to find and fire it in TreeController.
Root cause of this issue is that controller = me.getView().getController(); is going to give you back this/instance of TreeController. When you do me.getView(),it gives you TreeView and me.getView().getController() is going to give you back an instance of TreeController and you need an instance of UploadController cause myEvent is an event of UploadController.
The reason you're able to fire the event when you open UploadView first is cause you're already in UploadController.
Hope that helps!

Encapsulation with React child components

How should one access state (just state, not the React State) of child components in React?
I've built a small React UI. In it, at one point, I have a Component displaying a list of selected options and a button to allow them to be edited. Clicking the button opens a Modal with a bunch of checkboxes in, one for each option. The Modal is it's own React component. The top level component showing the selected options and the button to edit them owns the state, the Modal renders with props instead. Once the Modal is dismissed I want to get the state of the checkboxes to update the state of the parent object. I am doing this by using refs to call a function on the child object 'getSelectedOptions' which returns some JSON for me identifying those options selected. So when the Modal is selected it calls a callback function passed in from the parent which then asks the Modal for the new set of options selected.
Here's a simplified version of my code
OptionsChooser = React.createClass({
//function passed to Modal, called when user "OK's" their new selection
optionsSelected: function() {
var optsSelected = this.refs.modal.getOptionsSelected();
//setState locally and save to server...
},
render: function() {
return (
<UneditableOptions />
<button onClick={this.showModal}>Select options</button>
<div>
<Modal
ref="modal"
options={this.state.options}
optionsSelected={this.optionsSelected}
/>
</div>
);
}
});
Modal = React.createClass({
getOptionsSelected: function() {
return $(React.findDOMNode(this.refs.optionsselector))
.find('input[type="checkbox"]:checked').map(function(i, input){
return {
normalisedName: input.value
};
}
);
},
render: function() {
return (
//Modal with list of checkboxes, dismissing calls optionsSelected function passed in
);
}
});
This keeps the implementation details of the UI of the Modal hidden from the parent, which seems to me to be a good coding practice. I have however been advised that using refs in this manner may be incorrect and I should be passing state around somehow else, or indeed having the parent component access the checkboxes itself. I'm still relatively new to React so was wondering if there is a better approach in this situation?
Yeah, you don't want to use refs like this really. Instead, one way would be to pass a callback to the Modal:
OptionsChooser = React.createClass({
onOptionSelect: function(data) {
},
render: function() {
return <Modal onClose={this.onOptionSelect} />
}
});
Modal = React.createClass({
onClose: function() {
var selectedOptions = this.state.selectedOptions;
this.props.onClose(selectedOptions);
},
render: function() {
return ();
}
});
I.e., the child calls a function that is passed in via props. Also the way you're getting the selected options looks over-fussy. Instead you could have a function that runs when the checkboxes are ticked and store the selections in the Modal state.
Another solution to this problem could be to use the Flux pattern, where your child component fires off an action with data and relays it to a store, which your top-level component would listen to. It's a bit out of scope of this question though.

AngularJS directive toggle menu preventing default for other directive

So I made a directive for a toggle (drop down) menu in AngularJS. I used the directive for multiple items within the page but I have a small problem. When one item is open and I click another one I want the previous one to close. The event.preventDefault and event.stopPropagation stops the event for the previous item and doesn't close it. Any ideas on how to fix this? Is there a way to perhaps only stop the event within the scope?
app.directive('toggleMenu', function ($document) {
return {
restrict: 'CA',
link: function (scope, element, attrs) {
var opened = false;
var button = (attrs.menuButton ? angular.element(document.getElementById(attrs.menuButton)) : element.parent());
var closeButton = (attrs.closeButton ? angular.element(document.getElementById(attrs.closeButton)) : false);
var toggleMenu = function(){
(opened ? element.fadeOut('fast') : element.fadeIn('fast'));
};
button.bind('click', function(event){
event.preventDefault();
event.stopPropagation();
toggleMenu();
opened = ! opened;
});
element.bind('click', function(event){
if(attrs.stayOpen && event.target != closeButton[0]){
event.preventDefault();
event.stopPropagation();
}
});
$document.bind('click', function(){
if(opened){
toggleMenu();
opened = false;
}
});
}
};
And here's a Fiddle: http://jsfiddle.net/JknUJ/5/
Button opens content and content should close when clicked outside the div. When clicked on button 2 however content 1 doesn't close.
Basic idea is that you need to share the state between all your dropdown submenus, so when one of them is shown, all others are hidden. The simpliest way of storing state (such as opened or closed) are... CSS classes!
We'll create a pair of directives - one for menu, and another for sumbenu. It is more expressive that just divs.
Here is out markup.
<menu>
<submenu data-caption="Button 1">
Content 1
</submenu>
<submenu data-caption="Button 2">
Content 2
</submenu>
</menu>
Look how readable is it! Say thanks to directives:
plunker.directive("menu", function(){
return {
restrict : "E",
scope : {},
transclude : true,
replace : true,
template : "<div class='menu' data-ng-transclude></div>",
controller : function ($scope, $element, $attrs, $transclude){
$scope.submenus = [];
this.addSubmenu = function (submenu) {
$scope.submenus.push(submenu);
}
this.closeAllSubmenus = function (doNotTouch){
angular.forEach($scope.submenus, function(submenu){
if(submenu != doNotTouch){
submenu.close();
}
})
}
}
}
});
plunker.directive("submenu", function(){
return {
restrict : "E",
require : "^menu",
scope : {
caption : "#"
},
transclude : true,
replace : true,
template : "<div class='submenu'><label>{{caption}}</label><div class='submenu-content' data-ng-transclude></div></div>",
link : function ($scope, $iElement, $iAttrs, menuController) {
menuController.addSubmenu($scope);
$iElement.bind("click", function(event){
menuController.closeAllSubmenus($scope);
$iElement.toggleClass("active");
});
$scope.close = function (){
$iElement.removeClass("active");
}
}
}
});
Look thar we restricted them to HTML elements (restrict : "E"). submenu requires to be nested in menu (require : "^menu"), this allows us to inject menu controller to submenu's link function. transclude and replace controls the position of original markup in compiled HTML output (replace=true means that original markup will be replaced with compiled, transclude inserts parts of original markup to compiled output).
When we've done with this, we just say to menu close all your child menus! and menu iterates over submenus, forcing them to close.
We are adding childs to menu controller in addSubmenu function. It is called in submenus link function, thus every compiled instance of submenu adds itself to menu. Now, closing all submenus is as easy as iterating over all children, this is done by closeAllSubmenus in menu controller.
Here is a full Plunker to play with.

How Do I Reselect A KendoUI TabStrip After AJAX Postback in UpdatePanel

Setup
I've got a Telerik Kendo UI TabStrip with multiple tabs inside of an UpdatePanel...
<asp:UpdatePanel ID="DataDetails_Panel" UpdateMode="Conditional" runat="server">
<div id="ABIOptions_TabContainer">
<ul>
<li>Attendance</li>
<li>Grades</li>
<li>Gradebook</li>
<li>PFT</li>
<li>Scheduling</li>
<li>Miscellaneous</li>
<li>Parent Data Changing</li>
</ul>
</div>
</asp:UpdatePanel>
...which I then wire up in javascript later...
var optionTabContainer = $("#ABIOptions_TabContainer").kendoTabStrip({
animation: {
open: {
effects: "fadeIn"
}
},
select: onMainTabSelect
}).data("kendoTabStrip");
Scenario
The users will click on the various tabs and inside of each tab are settings for our portal. When they are in a tab and they make a change to a setting, the expectation is that they'll click on the 'Save' button, which will perform a postback to the server via ajax, because it is in the update panel.
Current Behavior
After the post back happens and the ul content comes back, I reapply the kendoTabStrip setup function call, which makes none of the tabs selected. This appears to the user like the page is now empty, when it just had content.
Desired Result
What I want to do, is after the partial postback happens and the UpdatePanel sends back the ul, I want to reselect the tab that the user previously selected.
What Already Works
I already have a way to preserve the tab that the user clicked on:
var onMainTabSelect = function (e) {
tabToSelect = e.item;
console.log("onTabSelect --> ", e.item.textContent);
}
and a function to reset the selected tab whenever it is called:
function setMainTab() {
if (!jQuery.isEmptyObject(tabToSelect)) {
var tabStrip = $('#ABIOptions_TabContainer').data("kendoTabStrip");
console.log("Attempt to set tab to ", tabToSelect.textContent);
tabStrip.select(tabToSelect);
} else {
console.log("tabToSelect was empty");
}
}
What Doesn't Work
My hypothesis is that the Kendo TabStrip says, "Hey, that tab is already selected" when I call the setMainTab after my postback:
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_endRequest(function () {
BindControlEvents();
setMainTab();
});
...and therefore, doesn't set my tab back. If I click on the tab, then Poof, all my content is there just like I expect.
Any ideas what I may be doing wrong?
I ended up changing the onMainTabSelect method to:
var onMainTabSelect = function (e) {
tabToSelect = $(e.item).data("tabindex");
}
which gets me the data-tabindex value for each li in my ul. I couldn't get the tab index from kendo, so I had to role my own. Once I got that value, then I was able to set the selected tab via an index rather than the tab object reference itself.

mvc view insert into wrong "DOM" telerik window

Apologies in advance if this becomes a very long question...
Background Info
I have an MVC 3 application, using Telerik components and this particular issue is specific (I think) to the Window() component.
From my main view (Index.cshtml) I executing an ajax request to return a partial view which is what I am populating the contents of my window with. This is the jquery which is executing the request:
var url = '#Url.Action("GetAddPart", "Jobs")';
var window = $("#Window").data("tWindow");
var data = $("#indexForm").serialize();
window.ajaxRequest(url, data);
window.center().open();
the controller action is:
[HttpGet]
public ActionResult GetAddPart(MDTCompletedJobM model)
{
// show the AddPart window
//TryUpdateModel<MDTCompletedJobM>(model);
model.ActionTakenList = ActionTakenList;
model.ProblemTypes = ActualProblemList;
var addPartM = new MDTAddPartM() { CompletedJobM = model };
return PartialView(string.Concat(ViewDefaultUrl, "AddPart.cshtml"), addPartM);
}
this opens my window hunky dory.
in my partial view i have a form with two or three fields and an "Add", "Cancel button. For the sake of brevity I'll just show what I think are the relevant parts of the partial view, but i can produce the entire view if need be:
<div id="resultDiv">
#using (Html.BeginForm("AddPart", "Jobs", FormMethod.Post, new { id = "addPartForm", name = "addPartForm" }))
{
** layout components removed from here **
<input type="button" value="Add" class="t-button" id="btnAdd"/>
<input type="button" value="Cancel" class="t-button" id="btnCancel"/>
<p />
<div id="progressdiv">
</div>
}
</div>
is the "top level" tag in the partial view.
my jquery to handle the Cancel button is:
$("#btnCancel").click(function () {
var window = $("#Window").data("tWindow");
window.close();
});
In my main view, I have a submit button, which when completed effectively reders the main view "disabled" or displays errors. The action for this submit button returns the main view:
Controller's action snippet:
if (ViewData["DoPayJobWarningStr"] == null)
return RedirectToAction("GetAutoDispatchJob", new { autoDispatchJob = model.JobReferenceNumber});
else
return View(string.Concat(ViewDefaultUrl, "Index.cshtml"), tmpModel);
My actual problem
For a specific example I am using, I am expecting ViewData["DoPayJobWarningStr"] NOT to be null, there the return View(...) will be executed.
if I execute this action (for the submit button) without opening the window, my view returns correctly and the page is updated to show the warning message. However, if I open the window first then execute the submit button, the View isn't updated on the page, but seems to be placed into the Window's html. ie, if I hit the submit button (nothing happens), then click on the button which opens the Telerik window, I briefly see the View returned by the submit Action being shown before it's updated with what the Partial View should contain. I don't understand at all how or why the returned View is being placed there?
Things I've tried:
Commenting out the ajax request (window.ajaxRequest(url, data);) fixes the issue (even though I obviously have a blank partial view to look at).
Not making the Partial View a "Form" doesnt' work
No matter how I "close" the window, the view is still placed within there. eg clicking the x in the top right hand corner
Using Firebug, the HTML after the submit button is clicked is not updated.
Rather than using "window.ajaxRequest(url, data)", i've also tried (with the same result):
$.ajax({
type: "GET",
cache: false,
url: url,
data: $("#indexForm").serialize(),
success: function (data) {
var window = $("#Window").data("tWindow");
window.content(data);
window.center().open();
$("#progress").html('')
},
error: function (e) {
alert(e.Message);
}
});
Is it all possible to determine what I am doing wrong? Is it the ajax request? Only assuming that because removing it fixes the issue, but there might be more to it than that.
Thanks and of course if you need more info, ask :)
Thanks
EDIT
After suggestions from 3nigma, this is what I've updated to (still no luck)...
Telerik window definition:
#{Html.Telerik().Window()
.Name("Window")
.Title("Add Part")
.Draggable(true)
.Modal(true)
.Width(400)
.Visible(false)
.Height(270)
.ClientEvents(e => e.OnOpen("onWindowOpen"))
.Render();
}
jquery function which is an OnClick event for a button:
function AddBtnClicked() {
$("#Window").data('tWindow').center().open();
}
the onWindowOpen event:
function onWindowOpen(e) {
//e.preventDefault();
var data = #Html.Raw(Json.Encode(Model));
var d2 = $.toJSON(data);
$.ajax({
type: 'GET',
url: '#Url.Action("GetAddPart", "Jobs")',
data: d2,
contentType: 'application/json',
success: function (data) {
var window = $("#Window").data("tWindow");
window.content(data);
$("#progress").html('');
},
error: function (xhtr, e, e2) {
alert(e +'\n' + xhtr.responseText);
}
});
};
OK.
Issue was related to this line in the Partial View:
<script src="#Url.Content("~/Scripts/ValidationJScript.js")" type="text/javascript"></script>
As soon as I took that out, it all works. Not sure why that was causing the issue - I don't think it had been previously been loaded but maybe it had which was causing the problem.
Thanks to 3nigma for your thoughts, nonetheless!

Resources