【问题标题】:How to dynamically bind a custom AngularJS modal?如何动态绑定自定义 AngularJS 模态?
【发布时间】:2019-03-26 21:13:05
【问题描述】:

对于我正在从事的教育副项目,我想避免使用 AngularJS Material Design、UI Bootstrap 或任何提供模态功能的自定义库。

但是,我遇到了一个障碍。我创建了一个应该管理和动态创建模式的服务。它提供了一个open 函数,该函数接受一个规范对象,然后在 DOM 中复制该对象。

这段代码的实际作用:
1. modal 正确地附加到 DOM。
2.模态控制器的$onInit函数触发。

这段代码没有做什么:
1. 将标记中的$ctrl.message 属性绑定到我们知道启动的控制器实例。

通常,我会在提供代码后问我的问题,但是重现此问题需要大量代码(如下所示,没有一些 AngularJS 样板。)不过,既然如此,这是我的 问题:

我可以通过什么方式让该服务分离出来的模式正确地将它们的内容绑定到给定的控制器?

我的尝试:
正如您在ModalService.bindModalDiv 中看到的那样,我尝试了几种思路,主要是使用$compile。然而,$compile 和生成的链接函数实际上似乎并未将新的 DOM 元素绑定到 Angular。

我尝试使用$controller 将正在生成的新范围显式绑定到正在实例化的someModalCtrl,但这似乎根本没有帮助。

因为我可以在someModalCtrl 上设置断点,并查看我用作完整性检查的console.log 消息,我想我误解了我应该如何将新的DOM 元素绑定到Angular。我确定我错过了一些我已经设法忘记或忽视的基本内容。

还有一点说明:
我确信让模态正确绑定到 AngularJS 的问题并不是唯一存在的问题。请记住,我这样做部分是作为学习练习;如果你们都可以帮助我找出我的模态问题,我将继续尽职调查并找出我无疑在这种方法中存在的缺陷。因此,如果您看到不是模态问题的问题,可以引起我的注意,但我不会重写问题来解决您发现的任何问题 - 除非我这样做绝对必要。举个例子 - 我知道ModalService.open 在我如何实现承诺设置方面存在一些问题。 $rootScope.$watch 可能更合理。

modalSvc.ts:

export interface IModalSpecObject {
    parent?: string | Element | JQuery;
    templateURL: string
    controller: string;
    controllerAs?: string;
    data: object;
}

export class ModalInstance {
    public isOpen: boolean = true;
    public returnData: object = null;
    public element: JQLite = null;
    public $parent: JQuery = null;

    public constructor(
        public specObject: IModalSpecObject
    ) {
    }

    public close(returnData: object): void {
        if (this.element)
            this.element.remove();

        this.isOpen = false;
        this.returnData = returnData;
    }
}

export class ModalService {
    public pollRate: number = 250;
    public instance: ModalInstance = null;

    public static $inject: string[] = [
        '$q', '$rootScope', '$compile', '$controller'
    ];
    public constructor(
        public $q: ng.IQService,
        public $rootScope: ng.IRootScopeService,
        public $compile: ng.ICompileService,
        public $controller: ng.IControllerService
    ) {
    }

    public open(specObject: IModalSpecObject): ng.IPromise<{}> {
        if (this.instance && this.instance.isOpen)
            this.instance.close(null);

        this.instance = new ModalInstance(specObject);

        const $parent: JQuery = this.setParent(specObject);
        const modalDiv: JQLite = this.buildModal(specObject);            
        this.bindModalDiv(modalDiv, $parent); 

        const result: ng.IPromise<{}> = this.$q((resolve) => {
            setInterval(() => {
                if (!this.instance.isOpen) {
                    resolve(this.instance.returnData);
                }
            }, this.pollRate);
        });
        return result;
    }

