Git Product home page Git Product logo

uniflow-polymer's Introduction

UniFlow for Polymer 2.x

Set of mixins to enable uni-directional data flow in Polymer application.

Important!

This library was developed as part of internal project at Google and isn't directly affiliated with the Polymer project (although Polymer team has provided some good feedback on UniFlow implementation).

History & Overview

When you start working on a new Polymer application, it's easy to start and build the first few elements, and make them communicate via events and data binding, so everything looks nice and rosy. However, as the number and complexity of elements grows, it becomes increasingly difficult to manage relationships between them, trace where/when the data changes happened, and debug the problems. So this project started as an attempt by our team at Google to find a good way to architect large Polymer application.

Inspired by React's community Flux (and, later, Redux) architecture, we implemented a unidirectional data flow pattern (data down, events up) for Polymer. We found that when using UniFlow application code becomes more streamlined (e.g. it is clear what the responsibilities of each element are) and much easier to manage; the code has fewer bugs, and debugging is a lot more efficient. Adding new functionality no longer exponentially increases complexity.

This project was also inspired by Backbone Marionette. Backbone.js back in the days of its glory was a great library that provided a nice set of building blocks for building JavaScript applications. However, it left much of the application design, architecture and scalability to the developer, including memory management, view management, and more. Marionette brought an application architecture to Backbone, along with built in view management and memory management. It was designed to be a lightweight and flexible library of tools that sits on top of Backbone, providing the framework for building a scalable application. Uniflow strives to achieve similar goal for Polymer.

We feel that Polymer, and web components in general, is a great concept that takes interoperability and encapsulation in Web development to the next level. But it lacked the patterns for building large and complex applications, and this is the void we expect UniFlow to fill. It is still in beta, so breaking changes may be happening before the first release. However, we believe that abstractions implemented in the library can be useful for Polymer community, so we encourage people to try, fork, ask questions, send comments, and submit pull requests.

Applicability

This library implements the architectural pattern called 'unidirectional data flow'. It works best if application logic involves complicated data management, when multiple elements need to have access to or modify the same data. Even though the pattern can be implemented just using built-in Polymer concepts, such as custom events and data binding, the UniFlow library provides a useful set of tools and abstractions, and helps to structure application code.

Implementation

UniFlow is implemented as a set of mixins that developers apply to their elements. It is assumed that each application has a singleton application element that maintains state of entire application. Each element that needs access to the data is bound, directly or indirectly, to sub-tree of application state tree. Two way data binding is never used to send data up, from child to parent, so only parent elements send data to children using one way data binding. Child elements, in turn, send the events (emit actions) responding to user actions, indicating that the data may need to be modified. Special non-visual elements called action dispatchers mutate the data, then all elements listening to the data changes render new data.

API Documentation

Action Dispatcher

Use UniFlow.ActionDispatcher for non-visual elements that process actions emitted by visual elements. Action dispatchers usually placed at the application level. Each action dispatcher element gets a chance to process the action in the order the elements are present in the DOM tree. It is important that action dispatcher elements get two-way data binding to application state as follows:

Action dispatcher elements can include nested action dispatchers, so you can have a hierarchical organization of action dispatchers.

Example:

HTML:

<dom-module id="parent-dispatcher">
<template>
  <child-dispatcher-a state="{{state}}"></child-dispatcher-a>
  <child-dispatcher-b state="{{state}}"></child-dispatcher-b>
</template>
</dom-module>

JavaScript:

class ParentDispatcher extends UniFlow.ActionDispatcher(Polymer.Element) {

  static get is() { return 'parent-dispatcher'; }
  
  MY_ACTION(detail) {
   // do MY_ACTION processing here
   // return false if you want to prevent other action dispatchers from
   // further processing of this action
  };
}

customElements.define(ParentDispatcher.is, ParentDispatcher);

Action Emitter

Whenever element needs to emit an action, this mixin should be used. Action object must always include type property.

Application State

Assign this mixin to your main application element. It provides global state and functionality to maintain individual elements states. This mixin is responsible for notifying all state-aware elements about their state changes (provided those elements have statePath property defined). Only one element in the application is supposed to have this mixin.

Example:

HTML:

<template>
  <!-- action dispatchers in the order of action processing -->
  <action-dispatcher-a state="{{state}}"></action-dispatcher-a>
  <action-dispatcher-b state="{{state}}"></action-dispatcher-b>
  
  <!-- state-aware elements -->
  <some-element state-path="state.someElement"></some-element>
</template>

JavaScript:

class MyApp extends UniFlow.ApplicationState(Polymer.Element) {

  static get is() { return 'my-app'; }

  connectedCallback() {
    super.connectedCallback();
    this.state = {
      someElement: {}
    }
  }
}

customElements.define(MayApp.is, MyApp);

In the example above, <some-element> will receive notification of any changes to the state, as if it was declared as follows:

<some-element state="[[state]]"></some-element>

Also, if <some-element> has propertyA, on element attach this property will be assigned the value of state.someElement.propertyA, and receive all notification of the property change whenever the corresponding data in state tree changes. This essentially translates to following declaration:

<some-element state="[[state]]"
              propertyA="[[state.someElement.propertyA]]">
</some-element>

Note that data binding is one-way in both cases. Although state-aware elements can modify their own state, it is considered their private state and no other elements will be notified of those changes.

List View

This mixin used by elements that need to render multiple models backed by 'list' array. You may want to use ModelView to render individual models in the list. The mixin supports element selection by setting predefined $selected property on list elements.

Example:

HTML:

<ul>
  <template id="list-template" is="dom-repeat" items="[[list]]">
    <li id="[[item.id]]">
      <paper-checkbox checked="{{item.$selected}}">
      <model-view state-path="[[statePath]].list.#[[index]]"></model-view>
    </li>
  </template>
</ul>
Selected: [[selectedCount]] items
<paper-button on-tap="onDeleteTap">Delete</paper-button>

JavaScript:

class ListElement extends Polymer.GestureEventListeners(UniFlow.ListView(UniFlow.StateAware(Polymer.Element))) {

  static get is() { return "list-element"; }

  onDeleteTap() {
    this.deleteSelected();
  }

}

customElements.define(ListElement.is, ListElement);

In the example above list view element is also state-aware, meaning it has its own place in the application state tree. Assuming it has been declared as follows:

<list-element state-path="state.listElement"></list-element>

it will be rendering state.listElement.list and observing changes to it. Each model-view within dom-repeat template will have state-path property set to state.listElement.list.#<index> where index is the element's index in the array.

Model View

Element rendering data represented by a single object (model) in the application state should use ModelView mixin. Model View is a powerful concept that encapsulates model data (likely the data received from the server and to be persisted to the server if modified as a result of user actions), status (validity of the data, flag that data was modified, notifications for the user, etc.). Auxiliary data supplied by action dispatchers and needed for display purposes or element's logic should be defined as element’s properties. Same applies to data created/modified by the element but not intended to be persisted. If StateAware mixin is used along with ModelView, you can take advantage of statePath property that indicates path to the element's state in the application state tree. Whenever any data is mutated by action dispatchers at statePath or below, the element will receive notification of its properties' change (even if there is no explicit binding for those properties). See UniFlow.StateAware for more details and example. ModelView mixin defines some properties that are intended to be overridden in the elements:

  • validation property allows to specify validation rules that will be applied when validateModel() method is called. As a result of this method validation status will be updated to indicate result for each model field that has validation rule associated with it.
  • saveAction property indicates which action should be emitted when saveModel method is called to perform save of the model.
  • getMessage should be overridden with the function returning message string for given error code (to translate validation error code to message)

Example:

HTML:

<template>
 Model: [[model.id]]
 <paper-input value="{{model.name}}"
              label="Name"
              invalid="[[status.validation.name.invalid]]"
              error-message="[[status.validation.name.errorMessage]]">
 </paper-input>
 <paper-button on-tap="onSaveTap">Save</paper-button>
</template>

JavaScript:

class MyModel extends Polymer.GestureEventListeners(UniFlow.ModelView(Polymer.Element)) {

  static get is() { return "my-model"; }
  
  get saveAction() { return 'MY_SAVE'; }
  
  get validation() { 
    return {
      name: (value) => {
        if (!value || !value.trim()) {
          return 'Name is not specified';
        }
      }
    }
  }
  
