A question regarding transclude within an angular 1.5.8 component, and it's uses.
Here is an example of some code;
var app = angular.module('app', [])
function AccordionController () {
var self = this;
// add panel
self.addPanel = function(panel) {
// code to add panel
}
self.selectPanel = function() {
//code to select panel
}
}
// register the accordion component
app.component('accordion', {
template: '<!---accordion-template-->',
controller: AccordionController
}
function AccordionPanelController () {
// use parents methods here
var self = this;
// add panel
self.parent.addPanel(self);
// select panel
self.parent.selectPanel(self);
}
// register the accordion-panel component
app.component('accordionPanel', {
// require the parent component
// In this case parent is an instance of accordion component
require: {
'parent': '^accordion',
template: '<!---accrodion-panel-template-->',
controller: AccordionController
}
My question is would it be better to nest all the according panels within the parent using transclude or alternatively pass in a data array to the parent which this loops out the required number of panels based on the array passed inside using a binding.
Thanks
// added
Many thanks for your reply, an example I have of transclude possibly being necessary is in the following bit of code
<modal modal-id="editCompany" title="Edit Company"> <company-form company="$ctrl.company"></company-form> </modal>
Here we have a modal component which may have a variety of other components used within it, on the example above I am adding the company form, but this could we be an contact form. is there an alternative way?
I've worked with angular pretty extensively. Two enterprise tools managing and displaying large amounts of data, dozens of interactive widget modules, all that.
Never, once, have I had anything to do with transclude. At work we are explicitly told not to use it (link functions too). I thought this was a good thing, and the way Angular 2 turned out it seemed that thinking wasn't totally without reason.
I would go with the iteration to lay out the required number of items. At work this wouldn't be a choice because transclude wouldn't be an option.
The thing with using transclude in a component architecture is that it visually breaks the idea of single responsibility and messes with the architecture.
<html>
<navigation></navigation>
<accordion>
<accordion-panel></accordion-panel>
</accordion>
<footer></footer>
</html>
In this example you know your page has a navigation menu, an accordion and a footer. But at the index level (or root component) you don't want to know / see what the accordion contains.
So the accordion-panel component should only appear in its direct parent component.
As for your other question, through the use of require or attributes you pass an array of panels that you iterate using ng-repeat inside the accordion component.
app.component('accordion', {
template: `<accordion-panel ng-repeat="..."></accordion-panel>`,
controller: AccordionController
}
Related
I'm currently learning vue.js and i'm struggling with the communication between parent and child components.
I'm trying to build two components (in separate files), a "accordion-container" and "accordion". The idea ist to then use them something like that on pages:
<accordion-container>
<accordion :title="'Accordion n1'">Insert HTML code here</accordion>
<accordion :title="'2nd Accordion'">Insert HTML code here</accordion>
</accordion-container>
Code for the container:
<template #closeAccordions="closeOtherAccordions">
<div class="accordion-container"><slot></slot></div>
</template>
<script>
export default {
props:['title'],
methods:{
closeOtherAccordions: function(){
console.log('Emit from child component received')
},
},
data: function() {
return {
}
}
};
</script>
Code for the accordions:
<template>
<div class="accordion" v-bind:class="{ open: isOpen }" :data-title="title">
<div class="title" #click="toggleAccordion">
<p>{{title}}</p>
</div>
<div class="content"><slot></slot></div>
</div>
</template>
<script>
export default {
props:['title'],
methods:{
toggleAccordion: function(){
this.isOpen = !this.isOpen
this.$emit('closeAccordions')
}
},
data: function() {
return {
isOpen: false
}
},
};
</script>
On the accordion i'm trying to emit "closeAccordions" (with the method toggleAccordion())
Then on the parent (accordion-container) i'd like to "listen" for that emit (with :closeAccordions="closeOtherAccordions"), and then execute a method on the parent.
But that method does not get called when i click the accordions.
Is my idea even possible? (Open to other ideas :) )
It won't work that way. The parent component cannot directly communicate to any components rendered within its slots via events, props, or by any other means that can only be achieved at the site where the slot contents are directly rendered (the container component doesn't control this).
When you are designing a component and you put a <slot> in the template, all you are doing is designating an insertion point within the template that users of the component can inject their own content.
You have 4 options:
(Advanced) Write the render function by hand and override the rendered slot vnodes to inject your own event listeners, props, etc.
Expose an API using scoped slots where you pass some data or methods to the slot which the user of the component would have to hook up in order for the component to operate correctly. Users of the component would have to remember to hook everything up correctly between the container and each accordion, so it's not ideal in this situation, but in general it is useful when you want to leave some of the functionality up to the user as to how the parent and children should operate.
Don't use events to communicate between the container and accordions, instead the accordions can call methods on the container directly via this.$parent.
Use provide/inject to allow the container to provide an API that each accordion can inject and use.
(3) is the recommended approach in this situation. The container and accordion components should be tightly coupled here. The accordion component can (and should) only be used directly within the container component, so it's OK if they communicate directly like that.
// Change this
this.$emit('closeAccordions')
// To this
this.$parent.closeOtherAccordions()
For more complicated components, (4) might be better.
In our rental application, we make an API call that populates an array, thus triggering an ngRepeat. This creates a list of divs that show basic information about the rental property.
Clicking on a property expands the div and then another API call is made to populate an interior ngRepeat with a list of tenants. Some of these properties have up to 100 tenants listed (past and present are included). The tenant divs themselves are also expandable, and this reveals the majority of the functionality. You can download rental agreements, view history, etc. All of this functionality is a single directive made up of a number of ng-includes.
If you're still following, there's an outside ngRepeat and an interior ngRepeat with a huge directive inside of it.
<div ng-repeat="properties in property_collection track by property.ID>
*code removed*
<div ng-repeat="tenant in property_tenant_collection track by tenant.ID>
*...code here...*
<div tenant-menu></div>
The directive tenant-menu and all of the ngIncludes and watchers that come with it are rendered when you expand the list of properties. They just aren't visible yet. Clicking on the tenant in the list just changes the height of the div, thus revealing the interior menu.
The performance implications of the way this UI was designed are absolutely dire. There's over 15,000 watchers on the page for elements that you can't even see. Taking action on one tenant fires the digest loop for all of them for no reason. It takes under a second to retrieve the necessary data, but almost 20 seconds to render a list of 60 applicants. I've removed the directive completely (which means nothing happens when you click on a tenant) and the loading & rendering time goes from 20 seconds to 2-3 seconds.
I'm not sure how I can achieve this, but is it possible to defer attaching this directive until the tenant is clicked? I don't want clicking on the tenant div to just change the height and reveal what's inside, I want to append the entire directive right then and THEN expand the height. Ideally, when the click event is fired again and the tenant is collapsed I would also be able to destroy any watchers and clean up after myself.
Whew.
Edit: The infamous sliding directive is pasted below. I think it's strange that a click event is being bound inside, but perhaps this is an O.K. code pattern in Angular. I have a hunch that I may be able to leverage the compile, postlink, and prelink functions along with the accepted answer. I inherited a bit of legacy code I think.
angular.module('jnjManagement.directives').directive('slideableTenant', function($compile, tenantService) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var targetX, contentX;
attrs.expanded = false;
element.bind('click', function() {
if (!targetX) targetX = document.querySelector(attrs.slideToggleTenant);
if (!contentX) contentX = targetX.querySelector('.slideable_tenant_content');
if (!attrs.expanded) {
contentX.style.border = '1px solid rgba(0,0,0,0)';
var y = contentX.clientHeight;
contentX.style.border = 0;
targetX.style.height = 'auto';
} else {
targetX.style.height = '0px';
}
attrs.expanded = !attrs.expanded;
});
}
};
});
Ideally you would have a property directive and a tenant directive and you would indeed only load the content of the expanded element when it was clicked. I would make use of Angular's $compile.
$scope.clickHandler = function(){
$('expanded-element').append($compile('<detail-directive></detail-directive')($scope));
};
Of course you would want to clean out elements when they are collapsed, too, but it sounds like you have a good grasp of performance considerations.
Routing in Ember.js is troubling me, and I can't seem to find the "correct" way of doing what I want to do.
I have a route, let's call it #/map, which contains a series of top-level and containers of child views.
Hierarchically, I have a top map_view, which contains 4 additional views: A topbar (which has topbar menu item triggers within it), a sidebar (which has sidebar menu item triggers in it), and two containerViews (a sidebar menu containerView and a topbar menu containerView), which will contain one or more nested views that are programatically inserted on clicking a menu item trigger.
My issue is that while this works, and I can embed all of these views into their various templates, none of them are linking with controllers, and the controller they are picking up is the map_controller (which makes sense as that is the linked outlet controller for the top level view). Currently I am using a method described on Ember's github here, but it seems a little...hacky?
Here is a JSFiddle showing the problem. Notice that the controller for level-one-entry and level-two-entry is the index_controller: http://jsfiddle.net/fishbowl/Z94ZY/3/
Here are some code snippets for what I am doing to get around it:
map.hbs:
<section id='map'>
{{view App.SidebarView}}
{{view App.TopbarView}}
<div id='map-canvas'></div>
</section>
topbar_view.js:
var TopbarView = Em.View.extend({
templateName: 'topbar',
classNames: ['topbar-container'],
init: function() {
var content = this.get('content'),
controller = App.TopbarController.create({
view: this
});
this.set('controller', controller);
this._super();
}
});
module.exports = TopbarView;
topbar_controller.js
var TopbarController = App.ApplicationController.extend({
content: Ember.computed.alias('view.content'),
trigger: null,
start_date: null,
end_date: null,
travelling: null,
word: 'topbar'
});
module.exports = TopbarController;
I'm not doing anything special in the router other than declaring this.route('map'). A further problem i'm having is that whenever I declare needs: ['some_other_controller'], I get an error
<App.TopbarController:ember413> specifies 'needs', but does not have a container. Please ensure this controller was instantiated with a container.
Am I missing something blindingly obvious about how to go about linking these together. I'm guessing that i'm using routing incorrectly. I don't want to change what the URL is, as i'm technically not moving pages, just opening and closing menus on the page, but I don't really understand how else i'm supposed to use the router to achieve this.
EDIT 2: i've mocked up another jsfiddle of what I could do with outlets and link-to's, but i'm not sure that I want the URL changing (as you'd probably be able to do odd things with the back button etc): jsfiddle - The alternative to this is to set location: 'none' in the Router, but I don't really like that option either...
I'm trying to shove mixitup inside my angular page and in order to do so I made a directive module for it
angular.module('MainCtrl', [])
.controller('MainController', function($scope) {
$scope.tagline = 'To the moon and back!';
})
.directive('mixitContainer', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
$(element).mixItUp(scope.$eval(attrs.mixitContainer));
}
};
});
Don't worry about the simplicity of the main controller, it is simply a test.
Now, the issue is that the directive only get's called once! If I go to another page and ask angular to load another controller and then go back to the home page and ask angular to load MainCtrl again, the directive isn't loaded!
Heres the with the directive:
<div id="Container" class="mixit-container" mixit-container="{load: {sort: 'order:asc'}, controls: {toggleFilterButtons: true, toggleLogic: 'and'}}">
Anyone got any ideas?
AngularJS doesn't include routing facilities. Those are provided either by ngRoute (a core but optional module), ui-router (ngRoute made super-awesome-amazing), or another replacement. You don't say which you use, and each has different behaviors.
Whichever it is, this is going to come down to the router, not the directive. The directive will get called whenever necessary. It doesn't control that itself - 'necessary' means Angular is compiling a portion of the DOM, usually from a template file, and has run into the directive. It will call the directive and ask it "what now?"
The above-named routers have different behaviors, both from each other and also based on how you configure them internally. In all of them you can arrange things so that templates do, or do not, get re-rendered. For example, in ui-router you can have 'child' states. If you have a child state active, the parent is also active... so going from the child to the parent will not re-render a template because it was already done earlier. And to make matters more complex, you can even override this by hooking the $stateChangeStart event and force the router to redraw that view even if it didn't think it needed to.
All this means... set your attention to your directive aside. It will do what you want as soon as the higher level does what you want. Get your router behaving the way you expect and you will be happy!
I'm creating a few directives that will make up a single "screen" in my app. To create this new screen you would write it like this:
<screen title="Test Title">
<side-menu align="left">
<menu-item>Test One</menu-item>
<menu-item selected="true">Test Two</menu-item>
<menu-item disabled="true">Test Three</menu-item>
</side-menu>
<content animation="fade">
<view>Content for menu option 1</view>
<view>Content for menu option 2</view>
<view>Content for menu option 3</view>
</content>
</screen>
Each <menu-item> will display one of the "views" inside of the <content> tag. It works like tabs. I've got this set up by keeping track of each <view> inside of the <content> directive in an array when they are linked. Same for <menu-item>.
My question is, now that I've got this set up, what is the best way to communicate between the <side-menu> directive and the <content> directive to hide and show the correct view when clicked? Should I use events, a common service to hold state, or is there a way I can maybe access the controller inside of the <screen> directive from the <view> and <menu-item> directives, and hold the data/state there? From my understanding I can only access the parent controller from a child directive, but not the "grand parent" controller if you will, unless I use some sort of pass-through.
I plan to have a few more components on this "screen" that will need to communicate as well so I'm hoping to determine the "correct" way to do this before I continue, or at least get some feedback and other ideas.
If any of that is confusing, I'd be happy to provide more information.
So after a bit of digging, I've learned that you can pass an array to the require property of a directive.
You can use this to find parent controllers, and grandparent controllers... etc. Previously each of my directives would have one require value such as: require: '^sideMenu' for the menuItem directive.
Now I can require the sideMenu and screen controllers into the menuItem directive by passing an array:
require: ['^screen', '^sideMenu']
Now in the link function of my menuItem directive, I can access these controllers this way:
link: function(scope, element, attrs, controllers) {
var screenCtrl = controllers[0];
var sideMenuCtrl = controllers[1];
}
Notice the controllers property is now an array of the controllers that I required, and are accessed by index. Although I feel as though my directives are little more tightly coupled now, I do like it better than using events/services.
I'm explaining all of this, because no where in the Angular docs does it mention this.