I am trying to namespace events emitted by multiple child controllers of the same type. The problem is letting the parent know the child id. Consider the following scenario.
<div ng-controller="Parent">
<div ng-include="child1"></div>
<div ng-include="child2"></div>
... and so on, the parent decides which templates to include on the fly
</div>
<script type="text/ng-template" id="Child.html">
<div ng-controller="Child">...</div>
</script>
function Parent($scope) {
$scope.child1 = "Child.html";
$scope.child2 = "Child.html";
// The problem is setting these
$scope.childId1 = 1;
$scope.childId2 = 2;
$scope.on($scope.childId1 + ".someEvent", function() {
// Handle event from child 1
});
$scope.on($scope.childId2 + ".someEvent", function() {
// Handle event from child 2
});
}
function Child($scope) {
$scope.id = getNewId();
// Here I would like to do something like
$parent[someKey] = $scope.id;
// So that I can emit events like this
$scope.someEvent = function() {
$scope.$emit($scope.id + ".someEvent", data);
}
}
So I think the problem has two parts.
1.) How do I pass someKey from the parent to the child.
2.) How do I set someKey on the parent scope from within the child. Would like to avoid using $parent. Not even sure if this variable is available in the controller?
Use the messaging to transmit the id you want to use
I would switch your "$on" message in your parent to look like this:
$scope.on(".someEvent", function(event,data,id) {
switch(id) {
case 1:
// Handle event from child 1
case 2:
// Handle event from child 2
}
});
And your emit messaging to look like this:
$scope.someEvent = function() {
$scope.$emit(".someEvent", data, $scope.id);
}
I have managed to solve my problem by creating a loadController directive. This directive sets a parent key on the child, and this allows me to create handlers for events from a specific child instance only. In my question I tried to set the child id on the parent. I ended up doing it the other way around, i.e. make the parent set an id on the child. So in the parent I would do
$scope.$on($scope.id + ".child1.click", function(event, data) {
$scope.messages.push(data);
});
Where the string "child1" is whatever the parent wants to make it. And in the child I would emit like this
$scope.$emit($scope.key + ".click", data);
Notice that
$scope.id + ".child1" == $scope.key
For a full example see this
http://plnkr.co/edit/2CdzQSKgWODIRvL5OZ9O
Related
I've a template:
<p class="text-right">
<a ng-click="editTherapeuticProposal(meow.accepted_tp)" class="fa fa-pencil"></a>
</p>
which calls the editTherapeuticProposal function defined in its controller, passing it the meow.accepted_tp object (here I use angular-dialog-service: https://github.com/m-e-conroy/angular-dialog-service):
// here tp is equal to meow.accepted_tp
$scope.editTherapeuticProposal = function(tp) {
dialogs.create('surgeon/templates/create_edit_therapeutic_proposal.tpl.html', 'SurgeonCreateEditTherapeuticProposalCtrl', {scope: $scope, tp: tp}, { copy: false });
};
tp is an object.
Then in the dialog controller I display a form in order to let the user modify tp. I do some stuff, the relevant ones are:
// data is the object received by the dialog controller: {scope: $scope, tp: tp}
if(typeof data.tp != 'undefined') {
$scope.therapeuticProposal = angular.copy(data.tp);
}
I copy the object to work on a different object (I don't want data to be updated if not saved)
When pressing the save button in the dialog, the following function runs:
var complete = function(tp) {
data.tp = tp;
//...
}
Ok, the problem is that meow.accepted_tp in the parent scope doesn't get updated. If I do
var complete = function(tp) {
data.tp.title = 'meow';
//...
}
Its title gets updated. There is clearly something wrong with the prototypal inheritance, I know that in order to get variables updated they should be properties of an object, but tp is already passed as an object property (of the data object). Any ideas?
Edit
After re-reading the angular-dialog-service docs, you can pass a result back using modalInstance. It sounds like this is what you want to do.
The reason your binding isn't working is because you're changing the object reference from a child scope, rather than a property on the object bound (which is why data.tp.title = 'meow' works).
Anyway, for your case, try this:
// here tp is equal to meow.accepted_tp
$scope.editTherapeuticProposal = function(tp) {
var dlg = dialogs.create('surgeon/templates/create_edit_therapeutic_proposal.tpl.html', 'SurgeonCreateEditTherapeuticProposalCtrl', {scope: $scope, data: data}, { copy: false });
dlg.result.then(function(tp) {
// Get the result and update meow.accept_tp
$scope.meow.accepted_tp = tp;
});
};
Then in the dialog, when you complete, do:
var complete = function(tp) {
$modalInstance.close(tp);
}
For an example, see http://codepen.io/m-e-conroy/pen/rkIqv, in particular the customDialogCtrl (not customDialogCtrl2) is what you want.
The widget inside the Child scope, with the ng-repeated div and select dropdown:
<div ng-repeat="term in ticker.terms">
<select ng-model="term.chosenTag" ng-change="ticker.changeTag(term)">
<option>...</option>
<option>...</option>
<option>...</option>
I'm trying to change ^ term.chosenTag from the parent scope, term is the repeated data object (term in terms)
Full code Gists:
Main Parent HTML: https://gist.github.com/leongaban/e0154005bed6b6892df7
Ticker Child HTML
https://gist.github.com/leongaban/9a5fd86643051fd9e1af
TickerController (Child)
https://gist.github.com/leongaban/2d58174cfe6e5c9c0465
MainController (Parent)
https://gist.github.com/leongaban/1563b09b906337a3e6ad
TagFactory
https://gist.github.com/leongaban/8db5027e1cb86f614fa5
Now the select in the parent scope that needs to also control the model in the child select:
<select ng-model="main.selected_tag"
ng-change="main.changeTag(main.the_tag)">
That main.changeTag function calls this:
vm.changeTag = function(term) {
// Update tag in model:
TagFactory.updateTag(term);
};
TagFactory then updates the term on the database as well as my model:
// Update tag:
tags.updateTag = function(data) {
var tag = data;
// Filter and find tag by id:
var tagObj = {};
tagObj = $filter('filter')(tags, { term_id: tag.term_id })[0];
// Update tag in model:
tagObj.tag = tag.chosenTag;
/**
* PUT : update tag on DB
*/
ApiFactory.updateTag(
tag.term_id,
tag.chosenTag,
tag.ticker).then(function(data) {
console.log('PUT:');
console.log(data);
});
return tag;
};
Since you fire your updateTag call onchange, as opposed to onclick, I am going to assume that constantly updating the service's data is OK. The other method where this wouldn't be advisable is if you only want to push data to the service at certain times, to avoid all your templates from seeing the changes to the factory data immediately if you still need to execute AJAX.
In the appropriate controller, with TagFactory injected, create a reference to the factory's data:
$scope.tags = TagFactory.tags;
Then your HTML can directly modify the TagFactory itself:
<div ng-repeat="tag in /*expression that determines ticker.terms with respect to $scope.tags*/">
<select ng-model="tag.chosenTag">
The expression can be as simple as "tags", but it will probably need to involve a filter that only returns tags that are ticker.terms.
I've been working on a requirement for a one-many-one relationship in Angular and Breeze and I've tried to apply Ward Bell's recommendation from this post in my solution so far. To see the code I'm using so far, please visit my original question and check the answer I submitted myself.
The problem, I'm now facing is when I want to prepare for saveChanges. The reason being, is that I have used nested controllers. The function that calls the save is in my parent controller and the property that holds my affected one-may-one entities is on the child controller.
In the parent controller...
function save() {
if (!canSave()) {
return $q.when(null);
}
// CODE TO PREPARE THE ONE-MANY-ONE ENTITIES FOR
// SAVING SHOULD GO HERE I'M GUESSING...
vm.isSaving = true;
return datacontextSvc.save().then(function(saveResult) {
vm.isSaving = false;
trapSavedDboardConfigId(saveResult);
}, function(error) {
vm.isSaving = false;
});
}
and in the child controller...
function getBusUnits() {
...
.then(function(data){
vm.busUnits = data;
vm.busUnits.forEach(function(bu){
getBusUnitChannels(bu);
});
});
}
function getBusUnitChannels(busUnit) {
datacontextSvc.dboardConfigs.getBusUnitChannelsById(busUnit.id)
.then(function (data) {
busUnit.busUnitChannelsList = data;
// THE PROBLEM IS THAT THE BUCHANNELS ARRAY IS CREATED
// FOR EACH BUSUNIT AND THE LIST OF BUSUNITS IS ONLY
// CALLED IN THE CHILD CONTROLLER.
// DOES THIS IMPLY I CAN'T HAVE A PROPERTY IN THE PARENT CONTROLLER
// THAT I CAN REFERENCE IN THE CHILD, SO THAT THE PARENT CAN ACCESS
// THE DATA DURING SAVE?
busUnit.buChannels = []; // HOW DO I DEFINE THIS GUY IN THE PARENT?
vm.channels.forEach(function (channel) {
busUnit.buChannels.push(channel);
});
busUnit.busUnitChannelsList.forEach(function (buc) {
busUnit.buChannels.forEach(function (buCh) {
if (buc.channelId === buCh.id) {
buCh.buChannel = buc;
buCh.isSelected = true;
} else {
buCh.isSelected = false;
}
});
});
});
}
The source of the problem is that the call for the one-many-one entity and the creation of an associated item viewmoder array (buChannels) only occurs after i've called for busUnits, and the call for busUnits happens in the child controller. Also, the buChannels array needs to be stored for each busUnit.
Because Angular parent controllers can't read child properties, the normal solution would be to create a property in the parent and reference it in the child. But, since the property I'd want to reference is dependent on an entity that's only fetched in the child, this can't be done.
Or can it? Any ideas or other recommended approaches. I'd really like to avoid only having a parent controller...
I am using same el for more than 1 view like below. I'm not facing any problem till now. Is this good approach or should i do any changes?
<div id="app">
<div id="app-header"></div>
<div id="app-container"></div>
<div id="app-footer">
</div>
App View:
{
el: "#app",
v1: new View1(),
v2: new View2(),
render: function () {
if (cond1) {
this.v1.render();
} else if (cond2) {
this.v2.render();
}}
}
View 1:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
View 2:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
By reading your question, I do not really see what advantages you could possibly have using this approach rather than having the different div elements being the root el for your views 1, 2, 3 and using
this.$el.html(template)
in the render method.
Your approach could work for a small application, but I think it will become really hard to maintain as the application grows.
EDIT
I still do not really get your point, you could only initialize everything only once in both cases.
Here is a working Fiddle.
By the way I am changing the content by listening to the click event but this is to simplify the example. It should be done by the router.
I do use a mixin to handle such situation, I call it stated view. For a view with all other options I will send a parameter called 'state', render will in-turn call renderState first time and there after every time I set a 'state' renderState will update the view state. Here is my mixin code looks like.
var setupStateEvents = function (context) {
var stateConfigs = context.getOption('states');
if (!stateConfigs) {
return;
}
var state;
var statedView;
var cleanUpState = function () {
if (statedView) {
statedView.remove();
}
};
var renderState = function (StateView) {
statedView = util.createView({
View: StateView,
model: context.model,
parentEl: context.$('.state-view'),
parentView:context
});
};
context.setState = function (toState) {
if (typeof toState === 'string') {
if (state === toState) {
return;
}
state = toState;
var StateView = stateConfigs[toState];
if (StateView) {
cleanUpState();
renderState(StateView);
} else {
throw new Error('Invalid State');
}
} else {
throw new Error('state should be a string');
}
};
context.getState = function () {
return state;
};
context.removeReferences(function(){
stateConfigs = null;
state=null;
statedView=null;
context=null;
})
};
full code can be seen here
https://github.com/ravihamsa/baseapp/blob/master/js/base/view.js
hope this helps
Backbone Rule:
When you create an instance of a view, it'll bind all events to el if
it was assigned, else view creates and assigns an empty div as el for that view and bind
all events to that view.
In my case, if i assign #app-container to view 1 and view 2 as el and when i initialize both views like below in App View, all events bind to the same container (i.e #app-container)
this.v1 = new App.View1();
this.v2 = new App.View2();
Will it lead to any memory leaks / Zombies?
No way. No way. Because ultimately you are having only one instance for each view. So this won't cause any memory leaks.
Where does it become problematic?
When your app grows, it is very common to use same id for a tag in both views. For example, you may have button with an id btn-save in both view's template. So when you bind btn-save in both views and when you click button in any one the view, it will trigger both views save method.
See this jsFiddle. This'll explain this case.
Can i use same el for both view?
It is up to you. If you avoid binding events based on same id or class name in both views, you won't have any problem. But you can avoid using same id but it's so complex to avoid same class names in both views.
So for me, it looks #Daniel Perez answer is more promising. So i'm going to use his approach.
I am making a custom directive and am trying to make it jQuery independent. In order to accomplish this without jQuery, the Scopes (parent and children) may have to interact directly. I certainly want to avoid "bad practices" as much as possible, so my question is this:
Is it bad practice to have a child scope directly call a function of the parent scope? If so, is there anyway to get the controller or element that governs a scope?
The reason this is necessary is that there may be multiple nested scopes of a directive. And I need to access a very particular one that is not necessarily the immediate parent of the current scope (though it is an ancestor).
Edit: UseCase Elaboration
The module contains two directives. Either of which may be nested in any number of parent directives. This child directive does require the parent directive accordingly, but in the case of nested parents, it only gets the controller of the most immediate parent.
The normal use of this will generally be:
<element parent-attr-dir="val">
<element child-attr-dir="val"></element>
</element>
Notice that the values of the attributes must match. This is what determines which "ancestor" to attach to.
Advanced usage may resemble:
<element parent-attr-dir="value1">
<element parent-attr-dir="value2">
<element child-attr-dir="value1">Will run on top most ancestor</element>
<element child-attr-dir="value2">Will run on immediate parent</element>
</element>
<element parent-attr-dir="value3">
<element child-attr-dir="value3">Will run on immediate parent</element>
<element child-attr-dir="value1">Will run on top most ancestor</element>
</element>
<element child-attr-dir="val">Will not run!!</element>
</element>
Edit: Solution in Question
This works, but need to know if its bad.
var _p = scope;
while (_p !== null) {
if (_p.doSomething
&& /* some other criteria. */ ) {
_p.doSomething(scope);
}
_p = _p.$parent;
}
Edit: Solution Using jQuery
This works, but requires jQuery as jQLite does not have parent(selector), just parent().
$(element).parents('[attrname=value]').controller('moduleName').funcName()
One solution would be for the children to require the parent directive and for the parent to optionally require the parent directive. Then a child calls the method of its immediate parent controller, passing along the criteria value; if the parent controller does not meet the criteria, it forwards the call to its own parent (if any), or nothing happens. A sketch of the solution:
app.directive("parentAttrDir", function() {
return {
require: ["?^parentAttrDir","parentAttrDir"],
link: function(scope,elem,attrs,controllers) {
var parentAttrDir = controllers[0],
thisController = controllers[1],
criterion = attrs.parentAttrDir;
thisController.parent = parentAttrDir;
thisController.criterion = criterion;
},
controller: function($scope) {
this.doSomething = function(data,criterion) {
if( this.criterion === criterion ) {
// actually do something with the data
}
else if( this.parent != null ) {
return this.parent.doSomething(data,criterion);
}
};
}
};
});
app.directive("childAttrDir", function() {
return {
require: "^parentAttrDir",
link: function(scope,elem,attrs,parentAttrDir) {
var criterion = attrs.childAttrDir;
// EXAMPLE USAGE FROM CHILD
scope.doSomeAction = function(data) {
parentAttrDir.doSomething(data,criterion);
};
}
};
});