Coder Social home page Coder Social logo

jsutilslib / activeobject Goto Github PK

View Code? Open in Web Editor NEW
0.0 1.0 0.0 50 KB

ActiveObject is a library that converts an object into an active object that can emit events when one of its properties changes its value.

License: Apache License 2.0

JavaScript 85.12% Makefile 1.49% HTML 13.39%
javascript objects active callbacks events javascript-events

activeobject's Introduction

Active Object

ActiveObject is a library that converts an object into an active object that can emit events when one of its properties changes its value.

The use case is better explained with an example. Let's have a var named people that contains a list of people (e.g. Bob and Alice), and we convert that variable into a_people, which is an active object:

let people = [ { name: "Alice" }, { name: "Bob" } ]
let a_people = ActiveObject(people)

For the purpose of the library, from now on we have to use the new variable a_people instead of the original one. But it is possible to watch for changes in that active object (e.g. show a text in the console):

a_people.watch('', function (e) {
    console.log('the list of people has changed', e)
})

And now, if we change the name of one of the persons in the list, our function will be triggered

$ a_people[1].name = 'John'
list of people has changed
{event: {}, variable: 'name', fqvn: '1.name', value: 'John', stopPropagation: ƒ}
    event:
        cancelled: false
        from: "1.name"
        target: Proxy {name: 'John'}
        type: "change"
        [[Prototype]]: Object
    fqvn: "1.name"
    stopPropagation: ƒ ()
    value: "John"
    variable: "name"
    [[Prototype]]: Object

ActiveObject can be used as a standalone library in your web applications, but it is also part of jsutilslib, which is a library that consists of a set of curated components, utility functions, clases, etc. that are flexible enough to be re-used in different javascript applications.

Why Active Objects

Web applications are asynchronous and we have different methods to deal with asynchrony: event subscription, promises, callbacks, etc. ActiveObject pretends to be yet another method to deal with asynchrony, placing the focus in variables and objects instead of events.

An example is to retrieve a set of data from the internet (e.g. using fetch library), and when a variable finally contains the retrieved and processed data, react doing things.

In the next example, we retrieve a list of trivia questions from the open trivia database. Once they are retrieved, these questions are stored in an active object. The active object is watched for changes, and whenever the field questions changes, a function is triggered (that function renders the questions elsewhere):

let trivia = jsutilslib.ActiveObject();
trivia.watch('questions', function(e) {
    clear_questions();
    for (let i in trivia.questions) {
        render_question(trivia.questions[i]);
    }
}, true)
fetch("https://opentdb.com/api.php?amount=25&difficulty=easy&type=multiple").then(function(response) {
    response.json().then(function(data) {
        if (data.response_code == 0) {
            trivia.questions = data.results;
        }
    })
});

This simple example could be implemented by other means (e.g. using other then function that renders). But ActiveObject library enables to do it in this way.

Installation

From a CDN

The preferred method to use ActiveObject is to get it from a CDN:

<script src="https://cdn.jsdelivr.net/gh/jsutilslib/[email protected]/common.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/jsutilslib/[email protected]/activeobject.min.js"></script>

Library jsutilslib/common is a prerrequisite for this library.

  • Please consider using the whole library jsutils.

From source

There are a set of javascript files that contain a part of the library, each one (in folder js). These files can be used individually or combined into a single one, by concatenating them (or by using uglify-js).

A Makefile is provided to create the single all-in-one js files for the library.

