I'm developing a new version of my website in Vue 2 and I have created a component to handle image broken links, swapping the url domain, in order to display images stored in my local host and also those stored in the production server. The problem is that sometimes the image filename in the server is different than what is set on my local host database (because the user changed the image on the production live version), and then I get infinite requests looping in the console since neither image url are valid.
I'm also using Bootstrap-Vue to lazy-load the images.
This is my code:
<template>
<b-img-lazy :src="'https://example.com/uploads/' + photo" #error.native="replaceSrc" :alt="titulo" :title="title" class="thumb"></b-img-lazy>
</template>
<script>
export default {
name: "ImgCarousel",
props: {
photo: {
type: String
},
title: {
type: String
}
},
data: function () {
return {
index: null
}
},
methods: {
replaceSrc(e) {
e.target.src = 'http://localhost:8888/uploads/' + this.photo;
},
}
}
</script>
How do I stop the GET requests from entering a infinite loop?
I also reached the solution below. Probably not elegant as Nikola's answer, although it needs less coding. Basically it adds a counter on each call for "replaceSrc" to prevent looping on the broken links:
<template>
<b-img-lazy :src="'https://example.com/uploads/' + photo" #error.native="replaceSrc" :alt="title" :title="title" class="thumb"></b-img-lazy>
</template>
<script>
export default {
name: "ImgCarousel",
props: {
photo: {
type: String
},
title: {
type: String
}
},
data: function () {
return {
counter: 0
}
},
methods: {
replaceSrc(e) {
if (this.counter == 0) {
e.target.src = 'http://localhost:8888/uploads/' + this.photo;
this.counter ++;
} else if (this.counter == 1) {
e.target.src = 'https://via.placeholder.com/300x190';
this.counter ++;
}
},
}
}
</script>
BTW the "index: null" data on my question was a wrong code I forgot to remove.
Try to check if image exists:
new Vue({
el: '#demo',
data: function () {
return {
startImg: 'https://picsum.photos/150',
index: null,
photo: '100',
title: '',
titulo: '',
url: 'https://picsum.photos/'
}
},
methods: {
replaceSrc(e) {
this.checkIfImageExists(this.url + this.photo, (exists) => {
exists ?
e.target.src = this.url + this.photo :
this.titulo = 'no image'
})
},
checkIfImageExists(url, callback) {
const img = new Image();
img.src = url;
if (img.complete) {
callback(true);
} else {
img.onload = () => callback(true)
img.onerror = () => callback(false)
}
},
change() {
this.startImg = this.startImg ? '' : 'https://picsum.photos/150'
},
breakSource() {
this.startImg = "wrong url"
this.photo = "wrong url"
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<link type="text/css" rel="stylesheet" href="https://unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="https://unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.css" />
<script src="https://unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.js"></script>
<script src="https://unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue-icons.min.js"></script>
<div id="demo">
<div>
<b-img-lazy :src="startImg" #error.native="replaceSrc" :title="title" :alt="titulo" class="thumb"></b-img-lazy>
</div>
<button #click="change">Change source</button>
<button #click="breakSource">Break source</button>
</div>
I am currently building out a Vue3 SPA and trying to use the oidc-client-ts javascript library for authenticating a user. Up to this point, I have been successful at implementing the login/logout functionality with my existing code. The problem I am experiencing is that I am not able to hook into the addUserLoaded or addUserSignedIn events that the UserManager provides (https://authts.github.io/oidc-client-ts/classes/UserManagerEvents.html).
One point to note is that when you land on my domain, we'll say myapp.mydomain.com, you are redirected to our identity server at myid.mydomain.com and then redirected back once login in successful.
On startup I've got the following
fetch("config.json", {'method': "GET"})
.then(response => {
if(response.ok) {
response.json().then(appConfigData => {
window.appConfig = appConfigData;
createApp(App)
.use(store)
.use(router)
.use(AccordionPlugin)
.component("font-awesome-icon", FontAwesomeIcon)
.mount("#app");
});
} else{
alert("Server returned " + response.status + " : " + response.statusText);
}
});
This window.appConfig has the settings object for initializing the UserManager
This is my AuthService.ts class which I export. Note the events in the constructor that I am registering to the UserManager.
import { UserManager, WebStorageStateStore, User, UserManagerEvents } from "oidc-client-ts";
import jwt_decode from "jwt-decode";
import store from "#/store";
export default class AuthService {
private userManager: UserManager;
constructor(configuration: any) {
const IDP_URL: string = configuration.IDP_URL;
const REDIRECT_URI: string = configuration.REDIRECT_URI;
const AUTH_TOKEN_URI: string = configuration.AUTH_TOKEN_URI;
const CLIENT_ID: string = configuration.CLIENT_ID;
const SILENT_RENEW_REDIRECT_URI: string = configuration.SILENT_RENEW_REDIRECT_URI;
const POPUP_REDIRECT_URI: string = configuration.POPUP_REDIRECT_URI;
const settings: any = {
userStore: new WebStorageStateStore({ store: window.localStorage }),
authority: IDP_URL,
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
popup_redirect_uri: POPUP_REDIRECT_URI,
response_type: "code",
automaticSilentRenew: true,
silent_redirect_uri: SILENT_RENEW_REDIRECT_URI,
scope: "openid profile ContactCenterApi.READ_WRITE",
post_logout_redirect_uri: AUTH_TOKEN_URI,
loadUserInfo: true
};
this.userManager = new UserManager(settings);
this.userManager.events.addUserLoaded(_args => {
console.log("USER LOADED EVENT");
debugger;
this.userManager.getUser().then(usr => {
store.dispatch("getProducts", usr?.profile.sub) // load the users products
});
});
this.userManager.events.addUserSignedIn(() => {
console.log("USER SIGNED IN EVENT");
debugger;
this.userManager.getUser().then(usr => {
store.dispatch("getProducts", usr?.profile.sub) // load the users products
});
});
}
public getUser(): Promise<User|null> {
return this.userManager.getUser();
}
public async login(): Promise<void> {
return this.userManager.signinRedirect();
}
public logout(): Promise<void> {
return this.userManager.signoutRedirect();
}
In my router/index.ts file I have the following
const auth = new AuthService(window.appConfig);
router.beforeEach(async (to, from, next) => {
const user = await auth.getUser();
const baseUri = window.appConfig.BASE_URI + "/#";
console.log("redirectedUri = " + baseUri + to.fullPath);
if (user) {
if (!user.expired) {
next();
} else {
sessionStorage.setItem("redirectedUri", baseUri + to.fullPath);
await auth.login();
}
} else {
sessionStorage.setItem("redirectedUri", baseUri + to.fullPath);
await auth.login();
}
});
export default router;
My logout button is pretty basic
import AuthService from "#/auth/AuthService";
onselect: async function(event: MenuEventArgs) {
var authService = new AuthService(window.appConfig); // settings are stored in the window when the app mounts
if(event.item.text === 'Logout') {
await authService.logout();
}
}
For example, one of the events that I would like to hook into is the addUserSignedIn event. However even when logging out and logging back in, the event does not fire. I would like to use this event to do some basic initialization of the user data. Any ideas on why these events are not firing?
Here is what ended up working for me. This is my AuthService.ts class that gets used around the codebase.
// AuthService.ts
import { UserManager, WebStorageStateStore, User } from "oidc-client-ts";
import jwt_decode from "jwt-decode";
import store from "#/store";
let userManager: UserManager;
const currentURL = document.location.origin;
fetch(currentURL + "/config.json", { 'method': "GET" })
.then(config => {
config.json()
.then(configuration => {
configuration['SSO_SETTINGS']['userStore'] = new WebStorageStateStore({ store: window.localStorage });
userManager = new UserManager(configuration['SSO_SETTINGS']);
userManager.events.addUserLoaded(newUser => {
console.log("USER LOADED EVENT");
userManager.storeUser(newUser);
userManager.getUser().then(usr => {
store.dispatch("getUserInfo", usr?.profile.sub) // load the user from my api
});
});
userManager.events.addAccessTokenExpired(() => {
userManager.signoutRedirect();
})
userManager.events.addSilentRenewError(error => {
console.log("ERROR RENEWING ACCESS TOKEN.");
console.log(error);
})
})
})
export default class AuthService {
public static getUser(): Promise<User | null> {
return userManager.getUser();
}
public static async login(): Promise<void> {
return userManager.signinRedirect();
}
public static logout(): Promise<void> {
localStorage.clear();
sessionStorage.clear();
return userManager.signoutRedirect();
}
}
My updated SSO settings object in my config.json that gets passed to the user manager
"SSO_SETTINGS": {
"userStore": "",
"authority": "https://myauthorityurl.com",
"client_id": "MyClientName",
"redirect_uri": "http://localhost:8080/callback.html",
"response_type": "code",
"response_mode": "query",
"automaticSilentRenew": true,
"monitorSession": true,
"silent_redirect_uri": "http://localhost:8080/silent-renew.html",
"scope": "openid profile",
"post_logout_redirect_uri": "http://localhost:8080",
"loadUserInfo": true,
"metadata": {
"issuer": "https://myauthorityurl.com",
"jwks_uri": "https://myauthorityurl.com/.well-known/openid-configuration/jwks",
"authorization_endpoint": "https://myauthorityurl.com/connect/authorize",
"end_session_endpoint": "https://myauthorityurl/Account/Logout",
"token_endpoint": "https://iddev02.carexm.com/connect/token",
"userinfo_endpoint": "https://iddev02.carexm.com/connect/userinfo"
}
},
And for the sake of completeness, here are my callback.html and silent-redirect.html pages. These two files are placed in the top level public folder along with your config.json and oidc-client-ts.min.js file.
callback.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Waiting...</title>
</head>
<body>
<script src="oidc-client-ts.min.js"></script>
<script>
var config = fetch("config.json", {'method': "GET"})
.then(config =>
{
config.json().then(settings => {
var mgr = new oidc.UserManager(settings['SSO_SETTINGS']);
mgr.settings.userStore = new oidc.WebStorageStateStore({ store: window.localStorage });
mgr.signinRedirectCallback()
.then(() =>
{
window.location.href = sessionStorage.getItem("redirectedUri") ?? "../";
})
})
});
</script>
</body>
</html>
silent-renew.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Waiting...</title>
</head>
<body>
<script src="oidc-client-ts.min.js"></script>
<script>
var config = fetch("config.json", {'method': "GET"})
.then(config =>
{
config.json().then(settings => {
var mgr = new oidc.UserManager(settings['SSO_SETTINGS']).signinSilentCallback();
})
});
</script>
</body>
</html>
In our application, there is large set of data (json ~ 630KB) getting populated which consumes around 219.28 MB of JavaScript memory displayed in chrome task manager.
It loads the data from the api and uses vuex to store the data and generates the menu recursively.
Since it is a big application, I mimicked the same behaviour with a sample vue.js application by loading 100000 records to understand how the memory utilization works.
Each time "Run test" is clicked, 200 MB memory is consumed in the task manager and is not released until so much time even if the page is refreshed.
I tried to use "beforeDestroy" hook to clear the object from vuex but it is not working. I would like to know if the data itself is causing the slow performance or any other improvements can be done.
<html>
<head>
</head>
<body>
<div id="myApp">
<button #click="setCounterModule()">Counter</button>
<button #click="setCustomersModule">Customers</button>
<br />
<br />
<div>
<button-counter v-if="currentModule=='counter'"></button-counter>
<customers v-else></customers>
</div>
</div>
<script src="vue.js"></script>
<script src="vuex.js"></script>
<script>
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
});
Vue.component('customers', {
computed: {
customers() {
return store.state.customers
}
},
template: '<div><button #click="testMe" :disabled="loading">Run Test</button><div v-for="c in customers">{{ c.name }}</div></div>',
data: function() {
return {
loading: false
}
},
methods: {
testMe: function () {
this.loading = true;
let customers = [];
for (var i=0; i < 100000; i++) {
let customer = {'name': 'John' +i, 'address': 'Addr' + i};
customers.push(customer);
}
store.commit('assignCustomers', customers);
this.loading = false;
}
},
beforeDestroy() {
// console.log('vuex destroyed');
// store.commit('assignCustomers', null);
}
});
const store = new Vuex.Store({
state: {
customers: null
},
mutations: {
assignCustomers (state, customers) {
state.customers = customers;
}
}
});
var app = new Vue( {
el: "#myApp",
data: function() {
return {
currentModule: "counter"
}
},
methods: {
setCounterModule: function() {
this.currentModule = "counter";
},
setCustomersModule: function() {
this.currentModule = "customers";
}
}
});
</script>
</body>
</html>
I created an element with class foo. Then I intended to swap its class with bar after an click event was occurred. However, when I clicked the element, streams that subscribes the click events of the foo and bar were triggered successively. As a result, element's class didn't change.
How can I subscribe events of the same element where its classes change in time?
Here's the link:
https://jsbin.com/kanomonexa/edit?html,js,console,output
Here's the sample code:
const {div, button, makeDOMDriver} = CycleDOM;
const toggleDriver = (data$)=> {
data$.subscribe(function(data) {
if (data.operation === 'remove' ) {
$(data.selector).removeClass(data.className);
}
else if (data.operation === 'add') {
$(data.selector).addClass(data.className);
}
else {
$(data.selector).toggle(data.className);
}
});
return Rx.Observable.empty();
};
const consoleLogDriver = (data$)=>{
data$.subscribe(data=> {
console.log(data);
});
return Rx.Observable.empty();
};
const main = (sources) =>{
const fooClick$ = sources.DOM
.select('.foo')
.events('click');
const fooLog$ = fooClick$.map(_ =>'.foo is Clicked');
const toggleFoo$ = fooClick$.flatMap(_ => {
const result = [
{
operation: 'remove',
selector: 'button',
className: 'foo'
},
{
operation: 'add',
selector: 'button',
className: 'bar'
}
];
return Rx.Observable.fromArray(result);
});
const barClick$ = sources.DOM
.select('.bar')
.events('click');
const barLog$ = barClick$.map(_ => '.bar is Clicked');
const toggleBar$ = barClick$.flatMap(_ =>{
const result = [
{
operation: 'remove',
selector: 'button',
className: 'bar'
},
{
operation: 'add',
selector: 'button',
className: 'foo'
}
];
return Rx.Observable.fromArray(result);
});
const log$ = Rx.Observable.merge(
fooLog$,
barLog$
);
const toggle$ = Rx.Observable.merge(
toggleFoo$,
toggleBar$
);
const vTree$ = Rx.Observable.of(div([
button('#button.foo',['Click me'])
]));
return {
DOM: vTree$,
consoleLogDriver: log$,
toggleDriver:toggle$
};
};
var drivers = {
DOM: makeDOMDriver('#app'),
toggleDriver: toggleDriver,
consoleLogDriver: consoleLogDriver
};
Cycle.run(main, drivers);
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.0.6/rx.all.js"></script>
<script src="https://code.jquery.com/jquery-2.1.4.js"></script>
<script src="https://rawgit.com/cyclejs/cycle-core/v6.0.3/dist/cycle.js"></script>
<script src="https://rawgit.com/cyclejs/cycle-dom/v9.4.0/dist/cycle-dom.js"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
You may delay(0) your toggle$ stream to schedule its values to the next event loop and then successfully change your className:
...
const toggle$ = Observable.merge(...)
.delay(0)
...
I see that you are using Jquery to toggle your class. Depending on what you want to do, I imagine it can have some usefull cases (eg: toggle a class somewhere else in the page out of scope from virtual dom).
But there your cycle app is handled by virtual dom so I suggest you to use the right tool for the job and instead rebuild your vtree$ on each change:
...
const currentClassName$ = Rx.Observable
.merge(
fooClick$.map(_ => '.bar'),
barClick$.map(_ => '.foo')
)
.delay(0) // fix the bug
.startWith('.foo');
const vTree$ = currentClassName$
.map(currentClassName =>
div([
button(
'#button' + currentClassName, ['Click me']
)
]));
...
Working demo
I want to bind CKEditor text to ng-model text. My View:
<form name="postForm" method="POST" ng-submit="submit()" csrf-tokenized class="form-horizontal">
<fieldset>
<legend>Post to: </legend>
<div class="control-group">
<label class="control-label">Text input</label>
<div class="controls">
<div class="textarea-wrapper">
<textarea id="ck_editor" name="text" ng-model="text" class="fullwidth"></textarea>
</div>
<p class="help-block">Supporting help text</p>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Post</button>
<button class="btn">Cancel</button>
<button class="btn" onclick="alert(ckglobal.getDate())">Cancel123</button>
</div>
</fieldset>
</form>
controller
function PostFormCtrl($scope, $element, $attrs, $transclude, $http, $rootScope) {
$scope.form = $element.find("form");
$scope.text = "";
$scope.submit = function() {
$http.post($scope.url, $scope.form.toJSON()).
success(function(data, status, headers, config) {
$rootScope.$broadcast("newpost");
$scope.form[0].reset();
});
};
$scope.alert1 = function(msg) {
var sval = $element.find("ckglobal");
//$('.jquery_ckeditor').ckeditor(ckeditor);
alert(sval);
};
}
PostFormCtrl.$inject = ["$scope", "$element", "$attrs", "$transclude", "$http", "$rootScope"];
I want to set CKEditor value in $scope.text at the time of form submit.
CKEditor does not update textarea while typing, so you need to take care of it.
Here's a directive that will make ng-model binding work with CK:
angular.module('ck', []).directive('ckEditor', function() {
return {
require: '?ngModel',
link: function(scope, elm, attr, ngModel) {
var ck = CKEDITOR.replace(elm[0]);
if (!ngModel) return;
ck.on('pasteState', function() {
scope.$apply(function() {
ngModel.$setViewValue(ck.getData());
});
});
ngModel.$render = function(value) {
ck.setData(ngModel.$viewValue);
};
}
};
});
In html, just use:
<textarea ck-editor ng-model="value"></textarea>
The previous code will update ng-model on every change.
If you only want to update binding on save, override "save" plugin, to not do anything but fire "save" event.
// modified ckeditor/plugins/save/plugin.js
CKEDITOR.plugins.registered['save'] = {
init: function(editor) {
var command = editor.addCommand('save', {
modes: {wysiwyg: 1, source: 1},
readOnly: 1,
exec: function(editor) {
editor.fire('save');
}
});
editor.ui.addButton('Save', {
label : editor.lang.save,
command : 'save'
});
}
};
And then, use this event inside the directive:
angular.module('ck', []).directive('ckEditor', function() {
return {
require: '?ngModel',
link: function(scope, elm, attr, ngModel) {
var ck = CKEDITOR.replace(elm[0]);
if (!ngModel) return;
ck.on('save', function() {
scope.$apply(function() {
ngModel.$setViewValue(ck.getData());
});
});
}
};
});
If you simply want to retrieve text in the editor textarea in angular, call CKEDITOR.instances.editor1.getData(); to get the value directly in an angularjs function. See below.
In your html
In the test.controller.js
(function () {
'use strict';
angular
.module('app')
.controller('test', test);
test.$inject = [];
function test() {
//this is to replace $scope
var vm = this;
//function definition
function postJob()
{
vm.Description = CKEDITOR.instances.editor1.getData();
alert(vm.Description);
}
}
})();
the Vojta answer work partly
in this post I found a solution
https://stackoverflow.com/a/18236359/1058096
the final code:
.directive('ckEditor', function() {
return {
require : '?ngModel',
link : function($scope, elm, attr, ngModel) {
var ck = CKEDITOR.replace(elm[0]);
ck.on('instanceReady', function() {
ck.setData(ngModel.$viewValue);
});
ck.on('pasteState', function() {
$scope.$apply(function() {
ngModel.$setViewValue(ck.getData());
});
});
ngModel.$render = function(value) {
ck.setData(ngModel.$modelValue);
};
}
};
})
edit: removed unused brackets
Thanks to Vojta for the excellent directive. Sometimes it doesn't load. Here is a modified version to fix that issue.
angular.module('ck', []).directive('ckEditor', function() {
var calledEarly, loaded;
loaded = false;
calledEarly = false;
return {
require: '?ngModel',
compile: function(element, attributes, transclude) {
var loadIt, local;
local = this;
loadIt = function() {
return calledEarly = true;
};
element.ready(function() {
return loadIt();
});
return {
post: function($scope, element, attributes, controller) {
if (calledEarly) {
return local.link($scope, element, attributes, controller);
}
loadIt = (function($scope, element, attributes, controller) {
return function() {
local.link($scope, element, attributes, controller);
};
})($scope, element, attributes, controller);
}
};
},
link: function($scope, elm, attr, ngModel) {
var ck;
if (!ngModel) {
return;
}
if (calledEarly && !loaded) {
return loaded = true;
}
loaded = false;
ck = CKEDITOR.replace(elm[0]);
ck.on('pasteState', function() {
$scope.$apply(function() {
ngModel.$setViewValue(ck.getData());
});
});
ngModel.$render = function(value) {
ck.setData(ngModel.$viewValue);
};
}
};
});
or if you need it in coffeescript
angular.module('ck', []).directive('ckEditor', ->
loaded = false
calledEarly = false
{
require: '?ngModel',
compile: (element, attributes, transclude) ->
local = #
loadIt = ->
calledEarly = true
element.ready ->
loadIt()
post: ($scope, element, attributes, controller) ->
return local.link $scope, element, attributes, controller if calledEarly
loadIt = (($scope, element, attributes, controller) ->
return ->
local.link $scope, element, attributes, controller
)($scope, element, attributes, controller)
link: ($scope, elm, attr, ngModel) ->
return unless ngModel
if (calledEarly and not loaded)
return loaded = true
loaded = false
ck = CKEDITOR.replace(elm[0])
ck.on('pasteState', ->
$scope.$apply( ->
ngModel.$setViewValue(ck.getData())
)
)
ngModel.$render = (value) ->
ck.setData(ngModel.$viewValue)
}
)
And just for the record if you want to use multiple editors in one page this can come in handy:
mainApp.directive('ckEditor', function() {
return {
restrict: 'A', // only activate on element attribute
scope: false,
require: 'ngModel',
controller: function($scope, $element, $attrs) {}, //open for now
link: function($scope, element, attr, ngModel, ngModelCtrl) {
if(!ngModel) return; // do nothing if no ng-model you might want to remove this
element.bind('click', function(){
for(var name in CKEDITOR.instances)
CKEDITOR.instances[name].destroy();
var ck = CKEDITOR.replace(element[0]);
ck.on('instanceReady', function() {
ck.setData(ngModel.$viewValue);
});
ck.on('pasteState', function() {
$scope.$apply(function() {
ngModel.$setViewValue(ck.getData());
});
});
ngModel.$render = function(value) {
ck.setData(ngModel.$viewValue);
};
});
}
}
});
This will destroy all previous instances of ckeditor and create a new one.
There's now a 'change' event that can be used for this.
Here's a directive I've just created that has a couple of different toolbar config options, I'm using the jQuery adapter to initialize ckeditor. For more info check out this blog post.
(function () {
'use strict';
angular
.module('app')
.directive('wysiwyg', Directive);
function Directive($rootScope) {
return {
require: 'ngModel',
link: function (scope, element, attr, ngModel) {
var editorOptions;
if (attr.wysiwyg === 'minimal') {
// minimal editor
editorOptions = {
height: 100,
toolbar: [
{ name: 'basic', items: ['Bold', 'Italic', 'Underline'] },
{ name: 'links', items: ['Link', 'Unlink'] },
{ name: 'tools', items: ['Maximize'] },
{ name: 'document', items: ['Source'] },
],
removePlugins: 'elementspath',
resize_enabled: false
};
} else {
// regular editor
editorOptions = {
filebrowserImageUploadUrl: $rootScope.globals.apiUrl + '/files/uploadCk',
removeButtons: 'About,Form,Checkbox,Radio,TextField,Textarea,Select,Button,ImageButton,HiddenField,Save,CreateDiv,Language,BidiLtr,BidiRtl,Flash,Iframe,addFile,Styles',
extraPlugins: 'simpleuploads,imagesfromword'
};
}
// enable ckeditor
var ckeditor = element.ckeditor(editorOptions);
// update ngModel on change
ckeditor.editor.on('change', function () {
ngModel.$setViewValue(this.getData());
});
}
};
}
})();
Here's a couple of examples of how to use the directive in HTML
<textarea ng-model="vm.article.Body" wysiwyg></textarea>
<textarea ng-model="vm.article.Body" wysiwyg="minimal"></textarea>
And here are the CKEditor scripts I'm including from a CDN plus a couple of extra plugins that I downloaded to enable pasting images from word.
<script src="//cdn.ckeditor.com/4.5.7/full/ckeditor.js"></script>
<script src="//cdn.ckeditor.com/4.5.7/full/adapters/jquery.js"></script>
<script type="text/javascript">
// load extra ckeditor plugins
CKEDITOR.plugins.addExternal('simpleuploads', '/js/ckeditor/plugins/simpleuploads/plugin.js');
CKEDITOR.plugins.addExternal('imagesfromword', '/js/ckeditor/plugins/imagesfromword/plugin.js');
</script>
ES6 example with CKEditor v5.
Register directive using:
angular.module('ckeditor', []).directive('ckEditor', CkEditorDirective.create)
Directive:
import CkEditor from "#ckeditor/ckeditor5-build-classic";
export default class CkEditorDirective {
constructor() {
this.restrict = 'A';
this.require = 'ngModel';
}
static create() {
return new CkEditorDirective();
}
link(scope, elem, attr, ngModel) {
CkEditor.create(elem[0]).then((editor) => {
editor.document.on('changesDone', () => {
scope.$apply(() => {
ngModel.$setViewValue(editor.getData());
});
});
ngModel.$render = () => {
editor.setData(ngModel.$modelValue);
};
scope.$on('$destroy', () => {
editor.destroy();
});
})
}
}