Code Monkey home page Code Monkey logo

knockout.merge's Introduction

Knockout.Merge

A simple addition to to the ko namespace to allow merging data into an existing object.

I was finding working with models which exposed bindings in a nested nature would not cope well with dynamic json data. It was either use the mapping plugin to generate the model which removed the ability to use computed observables without attaching them to the instance or manually writing the mapping logic to map data against a given model.

So this simple plugin was designed to take the effort out of working with external data sources which use some form of model as a contract. It will auto merge any data into the bindings without destroying or replacing any of the underlying objects functions. It works fine with nested objects and complex arrays, just make sure that the names of the json keys match the binding names.

Since version 1.5.0 it also works with knockout-es5 module so you can marge in and it will work on the observable behind the scenes so all your existing functionality (custom rules/methods/constructors) will work with them, although its all optional, if you dont use knockout-es5 then it will just operate as normal.

This can be used in nodejs by using npm install knockout.merge, then just require it after knockout and it will extend the object internally.

BREAKING CHANGE SINCE VERSION 1.4.0

Since version 1.4.0 the dependency on knockout.mapping has been completely removed as it relies upon no functionality from the library it was just in there to make it more familiar to developers. Now it has been moved to its own property upon the knockout object, so now it should be:

ko.merge.fromJS(model, data);

Also the name of the file has been changed to reflect this to now be knockout.merge.js.

BREAKING CHANGE SINCE VERSION 1.3.0

Since version 1.3.0 to improve requirejs and module loading capabilities the merge logic is no longer merged into the knockout.mapping namespace it is now in its own namespace knockout.mapping.merge. This is mainly done because in certain situations you would load knockout.mapping.merge as a module via a resource loader you may have all your objects isolated i.e:

var ko = require("knockout");
ko.mapping = require("knockout.mapping");
ko.mapping = require("knockout.mapping.merge"); // wont work as you overwrite the mapping var

So instead we moved all logic into its own namespace so it should now be:

var ko = require("knockout");
ko.mapping = require("knockout.mapping");
ko.mapping.merge = require("knockout.mapping.merge"); // will work as you are not merging functionality into ko.mapping

Anyway that aside there are now some changes to the method names as it makes no sense to have ko.mapping.merge.mergeFromJs, so it has now been changed to ko.mapping.merge.fromJS and ko.mapping.merge.rules (for your custom rules).

The knockout observable extensions remain unchanged so you wont need to worry about changing any of that stuff.

The examples are all updated to reflect this so check out the source code if you are unsure what I mean.

Finally there is an update to the d.ts file for typescript, which should be compatible with DefinitelyTyped stuff, so just whack it in a folder and feel free to change the references to whatever you want them to be.

Example

A simple example of merging some Json data into an existing model:

function User() {
    var self = this;
    
    self.Firstname = ko.observable();
    self.Surname = ko.observable();
    
    self.Fullname = ko.computed(function() {
        return self.Firstname() + " " + self.Surname();
    });
}

var someJson = {
	Firstname: "James",
	Surname: "Bond"
};

var someUser = new User();
ko.merge.fromJS(someUser, someJson);

// Will output "James Bond"
alert(someUser.Fullname());

This should also solve the problem when using Knockout Validation, as the model remains intact so you can map from json or complex objects without worrying about losing your bindings:

function User() {
    var self = this;
    
	self.Age = ko.observable().extend({ digits: true });
    self.Firstname = ko.observable().extend({ maxLength: 20 });
    self.Surname = ko.observable().extend({ maxLength: 20 });
    
    self.Fullname = ko.computed(function() {
        return self.Firstname() + " " + self.Surname();
    });
}

var someJson = {
	Firstname: "James",
	Surname: "Bond",
	Age: 40
};

var someUser = new User();
ko.merge.fromJS(someUser, someJson);