  connectedCallback() {
   super.connectedCallback();
   this.fetchData();
  },
  
  fetchData() {
   this.emitAction({
     type: 'MY_FETCH',
     path: 'model'
   });
  },
  
  onSaveTap() {
   this.validateAndSave();
  }
}

customElements.define(MyModel.is, MyModel);

In the example above model view has input field for name property and Save button. On element attach the action is emitted to fetch the model's data. Note that in emitAction() method the path is specified as 'model'. ActionEmitter mixin is responsible of expanding the path with element's state path, ensuring that when action dispatcher gets to process the action, the path contains full path in the state tree. So assuming that my-model is declared as follows:

<my-model state-path="state.myModel"></my-model>

the path in MY_FETCH action gets expanded to state.myModel.model.

validation property is an object that contains methods for fields validation. The keys in this object should match model field names, the values are validation methods. Method receives current value of the field and should return non-falsy value (string or error code) if the value of the field didn't pass validation. status.validation object will be populated with the results of validation with the keys matching field names and values being objects containing two fields:

  • invalid: true when the value is not valid
  • errorMessage: the message to show to user

So in the example above if user clicks on Save button with name not entered, they will get 'Name is not specified' error message on the input element. When the name is non-empty, validation will pass and MY_SAVE action will be emitted with model passed as a parameter and 'model' as path.

State Aware

Key mixin that must be assigned to all elements that need to access application state and/or have access to the application element. The element is notified of any changes to application's state, as well as all its properties when they're modified by state mutator elements. state-path property must be used to identify path to element's state in application state tree.

Example:

HTML:

<template>
 <div>Value A: [[state.valueA]]</div>
 <div>Value B: [[valueB]]</div>
</template>

JavaScript:

class MyElement extends UniFlow.StateAware(Polymer.Element) {

  static get is() { return 'my-element'; }
  
  properties: {
    valueB: String
  }
}

customElements.define(MyElement.is, MyElement);

When above element is declared as follows:

<my-element state-path="state.myElement"></my-element>

it will be notified about changes (and render those) to state.valueA or state.myElement.valueB in action dispatchers or other state mutating elements.

State Mutator

Some non-visual elements, like action dispatchers, need to modify application state, in which case they should have this mixin assigned. Implements state- aware and re-declares state property with notify attribute. State mutator elements are only supposed to exist at the application level.

uniflow-polymer's People

Contributors

militeev avatar urdeveloper avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

uniflow-polymer's Issues

Sample application

Hello militeev,

thank you for your work on UniFlow.

But until now, it's hard for me to understand how all this will and should work together in an application. So may it be possible for you to create a sample application which demonstrates how UniFlow is intended to be used?

Regards,
Michael

Async action-dispatcher

Any thoughts on making the async action dispatcher?

I am trying out uniflow for one of my projects and it is wonderful. But there is no support for async actions.

I made some changes to the dispatcher by wrapping the return of the dispatch with a Promise.resolve and chaining the them rather than iterate.

Any thoughts? If you r cool, I can make a pull request.

Possible to use with "polymer build"?

Hello militeev,

is it possible to use UniFlow with "polymer build"?

When doing so, my own app is crashing when calling the build version in the browser (says "this.emitAction is unknown"). In development mode with "polymer serve", UniFlow works like a charm with my app. So it's the "polymer build" which "breaks" it.

And building your sample todomvc app with "polymer build" isn't working at all, the building itself crashes.

Any ideas?

Regards,
Michael

Where to call server api

I have a current solution based on optimizely/nuclear-js and heavily borrowed from home-assistant/home-assistant-js. It is similar in that there is a stateBehavior (the only behavior). Properties are defined with a bindState value that is a nuclear-js getter (function that returns the specified state). A simple example is

properties: {
  isAdmin: {
    type: Boolean,
    bindState: MyApp.authGetters.isAdmin,
  }
}

It is notified of changes and actions dispatched are used to update the state, just like uniflow. When saving a form we call an action which returns a promise. We can then wait for the response from the server and only leave the screen if successful, showing validation errors if necessary.

save: function() {
  if (this.$.form.validate()) {
    MyApp.userActions.saveUser(this.data).then(() => { 
        this.fire('user-save-clicked', this.user);
    });
  };
},

