In Part 1 of this blog post you learned how to start developing custom component for Oracle Sites Cloud Service. In Part 2 you will complete development and create component that allows editing text in-place and persisting it in SCS page model. I will also explain how to make your custom component to participate in Undo / Redo action in the SCS Site Builder.
Step 5: Handle the Site Builder Edit / Preview Modes
In this step you will ensure that the DIV element’s content is not editable during preview or at runtime. To do this you need to have the component template display different content based on the mode passed in to the component.
In template.html, make the following change to the HTML:
<!-- ko if: initialized --> <div class="my-custom-component"> <!-- ko if: editMode --> <div contenteditable="true" data-bind="html: editText" style="-webkit-user-select: text; user-select: text;"></div> <!-- /ko --> <!-- ko ifnot: editMode --> <div data-bind="html: text"></div> <!-- /ko --> </div> <!-- /ko -->
Then in render.js, update the SampleComponentViewModel object to create the “editMode” observable:
Change:
// store standard args self.mode = args.viewMode; self.id = args.id;
To:
// store standard args self.mode = args.viewMode; self.id = args.id; // note if in Edit mode self.editMode = self.mode === 'edit';
Now when the mode is passed in, it will either display “edit me” when editing or “a text component” when previewing since we haven’t saved and synced the editing changes.
Checkpoint 5:
- Sync your changes
- Take the page into Edit mode. In Edit mode you will see the “Edit me” text.
- Click on “Edit me” and validate that you can update the text.
- Take the page into Preview mode.
- In Preview mode (or at runtime if you publish the site) your will see “Welcome to Components World!” text displayed.
- You also can no longer edit the text in Preview mode.
Step 6: Persist Text Edit Changes
In this step, you will persist the changes the end user makes and be able to see those changes copied across to be visible when you preview the component.
1. At the top of render.js, create a custom binding handler to handle the focus and blur events from the ‘contenteditable’ DIV element:
Change:
'use strict';
To:
'use strict'; // support in-place editing of text ko.bindingHandlers.myCompSetup = { init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { $(element).on('blur', function(event) { viewModel.handleBlur(event); }); $(element).on('focus', function(event) { viewModel.handleFocus(event); }); } };
2. In template.html file, update the ‘contenteditable’ DIV element:
Change:
<div contenteditable=true data-bind="html: editText" style="-webkit-user-select: text; user-select: text;"></div>
To:
<div contenteditable=true data-bind="html: editText, myCompSetup: true" style="-webkit-user-select: text; user-select: text;"></div>
3. In render.js file, replace the SampleComponentViewModel object with the implementation of the blur/ focus events required by the custom binding handler and persist the change:
// ---------------------------------------------- // Define a Knockout ViewModel for your template // ---------------------------------------------- var SampleComponentViewModel = function (args) { var self = this, SitesSDK = args.SitesSDK, nbspChar = String.fromCharCode(65279); // store standard args self.mode = args.viewMode; self.id = args.id; // note if in Edit mode self.editMode = self.mode === 'edit'; // create observables to persiste the data self.text = ko.observable(''); // set the default value for text self.defaultText = 'Edit me'; self.editText = ko.observable(''); self.editingText = ko.observable(false); // support in-place editing the text self.handleFocus = function(event) { // workaround Firefox issue with parent draggable - turn draggable back off SCSRenderAPI.setComponentDraggable(self.id, false); // set the user text to the actual text value self.editText(self.text() || nbspChar); // started text edit self.editingText(true); }; self.handleBlur = function(event) { var $span = $(event.target), userInput = $span.html(); // workaround Firefox issue with parent draggable - turn draggable back on SCSRenderAPI.setComponentDraggable(self.id, true); // copy the final text value into the text value if (!userInput || userInput === nbspChar) { self.text(""); // make sure notified that value has changed self.text.valueHasMutated(); } else { // update with the new value self.text(userInput); } // completed text edit self.editingText(false); }; // save changes for text self.text.subscribe(function(val) { if (self.saveContent) { SitesSDK.setProperty('customSettingsData', { 'text' : val } ); } }); // handle initialization self.customDataInitialized = ko.observable(false); self.initialized = ko.computed(function () { var initialized = self.customDataInitialized(); if (initialized) { // only writeback content after initialization self.saveContent = true; } return initialized; }, self); // handle property changes self.updateCustomSettingsData = function(customSettingsData) { if (customSettingsData && customSettingsData.text) { self.text(customSettingsData.text); } self.editText(self.text() ? self.text() : self.defaultText); self.customDataInitialized(true); }; // initialize the viewModel // // get the Custom Settings Data, we need both before first render SitesSDK.getProperty('customSettingsData', self.updateCustomSettingsData); };
The way this code works is that:
- Whenever the blur event occurs, the self.handleBlur() function writes changes to the text through to the self.text observable.
- Whenever the self.text observable changes, the self.text.subscribe() function sets the “customSettingsData” to the current value of self.text(), which the SDK then saves to the page data.
- We also don’t want to write the initial value that is retrieved for self.text() on initialization so we only write back to “customSettingsData” when we want to save the content (self.saveContent is set to true).
- Finally, we want to have some “placeholder” text so we have introduced the “defaultText”, which disappears when the user clicks on editText and there is nothing yet stored
Checkpoint 6
You can now edit the text in Edit mode and have the changes display in Preview mode. The changes are now saved as part of the page data.
- Sync your changes.
- Refresh site page in the Builder to pick up changes to the component.
- Take the page into Edit mode.
- Click on the “Edit me” text displayed in the component and make a change.
- Click out of the text and then switch to Preview mode.
- At this point you will see your changes persisted.
- Take the page back into edit mode. Again, you will see the text you previously entered.
Step 7: Integration with Site Builder Undo / Redo
In the Site Builder, as you made change to the text and clicked outside, you may have noticed the “Undo/ Redo” icons at the top of the page become enabled. This is because you are saving information to your component, which in turn stores that information in the page data. However, if you clicked undo, you would see no change to your component until you went all the way to removing the component from the page.
The reason for this is that you don’t have any code to handle the Undo/ Redo events within the viewModel. Whenever an Undo/ Redo event occurs that affects your custom component, the “updateSettings” event is called with the current data for the component. The component should re-render with this data to reflect the current state of the component on the page.
To update your component to take part in the page’s Undo/ Redo events, do the following to support listening for updates to the “customSettingsData”:
Change the SampleComponentViewModel to include:
// listen for settings update self.updateSettings = function(settings) { if (settings.property === 'customSettingsData') { // turn off saving content self.saveContent = false; // update the content self.updateCustomSettingsData(settings.value); // turn on saving content self.saveContent = true; } }; SitesSDK.subscribe(SitesSDK.MESSAGE_TYPES.SETTINGS_UPDATED, self.updateSettings); // initialize the viewModel // // get the Custom Settings Data, we need both before first render SitesSDK.getProperty('customSettingsData', self.updateCustomSettingsData);
Now, whenever the underlying settings value changes, the component will receive the event and update the text.
Checkpoint 7
- Sync the file.
- Take the page into Edit mode.
- Click on the text and change it to “one”
- Click out of the text
- Click on the text and change it to “two”
- Click out of the text
- Click on the text and change it to “three”
- Click out of the text
- Now click “Undo”
- The text will update to “two”
- Click “Undo” again
- The text will update to “one”
- Click “Undo” again
- The text will update to the default value
- Clicking “Redo” does the opposite as expected
Step 8: Review
You now have a very Basic Text Editor component. For a fuller editor features, you can integrate a third party library to include a toolbar such as CKEditor, which is already part of the SCS code stack or, if you are using a Bootstrap theme, Tiny Editor.
Step 9: Suggested Further Steps
As an exercise you can further enhance Basic Text Editor component:
- Add protection against cross-site scripting attacks
- Strip out any un-allowed tags in the HTML that was entered by the user before the HTML is rendered into the page
- This is not specific to custom components but you should always make sure you encode any user entered input before you display it to avoid XSS attacks
- Add Styles
- Update the settings.html to handle an LOV of styles to select (<h1>, <h2>, <h3>, <p>) and wrap the rendered content with the selected style
- Add in a 3rd party editing toolbar:
- Integrate with Tiny Editor or CKEditor
- Add Link with Triggers:
- Set the link around all the text and set from the settings panel or allow the user to select some text and raise a trigger from it
- Implement an Action to change the text
- Add in an action that will set the text value to that passed in