0

This is a simplified example of a bug we faced in our project recently.

I have a list of objects with "name" and "position" properties and wanna use TinyMCE editor instead of textarea to display "name".

Besides, the list is ordered by "position" property which is editable.

Noticed that once "position" property is changed(list is reordered), TinyMCE editor becomes empty.

Anyone has any ideas why it happens and how to fix this?

Example code: JsFiddle

HTML

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
<script src="//tinymce.cachefly.net/4.1/tinymce.min.js"></script>
<body ng-app="myApp">
    <div ng-controller="MyCtrl">
        <p>List of activities:</p>
        <div ng-repeat="activity in model.activities | orderBy: 'position'">
             <label for="$index">Position</label>
            <input id="$index" type="number" ng-model="activity.position" style="width: 50px">
            <textarea ng-model="activity.name" ui-tinymce="tinyMceOptions" rows="2" cols="10"></textarea>

            <hr>
        </div>
    </div>
</body>

JS

var myApp = angular.module('myApp', ['ui.tinymce']);
/**
 * Binds a TinyMCE widget to <textarea> elements.
 */
angular.module('ui.tinymce', [])
  .value('uiTinymceConfig', {})
  .directive('uiTinymce', ['uiTinymceConfig', function (uiTinymceConfig) {
    uiTinymceConfig = uiTinymceConfig || {};
    var generatedIds = 0;
    return {
      priority: 10,
      require: 'ngModel',
      link: function (scope, elm, attrs, ngModel) {
        var expression, options, tinyInstance,
          updateView = function () {
            ngModel.$setViewValue(elm.val());
            if (!scope.$root.$$phase) {
              scope.$apply();
            }
          };

        // generate an ID if not present
        if (!attrs.id) {
          attrs.$set('id', 'uiTinymce' + generatedIds++);
        }

        if (attrs.uiTinymce) {
          expression = scope.$eval(attrs.uiTinymce);
        } else {
          expression = {};
        }

        // make config'ed setup method available
        if (expression.setup) {
          var configSetup = expression.setup;
          delete expression.setup;
        }

        options = {
          // Update model when calling setContent (such as from the source editor popup)
          setup: function (ed) {
            var args;
            ed.on('init', function(args) {
              ngModel.$render();
              ngModel.$setPristine();
            });
            // Update model on button click
            ed.on('ExecCommand', function (e) {
              ed.save();
              updateView();
            });
            // Update model on keypress
            ed.on('KeyUp', function (e) {
              ed.save();
              updateView();
            });
            // Update model on change, i.e. copy/pasted text, plugins altering content
            ed.on('SetContent', function (e) {
              if (!e.initial && ngModel.$viewValue !== e.content) {
                ed.save();
                updateView();
              }
            });
            ed.on('blur', function(e) {
                elm.blur();
            });
            // Update model when an object has been resized (table, image)
            ed.on('ObjectResized', function (e) {
              ed.save();
              updateView();
            });
            if (configSetup) {
              configSetup(ed);
            }
          },
          mode: 'exact',
          elements: attrs.id
        };
        // extend options with initial uiTinymceConfig and options from directive attribute value
        angular.extend(options, uiTinymceConfig, expression);
        setTimeout(function () {
          tinymce.init(options);
        });

        ngModel.$render = function() {
          if (!tinyInstance) {
            tinyInstance = tinymce.get(attrs.id);
          }
          if (tinyInstance) {
            tinyInstance.setContent(ngModel.$viewValue || '');
          }
        };

        scope.$on('$destroy', function() {
          if (!tinyInstance) { tinyInstance = tinymce.get(attrs.id); }
          if (tinyInstance) {
            tinyInstance.remove();
            tinyInstance = null;
          }
        });
      }
    };
  }]);

myApp.controller("MyCtrl", ["$scope", function($scope) {
    $scope.model = { 
        activities: [
            {name: "activity 1", position: 1}, 
            {name: "activity 2", position: 2}, 
            {name: "activity 3", position: 3}, 
            {name: "activity 4", position: 4}, 
            {name: "activity 5", position: 5}
        ] 
    };
    $scope.tinyMceOptions = {
        selector: "textarea",
        theme: "modern",
        plugins: [
            "autolink lists link charmap print preview hr anchor pagebreak autoresize",//advlist
            "searchreplace visualblocks visualchars code",
            "insertdatetime nonbreaking save table directionality",
            "emoticons template paste textcolor colorpicker textpattern"
        ],
        toolbar1: "bold italic underline |  alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | forecolor backcolor | undo redo | link | pastetext",
        paste_auto_cleanup_on_paste: true,
        paste_strip_class_attributes: 'mso',
        paste_data_images: false,
        theme_advanced_buttons3_add: "pastetext,pasteword,selectall",
        image_advtab: true,
        //templates: [
        //    {title: 'Test template 1', content: 'Test 1'},
        //    {title: 'Test template 2', content: 'Test 2'}
        //],
        browser_spellcheck: true,
        menubar: false,
        //theme_advanced_disable: "bullist,numlist",
        target_list: [{ title: 'New page', value: '_blank' }],
        //advlist_number_styles: [
        //    {title : 'Standard', styles : {listStyleType : ''}},
        //    {title : 'a. b. c.', styles : {listStyleType : 'lower-alpha'}}
        //],
        //spellchecker_languages: "+English=en",
        //spellchecker_rpc_url: 'spellchecker.php',
        handle_event_callback: function (e) {
            // put logic here for keypress
        }
    };
}]);
  • 2
    Should really use a directive that initializes tinyMCE on element instances. There are several already available – charlietfl May 06 '15 at 16:29
  • Ok will rewrite the fiddle, though not sure this might help. – Ilia Syrtsou May 06 '15 at 16:40
  • 1
    If you try to investigate these questions: http://stackoverflow.com/questions/20244802/ngmodel-render-is-not-called-when-model-change and http://stackoverflow.com/questions/20244802/ngmodel-render-is-not-called-when-model-change you will determine that the issue is that ngModel.$render doesn't fire every time, when ngModel changes. – Artyom Pranovich May 07 '15 at 08:41

0 Answers0