    private buildModal(specObject: IModalSpecObject): JQLite {
        const modalDiv: JQLite = angular.element('<div/>');
        modalDiv.addClass('modal');
        const $modalPanel: JQuery = $('<div/>');
        $modalPanel.addClass('modal-panel');

        // Inject HTML template...
        $modalPanel.load(specObject.templateUrl);

        // Set up the angular controller...
        const controllerAs: string = specObject.controllerAs 
            ? specObject.controllerAs 
            : '$ctrl';
        $modalPanel.attr('ng-controller', `${specObject.controller} as ${controllerAs}`);
        modalDiv.append($modalPanel);

        this.instance.element = modalDiv;
        return modalDiv;
    }

    private setParent(specObject: IModalSpecObject): JQuery {
        let $parent: JQuery;
        if(!specObject.parent)
            $parent = $(document);
        else if (typeof specObject.parent === "string"
                 || specObject.parent instanceof Element)
            $parent = $(specObject.parent);
        else if (specObject.parent instanceof jQuery)
            $parent = specObject.parent;
        else
            $parent = $(document);

        this.instance.$parent = $parent;
        return $parent;
    }

    // !!!! !!!! I suspect this is where my problems lie. !!!! !!!!
    private bindModalDiv(modalDiv: JQLite, $parent: JQuery): void {
        const newScope: ng.IScope = this.$rootScope.$new(true);

        // Try #1: Bind generated element to parent...
        //$parent.append(this.$compile(modalDiv)(newScope));

        // Try #1a: Generate bindings, then append to parent...
        //const element: JQLite = this.$compile(modalDiv)(newScope);
        //$parent.append(element);

        // Try #2: Bind element to parent, then generate ng bindings...
        //$parent.append(modalDiv);
        //this.$compile(modalDiv)(newScope);

        // Try #3: Well, what if we bind a controller to the scope?
        const specObject: IModalSpecObject = this.instance.specObject;
        const controllerAs: string = specObject.controllerAs 
            ? specObject.controllerAs 
            : '$ctrl';
        this.$controller(`${specObject.controller} as ${controllerAs}`, {
            '$scope': newScope
        });

        const element = this.$compile(modalDiv)(newScope);
        $parent.append(element);
    }
}

angular
    .module('app')
    .service('modalSvc', ModalService);

SomeController.ts: SomeController.ts 几乎只是控制一个按钮来触发模态的外观;由于这个原因,我没有包含标记。

export class SomeController {
    public static $inject: string[] = [ 'modalSvc' ];
    public constructor(
        public modalSvc: ModalService
    ) {
    }

    public $onInit(): void {
    }

    public openModal(): void {
        const newModal: IModalSpecObject = {
            parent: 'body',
            templateUrl: '/someModal.html',
            controller: 'someModalCtrl',
            data: {
                'message': 'You should see this.'
            }
        };

        this.modalSvc.open(newModal)
            .then(() => {
                console.log('You did it!');
            });
    }
}

angular.module('app').controller('someCtrl', SomeController);

someModal.html:

<div class="modal-header">
    Important Message
</div>    

<!-- This should read, "You should see this." -->
<div class="modal-body">
    {{ $ctrl.message }}
</div>

<!-- You should click this, and hit a breakpoint and/or close the modal. -->
<div class="modal-footer">
    <button ng-click="$ctrl.close()">Close</button>
</div>

someModal.ts:

export class SomeModalController {
    public message: string = '';

    public static $inject: string[] = [ 'modalSvc' ];
    public constructor(
        public modalSvc: ModalService
    ) {
    }

    public $onInit(): void {
        console.log('$onInit was triggered!');
        this.message = this.modalSvc.instance.specObject.data['message'];
    }

    public close(): void {
        this.modalSvc.instance.close(null);
    }
}

angular
    .module('app')
    .controller('someModalCtrl', SomeModalController);

【问题讨论】:

    标签: angularjs typescript modal-dialog model-binding


    【解决方案1】:

    我知道我哪里出错了——我需要使用$().load() 的回调。 JQuery load 是异步的,这意味着$compile 工作正常;但是,当$compile 完成其工作时,我的模态部分中的 HTML 尚未加载,因此是未绑定的 HTML。

    不过,对我的 ModalService 稍作修改就解决了这个问题。

    ModalSvc.ts 的修改片段:

    // This is just a convenience alias for void functions.  Included for completeness.
    export type VoidFunction = () => void;
    
    // ...
    
    public open(specObject: IModalSpecObject): ng.IPromise<{}> {
        if (this.instance && this.instance.isOpen)
            this.instance.close(null);
    
        this.instance = new ModalInstance(specObject);
    
        const $parent: JQuery = this.setParent(specObject);
        // open already returned a promise before, we just needed to return
        // the promise from build modal, which in turn sets up the true
        // promise to resolve.
        return this.buildModal(specObject)
            .then((modalDiv: JQLite) => {
                this.bindModalDiv(modalDiv, $parent);
    
                const result: ng.IPromise<{}> = this.$q((resolve) => {
                    // Also, side-note: to avoid resource leaks, always make sure
                    // with these sorts of ephemeral watches to capture and release
                    // them.  Resource leaks are _no fun_!
                    const unregister: VoidFunction = this.$rootScope.$watch(() => {
                        this.instance.isOpen
                    }, () => {
                        if (! this.instance.isOpen) {
                            resolve(this.instance.returnData);
                            unregister();
                        }
                    });
                });
                return result;
           });
    }
    
    private buildModal(specObject: IModalSpecObject): ng.IPromise<{}> {
        const modalDiv: JQLite = angular.element('<div/>');
        modalDiv.addClass('modal');
        this.instance.element = modalDiv;
    
        const $modalPanel: JQuery = $('<div/>');
        $modalPanel.addClass('modal-panel');
    
        // By wrapping $modalPanel.load in a $q promise, we can
        // ensure that the modal is fully-built before we $compile it.
        const result: ng.IPromise<{}> = this.$q((resolve, reject) => {
            $modalPanel.load(specObject.templateUrl, () => {
                modalDiv.append($modalPanel);
                resolve(modalDiv);
            });
        });
        return result;
    }
    
    private setParent(specObject: IModalSpecObject): JQuery {
        let $parent: JQuery;
        if(!specObject.parent)
            $parent = $(document);
        else if (typeof specObject.parent === "string"
                 || specObject.parent instanceof Element)
            $parent = $(specObject.parent);
        else if (specObject.parent instanceof jQuery)
            $parent = specObject.parent;
        else
            $parent = $(document);
    
        this.instance.$parent = $parent;
        return $parent;
    }
    
    private bindModalDiv(modalDiv: JQLite, parent: JQLite): void {
        // parent should be a JQLite so I can use the injector() on it.
        parent.injector().invoke(['$rootScope', '$compile', ($rootScope, $compile) => {
            const newScope: ng.IScope = $rootScope.$new(true);
            this.$controller(this.getControllerAsString(), {
                '$scope': newScope
            });
            const element: JQLite = $compile(modalDiv)(newScope);
            parent.append(element);
        }]);
    }
    
    private getControllerAsString(): string {
        const specObject: IModalSpecObject = this.instance.specObject;
        const controllerAs: string = specObject.controllerAs 
            ? specObject.controllerAs 
            : '$ctrl';
    
        return `${specObject.controller} as ${controllerAs}`;
    }
    

    我通过返回并逐步进行工程来解决这个问题。我首先通过创建一个内容为{{ 2 + 2 }} 的元素,编译它,然后注入它来确保$compile 正在工作。当我看到 4 添加到我的页面时,我知道程序的编译然后注入方面工作得很好。

    从那里,我开始构建模态结构,发现它完美无缺……直到我使用 jQuery load。当我阅读文档时,我看到了我的方式的错误。

    TL;DR:阅读友好手册!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-12-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多