In a ticket entry page, I have a main ticketEntry.html page, which contains one grid for the lines and one for the payments.
When ticketEntry.html is loaded, it must first retrieve the ticket view model (via ajax calls to Web API). The line and payment grid cannot retrieve their data until the ticket view model has been received.
In my current solution, I have to use $timeout in the controller for ticketEntry.html for this to work. I am looking for a cleaner way.
Extracts from ticketEntry.html:
<div ng-controller="ticketLineController">
<div id="ticketLineGridDiv" kendo-grid="ticketLineGrid" k-options="ticketLineGridOptions"></div>
</div>
...
<div ng-controller="ticketPaymentController">
<div id="ticketPaymentGridDiv" kendo-grid="ticketPaymentGrid" k-options="ticketPaymentGridOptions"></div>
</div>
In the controller for ticketEntry.html, I have this:
$timeout(function () {
ticketService.getTicket(ticketId).then(
function(ticket) {
$scope.initPos(ticket);
},
...);
}, 500);
$scope.initPos = function(ticket) {
$scope.ticket = ticket; <-- $scope.ticket is used by the line and payment grid
$scope.$broadcast('PosReady'); <-- Tell the payment and line controllers to load their grids
}
As you can see, I am using $timeout to delay for 500ms, then I get the ticket view model and broadcast to the line and payment controller that they now can load their grids.
Here is the listener in the line controller:
$scope.$on('PosReady', function (event) {
$scope.ticketLineGrid.setDataSource(getGridDataSource());
$scope.ticketLineGrid.dataSource.read();
});
The problem is that if I do not use $timeout in the ticket entry controller, $scope.ticketLineGrid is sometimes undefined here (same thing with the payments controller).
I have tried using angular.element(document).ready(function () {...} instead of $timeout in the ticket entry controller, but that did not handle the issue.
How do I know when $scope.ticketLineGrid (for example) has been created/defined?
What is the proper way of handling this kind of scenario?
Update 9/27/2014, to provide more data on how the ticket line grid gets initialized:
In the AngularJs directive in ticketEntry.html, the k-options specifies the definition object for the grid:
<div id="ticketLineGridDiv" kendo-grid="ticketLineGrid" k-options="ticketLineGridOptions"></div>
ticketPaymentGridOptions is just an object with properties that defines the grid:
$scope.ticketPaymentGridOptions = {
autoBind: false,
height: 143,
columns: [
{
field: "payCode", title: "PayCode",
},
{
field: "amount", title: "Amount", format: "{0:n2}", attributes: { style: "text-align:right" },
},
],
pageable: false,
...
};
Update 9/29/2014: This is the solution I went with, based on suggestion by Valentin
I use two watches - one in the child scope where the ticketLineGrid lives:
$scope.$watch('ticketLineGrid', function (newVal) {
if (angular.isDefined(newVal)) {
$scope.ticketControl.lineGridReady = true;
}
});
This watch sets the parent property $scope.ticketControl.lineGridReady = true once the grid has been initialized.
The parent (ticketEntryController) has watches for lineGridReady:
$scope.$watch('ticketControl.lineGridReady', function (gridReady) {
if (gridReady) {
$scope.loadPage();
}
});
$scope.loadPage = function () {
ticketService.getTicket(ticketId).then(
function (ticket) {
$scope.initPos(ticket);
}
...
}
Not as clean as I would have liked it, but certainly better than using $timeout...
How do I know when $scope.ticketLineGrid (for example) has been created/defined?
You could use a $scope.$watch statement :
$scope.$watch('ticketLineGrid', function (newVal, oldVal) {
if(angular.isDefined(newVal)){
// do something with it
}
})
However, in my view the good way to do this is to retrieve the data not from a scope property, but from a promise. I would use only promises and no events at all for this :
var ticketPromise = ticketService.getTicket(ticketId);
ticketPromise.then(function (ticket) {
$scope.ticket = ticket;
});
// you know that part better than I do
var ticketLineGridPromise = ...;
$q.all([ticketPromise, ticketLineGridPromise])
.then(function (realizations) {
var ticket = realizations[0], ticketLineGrid = realizations[1];
$scope.ticketLineGrid.setDataSource(getGridDataSource());
$scope.ticketLineGrid.dataSource.read();
})
I can't be more precise because it's not clear from your code what initializes ticketLineGrid.
Finally, in many cases it's very handy to use resolve clauses in your route declaration.
Related
I am using this plugin - bootstrap-dropselect
I have written initDropSelect function to initialize this plugin but I am not sure where to call this function as I would like to append some html to the DOM as soon as route is loaded. I am getting data from two different ajax calls. That data has to be compared and manipulated to append that html to the DOM(Code below 'Append to DOM' comment).
let UserPanel = React.createClass({
mixins: [LinkedStateMixin],
getStateFromStores: function() {
var users = UserStore.getAll();
// Some more code
return {
users: users
// Other properties
};
}
componentDidMount: function() {
UserStore.addChangeListener(this._onChange);
},
_onChange: function() {
this.setState(this.getStateFromStores());
},
initDropSelect: function() {
var _self = this;
var dropSelect = $('#dropselect-demo1').dropselect({
filter: {
show: true,
placeholder: 'Search for an item'
},
multiselect: true,
onselect: function(e, item) {
},
onunselect: function(e, item) {
},
onclear: function(e) {
}
});
// Append to DOM
if(this.state.tagsList.length > 0) {
if(this.state.newLoan.data.tags.length > 0) {
// Getting data from two different resources
}
}
}
});
Please help me in deciding where to call initDropSelect to manipulate data from multiple async requests and append that data to DOM.
P.S. I am using react router so there are two scenarios. First I may come to this route from other route or I can straightaway reload the current page.
Thanks in advance.
One way is for your render method to return something like a <div /> that you will then use to mount the jQuery component into. Once mounted, this will point to the DOM element that you would normally pass to jQuery.
Since jQuery will be handling the rendering, you then want to always return false from shouldComponentUpdate(). This will prevent React from thrashing your jQuery component.
You can then use componentDidMount() to initialize your jQuery component and componentWillReceiveProps() to update/re-render it when new data is passed in.
I'm using Backbone Marionette to render layouts on my page. However, after the initial rendering, I try to re-render one of the partial templates on the page after issuing a refund by triggering ("RefModal:PayRef"). Unfortunately, I end up with the following error:
'Uncaught TypeError: Cannot read property 'show' of undefined'
After some debugging, it appears I no longer have access to totalRegion as "that.totalRegion" becomes undefined hence the error ("that" still appears to have scope however).
If anyone could help with a way to access "totalRegion" on the second, third, fourth, etc, triggering of "RefModal:PayRef" that would be awesome. I hope this is enough information to go on.
views.OrderDTLayout = Marionette.Layout.extend({
template: "orderManagement/bb-total-layout.dust",
className: "bbMgmt",
regions: {
totalRegion: "#bb-total-region",
shippingRegion: "#bb-global-shipping-region"
},
events: {
"click #bb-cancel": "showCancelModal",
"keypress #bb-cancel": "handleShowCancelModal"
},
initialize: function () {
var that = this;
var spinnerMod = mad.App.module("Seller.OrdMgmt.Spinner");
this.loadingController = new spinnerMod.Controller();
mad.App.vent.on("RefModal:PayRef", function (orderModel) {
var totalView = mad.App.Seller.OrdMgmt.OrdTotals.Controller.createPackageOrderTotals(orderModel);
that.totalRegion.show(totalView);
});
//triggered from separate file
mad.App.vent.trigger("RefModal:PayRef", order)
Try binding the context of this to your event handler:
mad.App.vent.on("RefModal:PayRef", function (orderModel) {
var totalView = mad.App.Seller.OrdMgmt.OrdTotals.Controller.createPackageOrderTotals(orderModel);
that.totalRegion.show(totalView);
}, this);
Notice this passed in as the last argument. Then you can access this.totalRegion.show(view)
I'm learning about Session and reactive data sources in Meteor JS. They work great for setting global UI states. However, I can't figure out how to scope them to a specific instance of a template.
Here's what I'm trying to do
I have multiple contenteditable elements on a page. Below each is an "Edit" button. When the user clicks on the Edit button, it should focus on the element and also show "Save" and "Cancel" buttons.
If the user clicks "Cancel", then any changes are eliminated, and the template instance should rerender with the original content.
Here's the code I have so far
// Helper
Template.form.helpers({
editState: function() {
return Session.get("editState");
}
});
// Rendered
Template.form.rendered = function(e){
var $this = $(this.firstNode);
var formField = this.find('.form-field');
if (Session.get("editState")) formField.focus();
};
// Event map
Template.form.events({
'click .edit-btn' : function (e, template) {
e.preventDefault();
Session.set("editState", "is-editing");
},
'click .cancel-btn' : function (e, template) {
e.preventDefault();
Session.set("editState", null);
},
});
// Template
<template name="form">
<div class="{{editState}}">
<p class="form-field" contenteditable>
{{descriptionText}}
</p>
</div>
Edit
Save
Cancel
</template>
// CSS
.edit-btn
.cancel-btn,
.save-btn {
display: inline-block;
}
.cancel-btn,
.save-btn {
display: none;
}
.is-editing .cancel-btn,
.is-editing .save-btn {
display: inline-block;
}
The problem
If I have more than one instance of the Form template, then .form-field gets focused for each one, instead of just the one being edited. How do I make so that only the one being edited gets focused?
You can render a template with data, which is basically just an object passed to it when inserted in to a page.
The data could simply be the key to use in the Session for editState.
eg, render the template with Template.form({editStateKey:'editState-topForm'})
you could make a handlebars helper eg,
Handlebars.registerHelper('formWithOptions',
function(editStateKey){
return Template.form({editStateKey:editStateKey})
});
then insert it in your template with
{{{formWithOptions 'editState-topForm'}}} (note the triple {, })
Next, change references from Session.x('editState') to Session.x(this.editStateKey)/ Session.x(this.data.editStateKey)
Template.form.helpers({
editState: function() {
return Session.get(this.editStateKey);
}
});
// Rendered
Template.form.rendered = function(e){
var $this = $(this.firstNode);
var formField = this.find('.form-field');
if (Session.get(this.data.editStateKey)) formField.focus();
};
// Event map
Template.form.events({
'click .edit-btn' : function (e, template) {
e.preventDefault();
Session.set(this.editStateKey, "is-editing");
},
'click .cancel-btn' : function (e, template) {
e.preventDefault();
Session.set(this.editStateKey, null);
},
});
Note: if you are using iron-router it has additional api's for passing data to templates.
Note2: In meteor 1.0 there is supposed to be better support for writing your own widgets. Which should allow better control over this sort of thing.
As a matter of policy I avoid Session in almost all cases. I feel their global scope leads to bad habits and lack of good discipline regarding separation-of-concerns as your application grows. Also because of their global scope, Session can lead to trouble when rendering multiple instances of a template. For those reasons I feel other approaches are more scalable.
Alternative approaches
1 addClass/removeClass
Instead of setting a state then reacting to it elsewhere, can you perform the needed action directly. Here classes display and hide blocks as needed:
'click .js-edit-action': function(event, t) {
var $this = $(event.currentTarget),
container = $this.parents('.phenom-comment');
// open and focus
container.addClass('editing');
container.find('textarea').focus();
},
'click .js-confirm-delete-action': function(event, t) {
CardComments.remove(this._id);
},
2 ReactiveVar scoped to template instance
if (Meteor.isClient) {
Template.hello.created = function () {
// counter starts at 0
this.counter = new ReactiveVar(0);
};
Template.hello.helpers({
counter: function () {
return Template.instance().counter.get();
}
});
Template.hello.events({
'click button': function (event, template) {
// increment the counter when button is clicked
template.counter.set(template.counter.get() + 1);
}
});
}
http://meteorcapture.com/a-look-at-local-template-state/
3 Iron-Router's state variables
Get
Router.route('/posts/:_id', {name: 'post'});
PostController = RouteController.extend({
action: function () {
// set the reactive state variable "postId" with a value
// of the id from our url
this.state.set('postId', this.params._id);
this.render();
}
});
Set
Template.Post.helpers({
postId: function () {
var controller = Iron.controller();
// reactively return the value of postId
return controller.state.get('postId');
}
});
https://github.com/iron-meteor/iron-router/blob/devel/Guide.md#setting-reactive-state-variables
4 Collection data
Another approach is to simply state by updating data in your collection. Sometimes this makes perfect sense.
5 update the data context
Session is often the worse choice in my opinion. Also I don't personally use #3 as I feel like being less tied to iron-router is better incase we ever want to switch to another router package such as "Flow".
in my ExtJS application I want to reset the stores when I change the page.
Which means I don't want any filters, groupings or listener from any old view/page.
Currentyl I am setting the store of my view like this:
{
xtype: 'admingrid',
...
columns: [
...
],
store: 'appname.store.administration.User'
}
I am loading the store like that:
onAfterRender: function() {
//load all users
this.setUserStore(this.getUserGrid().getStore());
this.getUserStore().load();
},
and in some cases like this:
onAfterRender: function() {
//load all users
this.setUserStore(Ext.StoreManager.lookup('appname.store.administration.User'));
this.getUserStore().load();
},
all my my pages extends my Base view and so I thought I just do something like this:
Ext.define("appname.view.Base", {
extend: 'Ext.panel.Panel',
ui: 'basepanel',
padding: 15,
contentPaddingProperty: 'padding',
listeners: {
beforedestroy: function() {
Ext.StoreManager.each(function (item, index, len) {
item.clearFilter(true); // param: suppressEvent
item.clearGrouping();
item.clearListeners(); // this will also remove managed listeners
});
}
}
});
this will cause that the grid is sometimes empty when I am entering the view the first time... I don't understand why.. when I am entering the view the second or sometimes the third time it does show the grid with the entries..
Is there a common way to accomplish such a thing? What I am doing wrong? I don't understand why this is happening.
Just clear the store if it's present in your afterrender event. That way, you'll re-use the store and just clear it.
So in your afterrender, use StoreManager to lookup the store. If its present, it's been created before and you can clear it.
var store = Ext.StoreManager.lookup('storeid');
if(store) {
store.clearFilter(true);
store.clearGrouping();
}
This will clear the store whenever it's present.
I am investigating Angular as a potential framework to use for an upcoming project. The test app I am making consists of an unordered list of which list items can be added to via an add link. Each list item contains a number of checkboxes. The number of checkboxes for a particular list item can be increased or decreased with plus and minus links next to each list item. Observe:
Hopefully that makes sense. Each checkbox has an ng-model directive binding the value of the checkbox to a property in an object. When the application is in the state above clicking any of the checkboxes fires six checks (one for each checkbox) -- the entire $scope of the root controller is checked for changes. Ideally, only the $scope of the relevant list item would be checked for changes. How can I accomplish this? I've attached my test code for reference. I've tried adding ng-click="$event.stopPropagation()" to the input node as well as to the li node but this appears to increase (double) the number of checks in the digest.
HTML:
<div ng-app ng-controller="App">
<ul>
<li ng-repeat="line in lines" ng-controller="LineController">
<input type="checkbox" ng-repeat="box in line.boxes" ng-model="box.on" />
<a ng-show="line.boxes.length > 1" ng-click="removeBox()">-</a>
<a ng-click="addBox()">+</a>
</li>
</ul>
<a ng-click="addLine()">Add</a>
</div>
JavaScript:
function App($scope) {
$scope.lines = [];
$scope.addLine = function () {
$scope.lines.push({
boxes: []
});
};
}
function LineController($scope) {
$scope.addBox = function () {
var box = {};
Object.defineProperty(box, 'on', {
enmerable: true,
get: function () {
console.log('Get!');
return this._on;
},
set: function (on) {
this._on = on;
}
});
$scope.line.boxes.push(box);
};
$scope.removeBox = function () {
$scope.line.boxes.pop();
};
}
If your concern is that AnguarJS dirty checking is going to be too slow for your needs, your question really need to be "is AngularJS going to be to slow to build X?" If X is a 3D game with lots of constant rendering then the answer is probably yes, AngularJS is not what you want. If X is "a scalable business/consumer oriented single page application", then the dirty checking algorithm is not going to be your bottle neck.
This SO answer has a good explanation of how data binding works and talks a bit about performance concerns.
What about to use $watch. We can invoke watch only for specific row. That means if you have 4x4 matrix (4 rows , 4 columns) on any checkbox state change we call watch 4 times
var webApp = angular.module('myModule', []);
webApp.controller('App', function ($scope) {
$scope.lines = [];
$scope.addLine = function () {
console.log("addLine");
$scope.lines.push({
boxes: []
});
};
});
webApp.controller('LineController', function ($scope) {
$scope.addBox = function () {
var box = {};
/* Object.defineProperty(box, 'on', {
enmerable: true,
get: function () {
console.log('Get!');
return this._on;
},
set: function (on) {
this._on = on;
}
});*/
$scope.line.boxes.push(box);
$scope.$watch(function () {
return $scope.line.boxes;
},
function (newValue, oldValue) {
if(newValue == oldValue) return;
console.log('Get new checkbox!');
}, true);
};
$scope.removeBox = function () {
$scope.line.boxes.pop();
};
});
Demo Fiddle