There is also an option to infer the types for observable arrays so you can just auto-merge array elements into your observables, however this functionality will not be able to distinguish between existing elements and new elements so this will currently just append elements to the array not merge them.

You can do this like this:

function SomeChildModel()
{
   this.Name = ko.observable();
}

function SomeModel()
{
   this.someArray = ko.observableArray().withMergeConstructor(SomeChildModel);
}

There has been some new additions to allow you to write custom merging logic, this is useful for certain situations such as mapping date strings to date objects, or do anything more complicated than a simple data replacement. You can either do this by embedding your own method into the merging logic such as:

function SomeModel()
{
	this.Date = ko.observable(new Date()).withMergeMethod(function(knockoutElement, dataElement, options) {
		knockoutElement(new Date(dataElement));
	});
}

Personally I am not a massive fan of this as you will rarely want to embed your mapping logic into your pojo models, so if you want to use more of an AOP style approach and have your logic elsewhere then use the merging rules style settings:

// This can go anywhere, just make sure you include the required libs first 
ko.merge.mergeRules["Date"] = function(knockoutElement, dataElement, options) {
	knockoutElement(new Date(dataElement));
};

function SomeModel()
{
	this.Date = ko.observable(new Date()).withMergeRule("Date");
}

Global Handlers

In version 1.5.1 you can also use global handlers, which are provided the current elements to see if it can deal with them, in most cases you will not need to use these. However lets imagine you have Date objects in your models and you do not want to have to constantly add .withMergeRule("my-date-rule") you can make a handler to check if it is a date object, and if so do something useful with it. Handlers should return a true if they have handled the data or a false if they are not.

A global handler should be a function taking the knockout element and data element and returning a bool as mentioned above, here is a simple example of one:

var globalDateHandler = function(knockoutElement, dataElement, options) {
    if(knockoutElement() instanceof Date) {
        knockoutElement(new Date(dataElement));
        return true; // We handled it so no need to check with other handling mechanisms
    }
    return false; // It is not a date type, so delegate to normal handlers
}

ko.merge.globalHandlers.push(globalDateHandler);

This is for niche scenarios where you do want to do system wide stuff and remember these global handlers will be iterated over for EVERY entry in the data model, so if you are doing some resource intensive stuff in there expect some slowdown. However in most cases "Fast is fast enough" so I wouldn't worry, and if there are no handlers then the normal merging predicates are used.

You can omit the options, and they are for you to be able to write your own options for passing into rules, however there is one rule the native merge uses mergeMissingAsObservables: true | false, if it is true then if you were to have an empty json containing object, it would end up creating it with observable fields not primitive ones.

Finally there is also a typescript descriptor file available in the source folder to give you compile time safety.

Here is an example of what it does and how to use it, but you will need to check out the source code. View Example

