Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

BreezeJS integration with DurandalJS issues related to how KnockoutJS is loaded

Durandal.JS and Breeze.JS have some troubles playing together. Durandal is based on a few libraries, the two of note are Require and Knockout. My original project, prior to using the modular pattern introduced by Require, used the Knockout style bindings on the Breeze models.

On my journey, I found out that Breeze can work with multiple libraries for Breeze's models, such as Backbone, Knockout, Angular, and other frameworks. When Breeze is loaded as a Require module, Breeze checks to see if the module for Knockout is present, with the alias of "ko". This module name conflicts with how Durandal aliases Knockout, as Durandal uses the module name "knockout" instead.

When Breeze loads, a check is made to determine how to present the data properties of the Breeze models. In my original project, Breeze would detect Knockout in the global scope and assign all properties "ko.observable()" style properties.

How can I get these modules to play together properly? I've tried several Require.JS tricks, such as adding this Shim: (from this post)

breeze: { deps: ['ko', 'jQuery', 'Q'] }

and adding these dummy module definitions:

define('ko', ['knockout'], function (ko) { return ko; });
define('Q', ['q'], function (Q) { return Q; });
define('jQuery', ['jquery'], function ($) { return $; });

in my main.js. The combination allows Breeze to run. I'm able to execute queries successfully against the backend API.

HOWEVER, the results do not properly get turned into Knockout observable. Instead, Breeze seems to be using native ES5 observable properties. While this is actually kinda cool, it completely breaks my existing modules.

To note, as suggested by the Druandal documentation, I am overriding the internal Promise library with Q using a snippet they provided.

   system.defer = function (action) {
        var deferred = Q.defer();
        action.call(deferred, deferred);
        var promise = deferred.promise;
        deferred.promise = function () {
            return promise;
        };
        return deferred;
    };

These same issues presented themselves with both the Q library, and the jQuery library, though the above shims and dummy modules corrected the behavior. I have no idea what to try next.

EDIT: In response to "Show your Context Setup" comment:

define([
'breeze',
'q',
'durandal/system',
'lodash'
],
function (breeze, Q, system, _) {
    return new function () {
        var self = this;

        self.create = create;
        self.init = init;

        var EntityQuery = breeze.EntityQuery;
        var BREEZE_URL = '/breeze/AtlasApi/';
        var masterManager = new breeze.EntityManager(BREEZE_URL);
        self.masterManager = masterManager;

        function init() {

            return masterManager.fetchMetadata()
                    .fail(function (error) {
                        system.error(error);
                    });
        };
        function create() {
            var manager = masterManager.createEmptyCopy();

            return manager;
        };
    };
});

The above module I load once at AppStart and call the Init method to ensure I have metadata. I then call .create() elsewhere to create an empty, isolated copy. This works quite well in a non Require.js environment. I use promises to ensure that init step has completed. I can run queries by hand and they work, minus the way Breeze materializes the entities (again, as ES5 properties, rather than knockout properties)

like image 715
Oliver Kane Avatar asked Mar 22 '23 01:03

Oliver Kane


1 Answers

Looks like you are trying to load every library through requireJS. The out-of-the-box Durandal approach as I recall is to load 3rd party scripts directly (outside of require) and only use require for application scripts.

That simplifies things but it isn't the only way to do it and a lot of people want to use require to load all of their scripts.

We recently (v.1.4.7) updated the "Todo-Require" sample to demonstrate that approach. I realize it is not a Durandal app but I'm hopeful that you'll find the direction you need.

I'll copy here the gist of what I think will be helpful to you.

index.html

...
<body>
    <div id="applicationHost"></div>

    <!-- Require + main. All scripts retrieved async by requireJS -->
    <script data-main="Scripts/app/main" src="Scripts/require.js"></script>
</body>
...

main.js

