RJS package: integrate require.js #11

Open
bemson opened this Issue Jan 19, 2013 · 5 comments

1 participant

@bemson
Owner

The "rjs" package would implement convenience tags and methods, for loading AMD modules with require.js. This issue list possible use-cases and implementations, of the RJS package.

The package may exist in a separate github repository.

@bemson
Owner

Use case: Define and access state dependencies

The primary use-case for integrating require.js functionality into Flow, is loading AMD modules for a specific program state or branch. Below demonstrates how this can be achieved using Flow's core functionality.

var myPage = new Flow({
  getApp: {
    _in: function () {
      var self = this;
      self.wait('//error', 10000);
      require(['path/to/moduleA'], function (dependencyA) {
        self.data('modA', dependencyA);
        self.go();
      });
    },
    _on: function () {
      var moduleA = this.data('modA');
      /* Do something with moduleA */
    },
    error: function () {
      console.log('Failed to load app!');
    }
  }
});

The above code does the following:

  1. Pause navigation (with possible timeout and fallback/error state)
  2. Load AMD modules a/synchronously via require.js.
  3. Expose loaded dependencies to other states.
  4. Resume navigating the flow.

The RJS package would seek to internalize this behavior, in order to reduce the glue code needed to depend on AMD modules.

@bemson
Owner

RJS _require

The RJS package may introduce a _require tag to Flow. The tag should provide the following functionality:

  1. Define dependencies for a state/program-branch.
  2. Configure the require.js loader, such as baseUrl, paths, etc.
  3. Provide a state to traverse when dependencies fail to load.

Short Form

Below demonstrates two variations of the tag's short form. The short form addresses the primary use-case for the tag; identifying dependencies. It is also meant to promote quick comprehension of a program, by making the reducing the amount of code needed to express functionality.

var myApp = new Flow({
  task1: {
    _require: ['moduleA', 'moduleB'],
    _on: function () {
      /* do something with the dependencies */
    }
  },
  task2: {
    _require: 'moduleC',
    _on: function () {
      /* use the dependencies */
    },
    task3: function () {
      /* use parent dependencies */
    }
  }
});

Long Form

The long form of the tag (an object literal), would support all the functionality listed above. It can be assumed that the object-literal would accept any configuration options used with require.js. Below demonstrates using the long form accross several states.

var myApp = new Flow({
  task1: {
    _require: {
      baseUrl: '/v1',
      deps: ['moduleA', 'moduleB']
    }
    _on: function () {
      /* do something with the dependencies */
    }
  },
  task2: {
    _require: {
      baseUrl: '/v2',
      deps: 'moduleC'
    },
    _on: function () {
      /* use the dependency */
    },
    task3: function () {
      /* use parent dependency */
    }
  }
});

Ignored configuration options

The only option that will be ignored by the _require tag is callback. This is primarily because it's execution does not apply to any navigation event, and breaks the idiom of defining logic within traversal callbacks of a state (i.e., "_in", "_out", etc.).