Here are the tests which you can run in your own browser: [View Tests] (https://rawgit.com/grofit/knockout.merge/master/tests/test-runner.html)

knockout.merge's People

Contributors

atpyatt avatar grofit avatar ipwright83 avatar totaldis avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

knockout.merge's Issues

Add a License

Could you please add a license to the GitHub rep? Preferably MIT as that would then match the Knockout license.

Properties with Null Value

I have found that properties with a null value are being treated in the fromJS function in the same way as properties that have a value which is an object that needs to be recursed through. This is because koModel[parameter]=null satisfies the condition below:

else if (typeof (koModel[parameter]) == "object" &&
!(koModel[parameter] instanceof Date) &&
!isArray(koModel[parameter])) {
exports.fromJS(.....
}

Changing the condition to the following solved the problem for me:

else if (koModel[parameter] !== null && typeof (koModel[parameter]) == "object" &&
!(koModel[parameter] instanceof Date) &&
!isArray(koModel[parameter])) {
exports.fromJS(.....
}

Initialize constructor with data

Hi,
maybe i'm missing if there is a way to initialize an observable array with data, at least for performance improvement. Actually, i'm doing that with a patch like this one below

if (knockoutElement.mergeConstructor.length === 1) {
  var arrayElement = new knockoutElement.mergeConstructor(element);
  knockoutElement.push(arrayElement);
}

But i'm asking if there would be a cleaner method...
THX in advance, keep up this great work!

Ability to create missing observables on a merge

If you take an object like:

{ 
   title: ko.observable("A")
}

and merge it with an object like:

{
   title: "A",
   value: "a"
}

Then the newly created value field won't be observable. It would be great if there was a way to add additional items as observables.

Would you be open to having a flag to do this?

Complex Merge doesn't seem to work correctly

I'm trying to write a 'complex' merge but it seems to be failing. Essentially my sub-properties are being overwritten with non-observable fields, when they should remain observable. I'm not sure why this is the case. You can see a JSFfiddle to demonstrate this (I found the issue when computed observables that I've taken out for simplicity couldn't read from sibling computed fields) http://jsfiddle.net/IPWright83/fnc0xysk/2/

So I've started by defining some data which I create a view model from using ko.mapping.fromJS:

var data = {
    name: "Sign-off",
    id: "XX",
    values: [{ text: "Signed-off", count: 150, color: "#5fb5cc" }, 
             { text: "Submitted", count: 90, color: "#75d181" }, 
             { text: "Not Submitted", count: 75, color: "#f8a25b" }
            ],
    aggregates: {
        count: 650
    }
};
var vm = ko.mapping.fromJS(data);

If I log the vm, then vm.values[0] and finally vm.values[0].text I get the following. Ending up with an observable field as I expected:

image

Once I've done this I wait for a second and attempt to update the data using ko.merge.fromJS and this is where I start getting the problem:

ko.merge.fromJS(vm, {
    values: [{ text: "Signed-off", count: 90, color: "#5fb5cc" }, 
             { text: "Submitted", count: 40, color: "#75d181" }, 
             { text: "Not Submitted", count: 35, color: "#f8a25b" }
            ],
    aggregates: {
        count: 250
    }
});

Before I started I tested it on one of the top level fields, which seemed to work fine. But if I churn out the same information to the console you can now see that the text field on the first value is no longer observable :(

image

More of question than issue: callbacks

So I only kind-of know what I am doing. I believe I am running into issues with the merge not completing (consistently) before other functions are running. If this was my own function I would add in a callback to be returned when everything has completed. I poked around and tried a few things but am having trouble. Your code is a bit more complex than I am used to following. Any suggestions?

(By the way - this thing is awesome and solved a huge problem I was having - thanks!)

Uncaught Error: Pass a function that returns the value of the ko.computed

I'm using knockout-3.1.0 and getting an error trying to use mergeFromJS for the first time. Here's what I'm currently doing:

Creating a basic object

  var settings = {
        mode: "standalone",  // mode can be standalone or connected
        backgroundColour: {
            override: false,
            value: "#000000",
            overrideValue: "#000000"
        },
        colour: {
            override: false,
            value: "#FFFFFF",
            overrideValue: "#FFFFFF"
        },
        fontSize: {
            override: true,
            overrideValue: "3"
        }
}

Adding some computed fields:

 // Determine the background colour
    retVal.backgroundColour.result = ko.computed({
        read: function () {
            if (retVal.backgroundColour.override()) {
                return retVal.backgroundColour.overrideValue();
            } else {
                return retVal.backgroundColour.value();
            }
        },
        write: function (val) {
            if (retVal.backgroundColour.override()) {
                retVal.backgroundColour.overrideValue(val);
            }
        }
    });

    // Determine the foreground colour
    retVal.colour.result = ko.computed({
        read: function () {
            if (retVal.colour.override()) {
                return retVal.colour.overrideValue();
            } else {
                return retVal.colour.value();
            }
        },
        write: function (val) {
            if (retVal.colour.override()) {
                retVal.colour.overrideValue(val);
            }
        }
    });

Loading some settings from local browser storage and trying to merge them:

   retVal.updateSettings = function (toUpdate) {
        ko.mapping.mergeFromJS(retVal, toUpdate);
    }

    /** Load the settings from the setting store */
    retVal.loadSettings = function () {

        // Only update if the store is enabled
        if (store.enabled) {

            console.log("client settings loaded from store");

            // Read the value from the store and 
            // merge them in using knockout.mapping.merge
            var toLoad = store.get("settings");
            retVal.updateSettings(this, toLoad);
        }
    };

At this point I get the error message detailed within the title. Here's a snapshot of the callstack:

image

Array merging is failing for simple types

Not 100% sure why yet but I've just discovered that the array merging isn't working properly for simple objects (e.g. an array of numbers).

Essentially given the following, the resultant viewModel.values()[0] will still equal 0.

var viewModel = { values: ko.observableArray([ 0 ]); }
var data = { values: [50] };
ko.merge.fromJS(viewModel, data);

I'm looking into this at the moment, if I find a resolution I'll submit a pull request.

Add the ability to infer types for sub objects

Currently the merging is very simple and will work for complex objects assuming there is no sub objects, so for example if you were to have a class called Company and it contained an observable array of Employees, you could not really map the child employees to the company.

So it would be good if there were some sort of system for handling this, be it an extension like with knockout validation.

function SomeContainerObject()
{
   this.SomeChildren = ko.observableArray().typedAs(new SomeChildObject);
}

This way when it goes to merge child objects it would be able to create the new child object then assign the values to it. Which would help massively for large object graphs where you need child objects with their own validation etc.

Merge of Arrays

Hey guys. I was using the library by trying to merge an object with many observable array properties.

The issue is that the merge actually does an array merge. I was expecting that this library could be used to update an observable with new data.

So, the new array would replace the old one. Version 1.5.2.

I am sure it worked in a previous version (maybe 1.4.2?). Its possibly related to the new merge algorithm for arrays, maybe this:
6299522

Can't get working with RequireJS

I can't seem to get this working with require JS. Wondering if this is a library problem or I'm doing something wrong?

Uncaught TypeError: Cannot read property 'mapping' of undefined knockout.mapping.merge.js:34
(anonymous function) knockout.mapping.merge.js:34
(anonymous function)

I'm using this for my configuration

requirejs.config({
    urlArgs: 'v=1.0.1',
    baseUrl: '/Scripts',
    paths: {
        'lib': '/lib',
        'app': '../app',
        'jquery': [
            '//code.jquery.com/jquery-1.11.0.min',
            'jquery-1.11.0.min'
        ],
        'jquery-ui': [
            '//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min',
            'jquery-ui-1.10.4.min'
        ],
        'bootstrap': [
            '//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min',
            'bootstrap.min'
        ],
        'signalR': [
            '//ajax.aspnetcdn.com/ajax/signalr/jquery.signalr-2.0.2.min',
            'jquery.signalR-2.0.2.min'
        ],
        'knockout': [
         '//cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min',
         'knockout-3.1.0'
        ],
        'knockout.mapping': [
            '//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min',
            'knockout.mapping-latest'
        ],
        'toggles': [
           'lib/toggles-2.0.5.min'
        ],
        'knockout.mapping.merge': [
            'lib/knockout.mapping.merge.min'
        ],
    },


    // define global dependencies
    deps: ['jquery', 'knockout', 'knockout.mapping', 'bootstrap'],

    // define callback function
    callback: function ($, ko, mapping) {
        ko.mapping = mapping;
    }
});

Then my include

require(['jquery', 'knockout', 'knockout.mapping'], function ($, ko, mapping) {
    ko.mapping = mapping;

    require(['knockout.mapping.merge'], function () {
        var v = ko.mapping.fromJS({ a: "B" });
        alert(v.a());
    });
});

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.