Как динамически связать пользовательский модальный AngularJS? - PullRequest
0 голосов
/ 22 октября 2018

В образовательном проекте, над которым я работаю, я хочу избегать использования AngularJS Material Design, UI Bootstrap или любых пользовательских библиотек, которые предоставляют модальные функциональные возможности.

Однако я столкнулся с проблемой.Я создал сервис, который должен управлять и динамически создавать модалы.Он предоставляет функцию open, которая принимает объект спецификации, который затем воспроизводится в DOM.

Что этот код на самом деле делает:
1. Модал правильно добавлен в DOM.
2. Срабатывает функция $onInit контроллера модального режима.

Чего не делает этот код:
1. Привязать свойство $ctrl.message в разметке к экземпляру контроллера, который, как мы знаем, запускается.

Обычно я хотел бызадайте мой вопрос после предоставления кода, однако для воспроизведения этой проблемы требуется немало кода (ниже он приведен без примера AngularJS). Однако в данном случае мой вопрос:

Каким образом я могу получить модалы, отключаемые этим сервисом, чтобы правильно связать их содержимое с данным контроллером?

Что я пробовал:
Как вы можете видеть в ModalService.bindModalDiv, я пробовал несколько путей мысли, в основном используя $compile.Тем не менее, $compile и получающаяся функция связи на самом деле, кажется, не связывают новые элементы DOM с Angular.

Я пытался использовать $controller, чтобы явно привязать генерируемую новую область к создаваемому экземпляру someModalCtrl, но это, похоже, совсем не помогает.

Поскольку я могу нажимать точки останова на someModalCtrl и видеть сообщение console.log, которое я использовал для проверки работоспособности, я думаю, что неправильно понимаю, как именно я должен связывать новые элементы DOM сУгловой.Я уверен, что упускаю что-то простое, о чем мне удалось как-то забыть или пренебречь.

Еще одно замечание:
Уверен, мои проблемы с получениеммодальный способ привязки к 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);

1 Ответ

0 голосов
/ 22 октября 2018

Я понял, где я ошибся - мне нужно было использовать обратный вызов $().load().JQuery load является асинхронным, что означает, что $compile работает правильно;тем не менее, HTML в моей модальной части не был загружен к тому времени, когда $compile выполнил свою работу, поэтому несвязанный 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}`;
}

Я понял это, вернувшись назад и выполняя пошаговую разработку.Сначала я убедился, что $compile работает, создав элемент, содержимое которого было {{ 2 + 2 }}, скомпилировав его, а затем внедрив его.Когда я увидел, что 4 добавлено на мою страницу, я понял, что аспект программы compile-then-inject работает очень хорошо.

Оттуда я начал создавать конструкцию модала и обнаружил, что она работает безупречно.... пока я не добрался до jQuery load.Когда я прочитал документацию, я увидел ошибку своих путей.

TL; DR: Прочитайте дружественное руководство!

...