عنوان:

‫فراخوانی یک تابع بعد از اتمام Render در AngularJS


نویسنده: بهنام محمدی
تاریخ: ۱۳۹۴/۰۹/۰۹ ۱۲:۵
آدرس: www.dntips.ir
در این مقاله در خصوص موضوعی صحبت خواهم کرد که شاید مشکل اکثر برنامه نویسان باشد؛ مخصوصا در استفاده از پلاگین‌های jQuery در پروژه‌های AngularJS.
مطمئنا برای شما هم پیش آمده که نیاز داشته باشید تابعی را بعد از اتمام Render در AngularJS صدا بزنید یا متوجه اتمام Render بشوید.

سوال اول: چرا این بحث مطرح هست؟
وقتی شما از AngularJS در پروژه‌ای استفاده می‌کنید و سبک کاری شما Model Based یا بهتر بگویم MVVM می‌باشد، عملیات Binding در View، توسط AngularJS انجام می‌شود و AngularJS توسط Watcher‌ها تغییرات را در View اعمال می‌کند.

سوال دو: مشکل کجاست؟
مشکل اینجاست که چه موقعی Binding یا Render تمام می‌شود؟ فرض کنید شما یک لیست دارید و می‌خواهید در View نمایش دهید. مسلما از ng-repeat استفاده می‌کنید و AngularJS از مدلی که مشخص شده است موارد را خوانده و در View نمایش می‌دهد. فرض کنید همه‌ی این موارد باید توسط jQuery UI دارای قابلیت Draggable بشوند. اما چه موقعی تابع مربوطه را صدا بزنیم؟ از کجا مطمئن شویم که element‌های لیست شهر‌ها در View موجود هستند؟ کد زیر را نگاه کنید.
<div class="city" ng-repeat="item in items">
     {{item.title}}
<div>
و در Controller
$scope.items=[
     {title:'اردبیل'},
     {title:'تهران'},
     {title:'تبریز'},
     {title:'مشهد'},
     {title:'اصفهان'}
];
ما فرض می‌کنیم بعد از اینکه شهر‌ها در View به صورت لیست نمایش داده شدند، کاربر باید بتواند شهر‌ها را Drag کند و مکان آنها را با ماوس جابجا کند. برای این کار ما از تابع jQuery UI زیر استفاده می‌کنیم:
$('.city').draggable();

سوال سوم: خوب مشکل کجاست؟
مشکل اینجاست که ما چه موقعی این تابع را صدا بزنیم تا مطمئن شویم که elementهای کلاس city در View موجود هستند و نسبت به تغییر لیست شهر که ممکن هست در طول اجرا این شهر‌ها کم یا زیاد شوند چه موقعی تابع jQuery UI را صدا بزنیم؟

سوال چهارم: راه حل چیست؟
در این چند سالی که من با AngularJS کار می‌کنم، از یک روش خیلی ساده استفاده میکنم و با همین روش از همه‌ی پلاگین‌های غیر AngularJS بدون تبدیل کردن این پلاگین‌ها به معادل AngularJS و یا گشتن چند ساعتی در اینترنت برای پیدا کردن پلاگین مشابه و منطبق با AngularJS که خیلی از مواقع هم پیدا نمی‌شوند، راحت شده‌ام. کد زیر را مشاهده کنید.
app.directive('ngFinishRender', function ($timeout) {
    return {
        restrict: 'A',
        link: function (scope, element, attr) {
            if (scope.$last === true) {
                $timeout(function () {
                    scope.$eval(attr.ngFinishRender);
                }, 0);
            }
        }
    }
});
این کد یک Directive جدید را در پروژه‌ی AngularJS شما تعریف می‌کند که توسط AngularJS قابل پردازش هست. این Directive را در View اضافه می‌کنم و کد View بالایی به کد زیر تغییر می‌کند:
<div class="city" ng-repeat="item in items" ng-finish-render="init()">
     {{item.title}}
<div>
اما این Directive چه عملی را انجام می‌دهد؟ این Directive توسط AngularJS پردازش شده و تابع init را که در مقدار ng-finish-render نوشته شده است، بعد از اتمام ng-repeat اجرا می‌کند. خوب تابع init را در controller می‌نویسیم؛ به کد زیر دقت کنید.
$scope.items=[
     {title:'اردبیل'},
     {title:'تهران'},
     {title:'تبریز'},
     {title:'مشهد'},
     {title:'اصفهان'}
];

$scope.init=function(){
     $('.city').draggable();
}
شما به همین راحتی می‌توانید از همه‌ی پلاگین‌های غیر AngularJS استفاده کنید و متوجه اتمام Render در View شوید.

نظرات

  • حیدر در ۱۳۹۴/۰۹/۱۴ ۸:۲۶
    با تشکر از مطلب خوبتان.یه سوال داشتم.اگر از ng-repeat  به همراه ng-include برای داده هایی با ساختار (Recursive) مثل منوها استفاده کنیم در این صورت چه طوری می‌توانیم متوجه رندر شدن کامل داده شویم؟
    • بهنام محمدی در ۱۳۹۴/۰۹/۱۴ ۱۴:۴۸
      در Directive ng-include علاوه بر بحث Render شما بحث اتمام بارگیری محتواها را هم دارید که برای این کار یعنی تشخیص اتمام بارگیری محتوا باید از دستور زیر استفاده نمایید.
      $scope.$on('$includeContentLoaded', function () {        
              //دستورات بعد از اتمام بارگیری محتوا
          });
      بعد از این مرحله هست که ngFinishRender وارد صحنه می‌شود. :)
      • حیدر در ۱۳۹۴/۰۹/۱۶ ۱۰:۵۹
        باتشکر مشکل حل شد. در Directive ngFinishRender  به دلیل استفاده از سرویس timeout$، فراخوانی تابع بعد از اجرای کامل چرخه Digest$ انجام می‌شود این کار باعث رندر شدن کامل داده بازگشتی می‌شود. برای شفاف شدن موضوع کد مربوطه را اینجا می‌گذارم
        <script type="text/ng-template" id="menu">
            {{item.caption}} -- <b>{{level}}</b>
            <ul ng-if="item.child.length > 0">
                <li id="{{item.caption}}" ng-finish-render="doWork()" ng-repeat="item in item.child" ng-include="'menu'" ng-init="level = level + 1"></li>
            </ul>
        </script>
        <div class="well">
            <ul>
                <li id="{{item.caption}}" ng-repeat="item in data" ng-include="'menu'" ng-init="level = level + 1" ng-finish-render="doWork()"></li>
            </ul>
        </div>
        $scope.doWork = function(){
             console.log($('#item2_1'));
        }
        $scope.level = 0;
        $scope.data = [
                    {
                        caption:'root',
                        child:[
                            {
                                caption:'item1',
                                child:[{
                                    caption:'item1_1',
                                    child:[{
                                        caption:'item1_1_1'
                                    }]
                                },{
                                    caption:'item1_1'
                                }]
                            },
                            {
                                caption:'item2',
                                child:[{
                                    caption:'item2_1',
                                    child:[{
                                        caption:'item2_1_1'
                                    }]
                                }]
                            }
                        ]
        
                    }
                ];