The MyApp.userActions.saveUser calls the backend server API to actually send the data and return any additional validation errors such as a non unique username error. It also updates other state such as the errorMessage to display on this screen. If the server returns an error, a Javascript error is thrown and the 'user-save-clicked' event is not fired. On success it is fired and an upstream component handles routing changes.

Where in uniflow should the calls to the server be put? I really like the ModelView validation method but where would it integrate with sending the data to the server and handling errors returned from the server? How would you wait to make sure it was successful before routing to a new screen?

I've reviewed the ToDoMVC app but of course it is only updating local state and not integrating with a server backend.

app-router integration

My app component contains my UniFlow dispatchers. It also contains my <app-location> and some of my <app-route> components. In Where to call server API, #1, we determined that we could emit a save action from a page such as a Profile Edit page. Some dispatcher would take care of calling the back end server API to save the data and emit its own action for success or failure. Another dispatcher (or the same) would receive the success action and handle routing.

How do you suggest setting that up? Would you pass the route into the dispatcher and put an <app-route> component in the dispatcher to be manipulated?

<my-app-dispatcher state="{{state}}" route="{{route}}"></my-app-dispatcher>

Then in <my-app-dispatcher> have a <app-route route="{{route}}"> in the template section so that it can be manipulated in the action methods that need to handle routing?

(I'll try this myself but) Can an observer be used to watch a specific portion of the state tree? For my application, my main app needs to know when the user is logged in or not and route to either the login screen or, depending on the logged in user's roles, a specific page. Can I use a simple

observers: [
  'onLoggedIn(state.auth.user.token)'
]

Is that the best method to listen to different branches of the state tree? In a lot of cases the state-path will be a section of the tree specific to the page but the page will still need access to other sections of the tree such as the current user's information.

bower polymer-uniflow-a

When I'm using UniFlowvia bower (bower install --save polymer-uniflow-a), Chrome gives me errors like "Uncaught ReferenceError: UniFlow is not defined at action-emitter.html:22".

When I download UniFlow from the website as a zip and put it manually in bower_components, everything is fine.

Is there any difference between the download version and the version supplied via bower?

Making a uniflow version of the Starter Kit app

I often do Polymer courses, workshops and mentoring, both to students and to professional developers, and I've found that most of my students like to use the Polymer Starter Kit as a base in order to build their applications, instead of beginning with an empty project.

I think that having an uniflow based version of the Starter Kit app, with the different elements already in place, would be a great way to introduce the uniflow pattern to Polymer developers.

I had thought about doing it myself, and maybe write an article on it, if it doesn't bother you and if you think it's a good idea.

Polymer 3

Hi, Polymer 3 is set to be released and I wanted to know if there was any chance this repo was going to be updated to use it or can it be used with Polymer 3 already as it stands?

Question: Does uniflow support using uniflow sub components?

Just starting to look at uniflow. Looks interesting. We are currently using Polymer-Redux in our app but interested in comparing it with alternative approaches.

One area we struggle with is building UI components that can manage their own state and actions and then just be dropped into an existing app which may also be using Redux. There are solutions in the Redux world, but I have not really got to grips with them yet, so we're still using the default approach of a single application level state atom that all our components tie into.

I'm sure I could answer this question myself after I've spent some time digging into uniflow a bit more, but just wondering if that is something that you considered with uniflow.

Access to state in state aware element

I'm trying to access state in javascript, all the examples I can find for state-aware show accessing state in the template. I can access this.stateAware and this.statePath ok but not the state itself. I'm using the latest 0.6.0 release.

What I'm trying to do is store some application level state to be used for many pages (the HATEOS route of my api which is pulled from the server) so I wanted to store it in state and access it using state-aware without passing it down through the element hierarchy. Is what I'm doing sensible or is there a better way, maybe the application object that I don't really understand what it's for?

Thanks for the help in advance, great project if I can work out how to use it properly.

Would it be possible to release a first version

Hi!

After having played a bit with UniFlow, I would like to use it in one of my apps, instead of a similar(-ish) home made redux-like setup. But in order to use it, I'd like to make the reference in my bower.json to point to a defined version, not to the head of the repository.

Would it be possible to put a tag and declare it as an initial version?

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.