(function () {
    requirejs.config({
        paths: {
            'breeze': '../breeze.debug',
            'jquery': '../jquery-1.8.3.min',
            'ko':     '../knockout-2.2.0',
            'Q':      '../q'
        }
    });

    //  Launch the app
    //  Start by requiring the 3rd party libraries that Breeze should find
    define(['require', 'ko', 'jquery', 'logger', 'Q'], function (require, ko, $, logger) {
        logger.info('Breeze Todo is booting');

        // require the 'viewModel' shell 
        // require '../text' which is an html-loader require plugin; 
        //     see http://requirejs.org/docs/api.html#text
        require(['viewModel', '../text!view.html'],

        function (viewModel, viewHtml) {
            var $view = $(viewHtml);
            ko.applyBindings(viewModel, $view.get(0));
            $("#applicationHost").append($view);
        });
    });
})();

Notice how we use paths both to locate the libraries and get the module names established as Breeze expects them.

Notice also that we force requireJS to load these dependent 3rd party libraries before Breeze itself is loaded. That's really important. They have to be in the requireJS IoC container by the time Breeze starts looking for them; if they aren't there, Breeze assumes they aren't ever going to be there.

That's why you see Breeze treating your entity properties as if they were ES5 properties. The define in your call "context setup" loads 'ko' and 'breeze' at the same time. That means that there is no guarantee that 'ko' will be loaded when Breeze looks for it during its own initialization phase.

If 'ko' isn't loaded when Breeze looks for it, Breeze assumes you aren't using Knockout and falls back to its native model library ("backingStore") ... which builds entities as ES5 properties. That happens to be the right choice for Angular apps. It's not the right choice for KO apps.

Finally, if Durandal expects a different name for a module (I'll take your word for it), use the requireJS "map" configuration to define the synonym as in this example:

requirejs.config({
    paths: {
        'breeze': '../breeze.debug',
        'jquery': '../jquery-1.8.3.min',
        'ko':     '../knockout-2.2.0',
        'Q':      '../q'
    },
    map: {
        '*': { 'knockout': 'ko' }
    }
});

Now when Durandal requests 'knockout', requireJS will map it to the (already loaded) 'ko' module.

This "map" technique is in lieu of the "dummy module" approach that works just as well:

define('knockout', [ko], function (ko) { return ko; });

As you review the sample code, you may wonder when this app loads Breeze. The answer: when the viewModel is resolved. The viewModel has its own dependencies including the dataservice which itself depends on Breeze. Isn't dependency injection marvelous? :-)

Alternative

You may also be able to resolve the problem in a different fashion.

According to your question, you can get Breeze and Durandal up and running but the Breeze model library appears to be configured for Breeze's native "backingStore" which writes entity properties as ES5 getter/setter properties.

You can change that choice later in your startup process, perhaps in your dataservice or datacontext module where you first interact with Breeze and create an EntityManager.

Before you have that first Breeze interaction, call

breeze.config.initializeAdapterInstance("modelLibrary", "ko", true);

This establishes Knockout as the model library Breeze should use when creating/materializing entities. Henceforth, the entities will be created with KO observable properties.

It is critical that knockout is loaded into the requireJS IoC container and accessible as 'ko' before you make this configuration change or else Breeze will throw an exception.

Do not expect Breeze to wait until requireJS has loaded 'ko' asynchronously. The adapter initialization is a synchronous process. 'ko' must have been loaded before Breeze looks for it.

Durandal 2.0

I am informed that Durandal v2.0 changes somewhat the setup pattern that was familiar to me in Durandal v.1.x. I believe my answer remains germane.

I'm not yet well acquainted with Durandal v.2. I'm excited about it in part because it offers the possibility of using ES5 property getters/setters instead of the observability functions. I like that a ton!

The cost of this specific feature (which you do not have to use) is that you have to run in an ES5 compatible browser ... which means you can't run in the still popular IE8. There is no polyfill for ES5 properties.

Most ... but not all ... single page applications can operate within this limitation.

Unfortunately, according to Durandal's architect, in the current 2.0 version ES5 properties don't work with Breeze. The two libraries fight over those getters and setters. So you can use Durandal v2.0 with Breeze but you have to stay with the observable function properties for now.

We expect this story to improve by v.2.1

In sum

Hope these thoughts and variations set you on a successful path.

like image 189
Ward Avatar answered Apr 30 '23 04:04

Ward