Also, a callback option is not needed, since Flow will resume navigating towards it's destination when all dependencies have loaded. As such, Flow's current destination (and the state's in between) become the callback for require.js loading.

Below demonstrates how the "_in" traversal function can be used to emulate a callback function, when require.js successfully loads dependencies.

var myApp = new Flow({
    task: {
      _require: 'lib.js',
      _in: function () {
        console.log('loaded dependency!');
      },
      _on: function () {
        /* do something with dependency */
      }
    }
});

Additional configuration options

The RJS package will add configuration options to the _require tag, in order to integrate require.js with Flow's approach to code execution.

configuration option: fallback

The "fallback" option takes a flow query, which is targeted by Flow when any dependency fails to load. Below demonstrates using the fallback option.

var myApp = new Flow({
  task: {
    _require: {
      deps: ['moduleA', 'moduleB'],
      fallback: '@parent/task_failed'
    },
    _on: function () {
      /* do something with dependencies */
    }
  },
  task_failed: function () {
    console.log('failed to load');
  }
});

The one problem is that the fallback option should allow targeting a descendant of the failed state/program-branch. But that presents a contradiction to the _require tag itself - it's supposed to deny access until dependencies have been met.

The result is a program that appears less categorical than desired, but it will have to be the case for now.

Managing multiple/nested configurations

At this time, require.js supports defining unique configurations of limited variety. As such, the _require tag would also serve as a custom configuration manager, allowing each state to define custom options.

Below demonstrates nested states, defining their own require.js configuration options.

var myApp = new Flow({
  task: {
    _require: {
      baseUrl: '/v2',
      urlArgs: 'bust=' + Math.random(),
      deps: 'lib.js'
    },
    _on: function () {
      /* do something with YUI2 */
    },
    subtask: {
      _require: {
        baseUrl: '/v3',
        deps: 'lib.js'
      },
      _on: function () {
        /* do something with YUI3 */
      }
    }
  }  
});

It is difficult to guess the expected behavior of nested configurations. At this time, the developer should assume that passing an object-literal defines a new/empty require.js configuration. Likewise, using the short-form would adopt the default (or nearest ancestor state's) require.js configuration.

More thought should go into this, but the overall goal is to avoid adding more options to the configuration object than are needed - since this object should be viewed as the require.js configuration options.

Accessing require.js dependencies

Speaking generally, a method or object would be add to Flow instances, in order to retrieve require.js dependencies. How the dependencies will be exposed to the state is unclear at this time.

@bemson
Owner

Use Case: Loading nested Flows

Given how require.js supports distributed architectures, it may be inferred that the _require tag will be used to execute nested Flows. Below demonstrates how this might be achieved:

main.js

var myPage = new Flow({
  task: {
    _data: 'nestedFlow',
    _in: function () {
      var self = this;
      self.wait();
      require(['/path/to/subflow.js'], function (subflow) {
        self.data('nestedFlow', subflow);
        self.go();
      });
    },
    _on: function () {
      var subflow = this.data('nestedFlow');
      subflow.target.apply(subflow, arguments);
    }
  }
});

subflow.js

define(function () {
  return new Flow(function () {
    console.log('subflow running');
  });
});

The above code does the following:

  1. Pause navigation (with possible timeout and fallback/error state)
  2. Load Flow instance a/synchronously via require.js.
  3. Expose the nested flow to the state.
  4. Resume navigating the flow.
  5. Execute nested Flow in context of the originating Flow (via the _on event handler).

The RJS package would seek to internalize this behavior, in order to reduce the glue code needed to load Flow instances via AMD.

@bemson
Owner

RJS _src

The RJS package may introduce a _src tag to Flow. The _src tag should provide the following functionality:

  1. Define an AMD dependency.
  2. Initialize and/or invoke the resolved dependency as a nested Flow.
  3. Direct the nested Flow to it's initial/program state.
  4. Pass along arguments from the "parent" Flow.

Below demonstrates how the _src tag allows a program-branch to come from an externalized file. In this example, the content of the lead state would be the object returned by the anonymous module.

greet.js

var greet = new Flow({
  _sequence: true,
  hello: {
    _in: function () {
      console.log('hello');
    },
    world: {
      _src: 'path/to/world.js'
    }
  }
});

world.js

define({
  _on: {
    this.go('is/here');
  },
  is: {
    here: function (name) {
      console.log(name || 'world!');
    }
  }
});

We could then use the greet Flow instance, with:

greet.target('//hello/world', 'Joey');

// hello
// Joey

Source-able external files

The _src tag would only work with "source-able" external files. Such files would meet the following requirements:

  • Be an anonymous AMD module.
  • Define an object-literal, function, or Flow instance

Note: In a AMD environment, the author would need to be mindful of unique Flow constructors, in which case the consuming Flow may not recognize unknown instances. As a best practice, external files should define generic objects, instead of instances.

Desired Implementation

The above may be implemented as is, but there is a glaring desire to use the _src tag as a means of breaking up a program into files. This includes targeting states of a Flow that have yet to be loaded.

Given that Flow operates on a compiled/known state graph, there are many technical challenges involved with targeting states outside of that graph. It may altogether be impossible. Below demonstrates how one might target unknown states of a nested flow - permitting a kind of deep linking into an app.

var myApp = new Flow({
  main: {
    _src: 'path/to/primary/app.js'
  }
});

myApp.target('//main/app/login/screen');

In order to target unknown, a map of the nested flow would either need to be known, or somehow passing through the remainder of a query. As well, the nested flow may have nested flows, and/or a dynamic query would need to be able to target states beyond a nested flow's root.

Altogether, the endeavor sounds flawed, though the premise is attractive.

@bemson bemson was assigned Jan 25, 2013
@bemson
Owner

This package seems useful enough to be included with the 0.5.0 release of Flow (see #42)

@bemson bemson added backlog and removed Figuring Out labels Feb 12, 2014
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment