Question

I've been searching and I've found somewhat similar questions to mine, but none that quite match what I'm trying to do (or at least, the solutions have not worked for me). I'm really new to Durandal so I have little clue where to start to accomplish this. I'm working on a testing application and I have a div that is data-bound to display html like so:

Data-bind on the View

<div class="active-document-text" id="document-text" data-bind="html: documentBody">

In the javascript for the view model, I have it grab an external HTML file using an AJAX call. It works fine and binds to the view properly, displaying the document. My issue is that the external HTML will have one or more data-binds in it as well:

External.html

Lorem ipsum dolor 
<div class="selectable" id="selectable-1"
 data-bind="event: { click: $parent.onmouseClick }" >
sit amet, consectetur adipiscing</div> elit.
Integer nec odio. Praesent libero. Sed cursus ante dapibus diam.
Sed nisi. Nulla quis sem at nibh elementum imperdiet.

I'm wondering how I could set it up so that it'll data-bind those instances to the current viewmodel to be processed there. The idea is to have a selectable area of text (simple mouseover highlighting), and compare it against a currently selected index. A simpler explanation is that it would be similar to an application that provided a sentence and the user would click on the noun category and then select the noun in the sentence. As shown in the example above, the selectable area could be anywhere in the text. I've been able to get it to render all the html, but have been unsuccessful in getting the data-binding to work. I have tried applying a ko.applyBindings() after it as per knockout data-bind on dynamically generated elements, but I would receive an undefined router error, I've also tried the creating a compose passing in the data like inserting dynamic html into durandal views and it looks like the external html would have it's own .js model/viewmodel. Am I going about this completely the wrong way? Over-complicating it, perhaps? Originally, we had it broken up in a model where each section of text had a selectable property, but it's really clunky breaking up sizable documents and a HMTL formatting nightmare so I'm trying to find a more elegant solution. I appreciate your help!

EDIT

The following issue expands on this question: The long div tag that's required in order for it to incorporate the event bindings in the external html file is not friendly for non-developers who are likely to be the creators of the documents. I've currently got it grabbing the html file with the AJAX call again then replacing a simple custom '[selectable]' tag with the long div tag and storing that in an observable, but I'm still not sure how to incorporate it with bindings into the current view.

Here is the current look of it attempting to get it working. I've added double asterisks to the beginning of the particularly important lines.

The View:

<h3 data-bind="html: displayName"></h3>
<div class="document-analysis document-analysis-border">
    <span class="title-bar">Analyze Documents</span>
    <img src="./assets/images/arrow_minimize.jpg" class="minimize-button" />
    <div class="container">
        <div class="document-bar">
            <span class="title">Documents to Analyze</span><br />
            <img class="arrow-up" src="./assets/images/arrow_up.jpg" alt="Up Arrow" />
            <div data-bind="foreach: documentData()" class="documents scroll-bar">
                **<div data-bind="event: { click: function () { $parent.changeDocument($data); } }, attr: { id: 'document-' + id }" class="document">
                    <img data-bind="attr: { alt: title }" src="./assets/images/document_image.gif" class="document-image" />
                    <span data-bind="text: title" class="document-name"></span>
                </div>
            </div>
            <img class="arrow-down" src="./assets/images/arrow_down.jpg" alt="Down Arrow" />
        </div>
        <div class="inner-container">
            <div class="active-document">
                **<!--<div class="scroll-bar text" id="document-text" data-bind="compose: { view: currentDocument().url, transition: 'entrance' }"></div>-->
                **<div class="scroll-bar text" id="document-text" data-bind="compose: { view: documentFormatted, transition: 'entrance' }"></div>
                <button class="submit">Submit</button>
            </div>
            <div data-bind="foreach: bucketData()" class="buckets">
                <div data-bind="event: { click: function () { $parent.changeBucket($data); } }" class="bucket">
                    <img data-bind="attr: { id: 'bucket-' + id, src: image, alt: title }" src="//:0" class="bucket-image" />
                    <span data-bind="text: title" class="bucket-name"></span>
                </div>
            </div>
        </div>
    </div>
</div>