# npm install -g uglify-js
...
# git clone https://github.com/jsutilslib/common
# cd common
# make
uglifyjs js/*.js  -b | cat notice - > common.js
uglifyjs js/*.js  | cat notice.min - > common.min.js
# git clone https://github.com/jsutilslib/activeobject
# cd activeobject
# make
uglifyjs js/*.js  -b | cat notice - > activeobject.js
uglifyjs js/*.js  | cat notice.min - > activeobject.min.js

Now you can use files common.min.js and activeobject.min.js in your project:

<script src="common.min.js"></script>
<script src="activeobject.min.js"></script>

Library jsutilslib/common is a prerrequisite for this library.

Working with ActiveObject

When importing the library, a default sink for objects is automatically created. It is window.$watched.

It is advisable to use the default $watched object in most of cases, but it is also possible to create your own ActiveObject to define its particular behavior.

Let's walk on an example:

  1. Using function watch from any ActiveObject, we will watch for changes on its properties. And if a change happens, the provided function will be triggered. (in the example, when variable $watched.person changes, the function welcome will be executed).
function welcome(e) {
    console.log("one new person has arrived");
}
$watched.watch('person', welcome);

Have in mind that $watched.person does not yet exist, but we are watching for its changes.

Now we create the property:

$watched.person = [];

If inspecting the console, we'll see that the event has been triggered.

In this moment we are creating two person: Bob and Alice.

class Person {
    constructor(name) {
        this.name = name;
        this.friends = [];
    }
}
let Bob = new Person("Bob");
let Alice = new Person("Alice");

Now we add Bob to the list of person:

$watched.person.push(Bob)

Surprisingly the event is not triggered, but this is because variable $watched.person still is a list; it has changed its content.

If we wanted to watch the content of $watched.person we should subscribe for changes on person.? or person.*, depending on what we wanted.

Char ? matches one single property and * matches any amount of properties in the full qualified variable name.

$watched.watch('person.?', function(e) {
    let person = this[e.variable];
    console.log(`hi ${person.name}`);
})

Now we add Alice to the list of person:

$watched.person.push(Alice)

Now the event is triggered and we can check the console to see the welcoming text to Alice.

But we can also subscribe to properties of properties, as in the next example

$watched.watch('person.?.friends.?', function(e) {
    let person = this[e.variable];
    console.log(`a new friendship with ${person.name}`);
})

Now if we add Alice as a friend of Bob, we'll see that the function is properly triggered:

$watched.person[0].friends.push(Alice)

The final situation it

JSON.stringify($watched.person)
'[{"name":"Bob","friends":[{"name":"Alice","friends":[]}]},{"name":"Alice","friends":[]}]'

Options

The prototype for the function is:

function ActiveObject(original = {}, options = {})

Where original is the original object to make active, and options configure the way that the active object will behave. The default values are the next:

options = {
    // The depth of the properties that can be watched
    propertiesdepth: -1,
    // Whether to clone objects prior to watch the object (refer both to the original one and the values that are set to the properties)
    cloneobjects: false,
    // If propagatechanges is true, the events of changes of a variable are propagated to its parent objects
    propagatechanges: false,
};

propertiesdepth

One object is built by its properties, but these properties may also be objects that have their own properties and so on. ActiveObject enables to watch changes in the properties, and that behavior is controlled by option propertiesdepth.

Being a is an ActiveObject, we can set value of a.b.c.d to value "Test". If we set propertiesdepth to watch for changes at any depth, such change of a.b.c.d to "Test" will trigger an event. But it is also possible to not to watch for in-depth properties by changing the value of propertiesdepth:

  • -1: means that we want to enable watching for changes at any depth of properties.
  • 0: means that we do not want to watch for changes at any depth of properties. We are only interested on changes on root properties.
  • any other value: sets the maximum depth to enable watching for changes (e.g. depth 2 means that in a.b.c.d, changes to a.b.c will trigger events, but a.b.c.d no).

cloneobjects

The original object and the values that are assigned to the properties of the ActiveObject may be objects. In javascript, any object is passed by reference, and ActiveObject deals with it by transforming the objects and its properties into ActiveObjects.

As the objects are references, if ob1 = { a:1, b: 2}; $watched.ob1 = ob1;, if we set $watched.ob1.a = 3, it will be reflected in ob1: JSON.stringify(ob1) is {"a":3,"b":2}. Reciprocally, if we set ob1.b = 4, JSON.stringify($watched.ob1) is {"a":3,"b":4}.

If we do not want such behavior, we can use jsutilslib.clone function to obtain a deep clone from the object to use in either case. But it is also possible to initialize the object using cloneobjects to true. In such case, any object in the tree will be cloned before using it.

This behavior is all-or-none. If you want to change it for each property, it is better to use jsutilslib.clone when needed.

propagatechanges

When setting the value of a in-depth property of the object (e.g. a.b.c.d = 1), it is obvious that a.b.c.d has changed, and the events will be triggered.

But one can consider that a.b.c has also changed, and a.b too, and so a. This is called propagation of changes.

An ActiveObject may want to propagate or not the events of changes in the leaves to the root object, and that is controlled by setting propagatechanges. If set to true the changes will be propagated to the root.

Properties and Functions

ActiveObject is not a class, but a function that returns a Javascript Proxy, that contains additional functions:

  • is_proxy: is a property whose value is true.
  • watcher: is a property used to obtain the watch controller object, which is used for internal purposes (you should know that it is here, but it is better to not to deal with it).
  • watch: is the function used to subscribe to changes in properties.
  • unwatch: is the function used to subscribe to changes in properties.
  • value: returns the plain object that is being controlled by the ActiveObject (it will keep the types and classes of each property)
  • settings: returns a copy of the settings for the object (if the copy is modified, it does not affect to the effective settings of the object)
  • reconfigure: enables the reconfiguration of an active object (and its children properties)

watch

Function watch is used to subscribe to changes in the properties of an ActiveObject

function watch(varname, eventHandler, autocancel = false)
  • varname is the Fully Qualified Variable Name to which is wanted to subscribe for changes. That means that (starting from the properties of the object to which is requested the subscription), the varname may contain properies and properties of properties.

    in

    $watched.a = { b: { c: { d: {} }} }

    we can subscribe for changes as $watched.a.watch("b.c.d", function() ...), but also to $watched.a.watch("b", function() ...), depending on our insterests.

    There are special values for matching the FQVN: ? will match a single property, while * will match any sequence of properties. e.g. b.c.d, b.?.?, b.* will match b.c.d, and so any changes to b.c.d will trigger events for the three expressions.

  • eventHandler is the function called when a change is detected, and the prototype of the function is the next:

    function eventHandler(e)

    Where e is an object of type ActiveObject event

    event = {
        event: {
            target: originalproxy,  // The real target is the proxy object that triggered the event
            type: "change",         // The type of the event is "change"
            from: var_fqn,          // The variable that has triggered the event
            cancelled: false        // Whether the event has been cancelled or not
        },
        variable: var_name,             // The name of the variable in the object that receives the event
        fqvn: var_fqn,                  // The full qualified name of the variable that receives the event
        value: proxy[var_name],         // New value
        stopPropagation: function() {   // Function to stop propagation (if activated)
            e.cancelled = true;
        }
    }
  • autocancel: if set to true, if any watch rule matches the modified variable, the event is triggered but it is also autocancelled after the execution of the callback. So any other watch matching is prevented and so the event propagation (if activated).

unwatch

Function unwatch is used to stop receiving subscriptions about the changes of an ActiveObject.

function unwatch(varname, eventHandler = null)
  • varname is the string used as the varname when calling function watch. No other type of variable substitution or expansion is made. If does not exist, it does nothing.

  • eventHandler is the event handler that is wanted to remove. If set to null, any event handler for rule varname is removed.

value

Is the function used to retrieve the plain object from an ActiveObject. Although an ActiveObject can be used intechangeable with the original one (e.g. for serializing), the original object can be retrieved using this function.

settings

This is a readonly property that returns a copy of the actual settings of the ActiveObject. Updating this copy have no effect in the actual settings of the object. For that purpose, use function reconfigure.

reconfigure

It is a function that enables to reconfigure an object (and its children properties, if wanted)

function reconfigure(options, reconfigurechildren = true)

The reconfiguration will take effect from the call of the function on. It is important that the actions made before calling that option will not be changed. e.g. if the object had setting cloneobjects set to false, even reconfiguring it to true will not make that not cloned objects will be cloned; instead, the new objects used for the properties will be cloned.

Use case

Let's have the next use case (to follow the use case, we can use the console of chrome or any chromium derived browser)

> $watched.a = { b: { c: { d: {} }} }
> $watched.watch('a', function(e) { console.log(`changes`, e); })

The default configuration of $watched is to not to propagate events. So if we make changes in the children properties

> $watched.a.b.c.d.e = "a new value";

The watch that we have defined will not be propagated. If we want to change the behavior, we could reconfigure the object:

> $watched.a.reconfigure({propagatechanges: true});

And now if we make a similar change, the watch will be triggered

> $watched.a.b.c.d.f = "other new value"
...
changes {event: {}, variable: 'a', fqvn: 'a', value: Proxy, stopPropagation: ƒ}

If properties are also watched, we could intercept the event at a deeper stage, and stop its propagation using the event's stopPropagation function:

> $watched.watch('a.b.c', function(e) { console.log(`intercepted changes`, e); e.stopPropagation(); })

Now if introduced changes, the event will be intercepted in the new watch but not in the upper, but if changes are made outside the scope of this subscription, they will arrive to the root:

> $watched.a.b.c.d.g = "yet another new value"
intercepted changes {event: {}, variable: 'c', fqvn: 'a.b.c', value: Proxy, stopPropagation: ƒ}
> $watched.a._b = "a new property"
changes {event: {}, variable: 'a', fqvn: 'a', value: Proxy, stopPropagation: ƒ}

The watches can also be removed using unwatch method, and the other watches will continue working

> $watched.unwatch('a.b.c')
> $watched.a.b.c.d.h = "the last change"
changes {event: {}, variable: 'a', fqvn: 'a', value: Proxy, stopPropagation: ƒ}

After all these changes, our object will be the next

> JSON.stringify($watched.a)
'{"b":{"c":{"d":{"e":"a new value","f":"yet another new value","h":"the last change"}}},"_b":"a new property"}'

activeobject's People

Contributors

dealfonso avatar

Watchers

 avatar

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.