ng-lit

Mixing to pass angular object and array from scope to lit-element

⚠️ Under development 🛠️

🔌 Boost Your Old AngularJS App LitElement Components.

CircleCI NPM version License

Are you stuck maintaining a crufy old angularjs 1.x app? Wish you could be writing straightforward component-based views but can't afford to move the whole thing to the Fancy New Framework™️? With LitElement and some helpers from ng-lit, you can incrementally update your old app piece by piece.

👩‍🚀 Installing

npm i -S ng-lit

ng-lit lets you pass objects and arrays from your AngularJS application into your lit-element views without parsing or watching them yourself.

👨‍💻 Usage

Demo on jsfiddle

Your New lit-element View

import { LitElement, html } from "lit-element";
import { NgLit } from "ng-lit";

class NgLitUser extends NgLit(LitElement) {
  static get properties() {
    return {
      age: { type: Number },
      user: { type: Object }
    };
  }

  // declare the angular props
  static get ngProps() {
    return {
      user: { default: {} }
    }
  }

  render() {
    const { age, user } = this;
    return html`
      <span>${user.firstName} ${user.lastName} is ${age} years old</span>
    `;
  }
}
customElements.define('ng-lit-user', NgLitUser);

Your AngularJS App:

<!-- angular -->
<div ng-app="myApp"
     ng-controller="myCtrl">
  <ng-lit-user
       user="ngUser"
       age="15">
  </ng-lit-user>
</div>
<script>
  angular.module('myApp', [])
    .controller('myCtrl', $scope => {
        $scope.ngUser = {
          firstName: "John",
          lastName: "Doe"
        };
  });
</script>

🧙‍♀️ Motivation

ng-lit allows you to bring your old AngularJS application up to date piece-by-piece, using lit-element components to refactor from the bottom-up.

Web components work out-of-the-box in angular templates, but due to some quirks in angular's data binding system, it can sometimes be awkward passing your app's data back down into your web components. ng-lit helpers make it easier for your new lit-element components to live side by side with old AngularJS code, until you can fully drop AngularJS from the app.

Conceptual example

Consider an angularjs todo-app composed of three components:

  • Main app entrypoint that loads a list of todo objects on to the $scope.
    <todo-main-app>
  • Component that get a list of todo objects and and renders <todo-item>s for each one.
    <todo-list todos="vm.myTodoList">
  • Component that get a single todo object and render it's text and isDone state.
    <todo-item todo="vm.singleTodo">

For your first incremental change, you can use ng-lit to build a new implementation for <todo-item> based on lit-element with same interface as the old angularjs directive.

<lit-todo-item todo="vm.singleTodo">

You can stop here and the app will still work fine. When you're ready to move on, you can continue by upgrading <todo-list> and finally <todo-main-app>.

👨‍🏫 API

Reactive

Dy default ng-lit will render your component on changes made to object or array's reference.

The following example will update your element when $scope.myBook is updated with new object:

class NgBook extends NgLit(LitElement) {
  static get properties() {
    return {
      book: { type: Object }
    };
  }

  static get ngProps() {
    return {
      book: { type: Object }
    }
  }

  render() {
    const { book } = this;
    return html`
      <span>${book.title} by ${book.author}</span>
    `;
  }
}
customElements.define('ng-lit-book', NgBook);
<!-- angular -->
<div ng-app="myApp" ng-controller="myCtrl">
    <ng-lit-book book="myBook"></ng-lit-book>
    <button ng-click="selectBook({title: 'Anna Karenina', author: 'Leo Tolstoy'})">
      Anna Karenina
    </button>
    <button ng-click="selectBook({title: '1984', author: 'George Orwell' })">
      1894
    </button>
</div>
<script>
  angular.module('myApp', [])
    .controller('myCtrl', $scope => {
        $scope.myBook = null;
        $scope.selectBook = book => {
            $scope.myBook = book;  
        }
  });
</script>

Properties

When you want your component to get certain props, add them to the ngProps static getter. You still have to define those properties in the regular lit-element static properties getter. The idea is that eventually you'll remove angular from your app entirely, at which point you'll just need to remove the ngProps block;

The following example will fetch a list of books and a selectedBook object from angular while userId will be treated as normal custom element property, without special arrangements for angularjs' data system.

static get properties() {
  return {
    userId: { type: Number },
    books: { type: Array },
    selectedBook: { type: Object }
  };
}

static get ngProps() {
  return {
    books: { default: [] },
    selectedBook: { default: {} }
  }
}

Defaults

use the default option to pass a default value which your prop will get in case angular doesn't have that value in scope, or the value found was null.

static get ngProps() {
  return {
    selectedBook: { default: { title: '1984', author: 'George Orwell' } }
  }
}

If you pass an object as the default, it will be cloned before it's assigned to the instance.

Watch

Set the watch boolean option to make your element update when angular changes the property.

The following example will update your element when $scope.addBook() is called:

class NgLitBookList extends NgLit(LitElement) {
  static get properties() {
    return {
      books: { type: Array }
    };
  }

  static get ngProps() {
    return {
      books: { default: [], watch: true }
    }
  }

  render() {
    const { books } = this;
    return html`
      <ul>
        ${books.map(({title, author}) => html`
          <li>${title} by ${author}</li>
        `)}
      </ul>
    `;
  }
}
customElements.define('ng-lit-books', NgLitBookList);
<!-- angular -->
<div ng-app="myApp" ng-controller="myCtrl">
    <ng-lit-books books="myBooks"></ng-lit-books>
    <button ng-click="addBook({title: 'Anna Karenina', author: 'Leo Tolstoy'})">
      Anna Karenina
    </button>
</div>
<script>
  angular.module('myApp', [])
    .controller('myCtrl', $scope => {
        $scope.myBooks = [];
        $scope.addBook = book => {
            $scope.myBooks.push(book)  
        }
  });
</script>

👨🏽‍💻 Unit Test your components

We recommend using @open-wc/testing-helpers for unit testing your Web Components.

In order to Unit Test ng-lit component we just need to mock angular scope, to do so we expose MockScope API.

The following example will text NgLitBookList component:

  import '/components/NgLitBookList.js' 
  import {fixture, html} from '@open-wc/testing-helpers';
  import {MockScope} from 'ng-lit/mock';

  describe('ng-lit-books', async () => {
    it('should render component with 2 books', async () => {
      // Mock angular's scope with 2 books
      MockScope({myBooks: [
        {title: 'Anna Karenina', author: 'Leo Tolstoy'},
        {title: '1984', author: 'George Orwell' }
      ]});
      const {shadowRoot} = await fixture(html`
          <ng-lit-books books="myBooks">
          </ng-lit-books>
      `);
      const renderedBooks = shadowRoot.querySelectorAll('li');
      expect(renderedBooks.length).to.equal(2);
      expect(renderedBooks[0]).to.equal('Anna Karenina by Leo Tolstoy');
      expect(renderedBooks[1]).to.equal('1984 by George Orwell');
    })
  });

👨🏽‍💻 Developing

Installation

git clone [email protected]:oriweingart/ng-lit.git
cd ng-lit
npm i

Run Locally

npm run dev

Run demo examples

npm run demo

Run end-to-end tests

npm run test:e2e

Run unit tests

npm run test:unit

Run both unit and e2e tests

npm run test

Run Lint

npm run lint