The first marked line calls the changeDocument() function when a new document is clicked. The second and third lines are the attempts to get the external documents working. The commented out compose works fine, but I have to use the long tags in order to facilitate the highlighting of text on mouseOver and mouseOut. The click is mostly used for debugging at the moment. If they click one of the buckets (categories), and then click the selectable area in the external document, it checks against data and if they selected the right category for the text selection, they earn points.

Here's the relevant viewmodel information:

var vm = {
        displayName: 'Document Analysis',
        currentDocument: ko.observable(docAnalysisObj.documents[0]),
        documentData: ko.observableArray(docAnalysisObj.documents),
        documentFormatted: ko.observable(),

        $init: $init,
        activate: activate,
        onmouseOver: onmouseOver,
        onmouseOut: onmouseOut,
        mouseClick: mouseClick,
        changeDocument: changeDocument,
        canDeactivate: canDeactivate,
        viewAttached: viewAttached
    };
    return vm;

function changeDocument(newDocument) {
        var self = this;
        // If they clicked the same document, ignore and return
        if (!newDocument || (self.currentDocument() && self.currentDocument().id === newDocument.id)) {
            return;
        }

        // Set the id selector name
        var docElementSelector = '#document-' + newDocument.id;

        // Remove the highlight from the previous class if it exists
        if (self.currentDocument()) {
            $('#document-' + self.currentDocument().id).removeClass('document-selected');
        }
        // Set the document to the new one
        self.currentDocument(newDocument);
        // Use data service to pull the html into self.documentFormatted
        dataservice.getDocument(self.documentFormatted, self.currentDocument().url);

        // Highlight the new current document
        $(docElementSelector).addClass('document-selected');

    }

The mouseOver and mouseOut really just add and remove CSS classes when mousing over a selectable area. changeDocument() is my attempt to load the html using the following dataservice object and handle the CSS changes.

The Dataservice object:

var getDocument = function (documentObservable, url) {
        documentObservable([]);

        if (!url) {
            console.log('Error: No url provided for document');
            documentObservable('<h1>Error: Document Not Found</h1>Document source was undefined.');
            return;
        }

        url = './app/views/' + url;

        var options = {
            url: url,
            type: 'GET',
            dataType: 'html',
            cache: false,
            error: queryFailed
        };

        return $.ajax(options)
            .then(querySucceeded);

        function querySucceeded(data) {
            console.log('Document \'' + url + '\' retrieval succeeded.');
            documentObservable(data);

            var currentID = 1;
            while (documentObservable().match(/\[selectable\]/g)) {
                documentObservable(documentObservable().replace('[selectable]', '<div class="selectable" selectID="' + currentID + '" data-bind="event: { mouseover: function () { $root.onmouseOver(' + currentID + '); }, mouseout: function () { $root.onmouseOut(' + currentID + '); }, click: function () { $root.mouseClick(' + currentID + '); } }">'));
                currentID++;
            }
        }

        function queryFailed(jqXHR, textStatus) {
            console.log('Error getting document ' + url + ': ' + textStatus);
            documentObservable('<h1>Error: Document Not Found</h1>' + textStatus);
        }

    };

The dataservice is the meat and potatoes of this. It loads the html and replaces all occurrences of of [selectable] with the long tag that will be used for data-binding. I haven't yet implemented the replacement for end tags, but that's a simple matter. The reason the div uses a custom attribute selectID instead of an ID is because the boss says using ID's is a bad idea as they can be duplicated across a document whereas a custom attribute is less likely to occur.

And a sample document:

[selectable]
            &bull; This is a sample selectable area. This will be highlighted<br />
            when someone mouses over it.
            <br />[/selectable]

The long divs have been replaced with [selectable] tags to make it easier for someone with basic HTML skills to build a sample document.

Ultimately, the goal is to give the person creating the document an easy tag to work with instead of having to try and paste the long tag in and keeping track of its individual ID. I'd like to keep the mouse events tied to the viewmodel since it's all the same activity (and the points for all the documents will be tallied together for a final score). From the user perspective, when they mouse over a selectable text, it should just change color (simple jQuery). If they click on it, it will check if they have the right category selected (I have this working already). My current issue is doing the text replacement and being able to bind the events to the view's functions.

Was it helpful?

Solution

I wouldn't use the knockoutjs html binding.

Instead use durandals compose binding to insert new elements into the dom. Durandal will handle the bindings for you too.

See this article: http://durandaljs.com/documentation/Using-Composition/

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top