import ngModule from '../module'; import EventEmitter from './event-emitter'; import {kebabToCamel} from './string'; /** * Base class for component controllers. */ export default class Component extends EventEmitter { /** * Contructor. * * @param {HTMLElement} $element The main component element * @param {$rootScope.Scope} $scope The element scope * @param {Function} $transclude The transclusion function * @param {Function} $location The location function */ constructor($element, $scope, $transclude, $location) { super($element, $scope, $transclude, $location); this.$ = $scope; if (!$element) return; this.element = $element[0]; this.element.$ctrl = this; this.$element = $element; this.$transclude = $transclude; this.classList = this.element.classList; const constructor = this.constructor; const $options = constructor.$options; if ($options && $options.installClasses) this.classList.add(...this.constructor.$classNames); if ($transclude && constructor.slotTemplates) { for (let slotTemplate of constructor.slotTemplates) this.fillSlots(slotTemplate); } } $postLink() { if (!this.$element) return; let attrs = this.$element[0].attributes; let $scope = this.$; for (let attr of attrs) { if (!attr.name.startsWith('on-')) continue; let eventName = kebabToCamel(attr.name.substr(3)); let callback = locals => $scope.$parent.$eval(attr.nodeValue, locals); this.on(eventName, callback); } } /** * The component owner window. */ get window() { return this.document.defaultView; } /** * The component owner document. */ get document() { return this.element.ownerDocument; } /** * Translates an string. * * @param {String} string String to translate * @param {Array} params Translate parameters * @return {String} The translated string */ $t(string, params) { return this.$translate.instant(string, params); } /** * Fills the default transclude slot. * * @param {JQElement|String} template The slot template */ fillDefaultSlot(template) { const linkFn = this.$compile(template); this.$transclude.$$boundTransclude = this.createBoundTranscludeFn(linkFn); } /** * Fills a named transclude slot. * * @param {String} slot The trasnclude slot name * @param {JQElement|String} template The slot template */ fillSlot(slot, template) { const linkFn = this.$compile(template); const slots = this.$transclude.$$boundTransclude.$$slots; slots[slot] = this.createBoundTranscludeFn(linkFn); } /** * Fills component transclude slots using the passed HTML template string * as source. * * @param {String} template The HTML template string */ fillSlots(template) { const name = this.constructor.$options.name; const transclude = this.constructor.$options.transclude; if (!transclude) throw new Error(`No transclusion option defined in '${name}'`); if (!this.$transclude) throw new Error(`No $transclude injected in '${name}'`); let slotMap = {}; for (let slotName in transclude) { let slotTag = transclude[slotName].match(/\w+$/)[0]; slotMap[slotTag] = slotName; } const $template = angular.element(template); for (let i = 0; i < $template.length; i++) { let slotElement = $template[i]; if (slotElement.nodeType != Node.ELEMENT_NODE) continue; let tagName = kebabToCamel(slotElement.tagName.toLowerCase()); if (tagName == 'default') this.fillDefaultSlot(slotElement.childNodes); else { let slotName = slotMap[tagName]; if (!slotName) throw new Error(`No slot found for '${tagName}' in '${name}'`); this.fillSlot(slotName, slotElement); } } } /** * Creates a bounded transclude function from a linking function. * * @param {Function} linkFn The linking function * @return {Function} The bounded transclude function */ createBoundTranscludeFn(linkFn) { let scope = this.$; let previousBoundTranscludeFn = this.$transclude.$$boundTransclude; function vnBoundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { if (!transcludedScope) { transcludedScope = scope.$new(false, containingScope); transcludedScope.$$transcluded = true; } return linkFn(transcludedScope, cloneFn, { parentBoundTranscludeFn: previousBoundTranscludeFn, transcludeControllers: controllers, futureParentElement: futureParentElement }); } vnBoundTranscludeFn.$$slots = previousBoundTranscludeFn.$$slots; return vnBoundTranscludeFn; } copySlot(slot, $transclude) { this.$transclude.$$boundTransclude.$$slots[slot] = $transclude.$$boundTransclude.$$slots[slot]; } } Component.$inject = ['$element', '$scope', '$location', '$state']; /* * Automatically adds the most used services to the prototype, so they are * available as component properties. */ function runFn(...args) { const proto = Component.prototype; for (let i = 0; i < runFn.$inject.length; i++) proto[runFn.$inject[i]] = args[i]; Object.assign(proto, { $params: proto.$stateParams }); } runFn.$inject = [ '$translate', '$q', '$http', '$httpParamSerializer', '$state', '$stateParams', '$timeout', '$transitions', '$compile', '$filter', '$interpolate', '$window', 'vnApp', 'vnToken', 'vnConfig', 'vnModules', 'aclService' ]; ngModule.run(runFn);