Что нового в Angular 5 (по мотивам митапа AngularNYC September)

Посмотрела на днях запись очень интересного митапа AngularNYC September в Нью Йорке.

Первый спикер Alex Zhang рассказывала, что нового в Angular 5. Да, да, окончательная версия уже совсем на подходе (23.10.17 релиз), так что информация довольно полезная.

Правда, насколько я поняла, это то, что входит в бету 5.0.0‑rc.0. Так как релиза-то еще нет.
И, конечно, не все, а самое интересное по мнению Alex.

preserveWhitespaces

preserveWhitespaces — это свойство компонента, с помощью которого можно предотвратить в простейшем случае, например, пробел между кнопками.

Назначаем false:

@Component({
  selector: 'app-root',
  templateUrl:'app/app.component.html',
  styleUrls: ['app/app.component.css'],
  preserveWhitespaces: false
})

В таком случае в шаблоне компонента будут удалены все пробелы.

Но мы можем пойти дальше и задать его глобально:

platformBrowserDynamic().bootstrapModule(AppModule, {
   preserveWhitespaces: false;
});

Это может уменьшить размер сгенерированного через AOT кода, так как будут удалены все пробелы.

По умолчанию данное свойство сейчас true, но это может измениться в будущем.

Его можно обойти несколькими способами:

  1. Написать вместо
     
    

    специальный символ &ngsp; , который компилятором Angular будет трансформироваться в пробел.

    <button>one</button>&ngsp;<button>two</button>
    
  2. Можно завернуть кнопки в директиву ngPreserveWhitespaces>

    <div ngPreserveWhitespaces>
     <button>one</button> <button>two</button>
    </div>
    

    В таком случае, даже при наличии глобального значения preserveWhitespaces: false, между кнопками останется пробел.

Однако, нужно учитывать, что можно получить только один пробел, даже если проставить кучу &ngsp;, они трансформируются в один пробел:

<button>one</button>&ngsp;&ngsp;&ngsp;&ngsp;&ngsp;<button>two</button>

Множественные exportAs имена (multiple exportAs names)

ExportAs — свойство директивы, которое позволяет предоставить доступ к ее экземпляру в шаблоне, а следовательно по сути доступ к API директивы. Подробнее можно прочитать в статье Angular 2 — Take Advantage Of The exportAs Property

Так вот, теперь можно задавать два значения exportAs — одно старое, другое новое, что может помочь с обратной совместимость.

@Directive({
  selector: '[some-directive]',
  exportAs: 'one, two'
})
export class SomeDirective {
}
<div some-directive #someone="one">ice cream</div>
<div some-directive #someone="two">cake</div>

Обновление i18n

i18n — инструмент для локализации, облегчающий перевод приложения, использующий стандартные форматы: xlf, xlf2, xmb.

Сама я вместо него начала работать с ngx-translate. Так как переписываю с AngularJS, где использовался angular-translate, чей синтаксис в шаблоне похож на ngx-translate, так что это мне несколько облегчает жизнь.
Плюс мы используем удаленный сервис переводов, так что просто нужно заменять в шаблонах обозначение слова на само слово. Так что могла перевести не совсем удачно, исправляйте, если что 🙂

Одна из фич — можно помечать i18n текст, не создавая DOM элемент. Это полезно, если для каких-то css целей элемент совсем не нужен.

Раньше было два способа:

  1. С помощью элемента ng-container:

    <ng-container i18n>I don't output any element</ng-container>
    
  2. Обернув текст в html комментарии:

    <!--i18n: optional meaning|optional description -->
    I don't output any element either
    <!--/i18n-->
    

Так вот, теперь второй исключен.

Следующий момент — теперь можно добавлять source file information в xmb/xliff переводы.

<msg id="8884523860497558714" meaning="header" desk="desk" source="src/basic.ts;5,6">Translate me</msg>

Это полезно, так как id уникальны, но базируются на тексте. И если изменить текст, то измениться и id.
Поэтому здорово разделять значение и исходный файл.

Подробнее про существовавшую проблему можно прочитать тут.

Далее — больше не используется Intl Api

Почему?

  • Многочисленные баги
  • Несогласованность с браузерами, ограниченная поддержка
  • Вместо этого можно экспортировать данные из Unicode Common Locale Data Repository (CLDR)
  • ….но это нарушает некоторые вещи, связанные с i18n и date pipe

Есть изменения и в i18n pipes.

По умолчанию в Angular локальные данные только для en-US.
Если вам нужен LOCALE_ID для другого языка, то необходимо его импортировать.

Старые пайпы i18n можно использовать, но учтите, что они теперь не в CommonModule, а в DeprecatedI18NPipesModule.

import { NgModule } from '@angular/core';
import { CommonModule, DeprecatedI18NPipesModule } from '@angular/common';

@NgModule({
  imports: [
    CommonModule,
    // import deprecated module after
    DeprecatedI18NPipesModule
  ]
})
export class AppModule { }

Со всеми изменениями можно ознакомиться в changeLog и в табличке.

Обновление zone.js

zone.js оборачивает любые асинхронные вещи в браузере и помогает Angular находить где запустить change detection. Что намного круче, чем запускать CD полностью.

События могут быть интенсивными (например, скрол), что делает приложение медленным.
Но теперь есть возможность добавить его в черный список, тем самым событие будет запускаться вне ngZone.

// before load polyfill.js
<script>
// black list scroll event handler for addEventListener
Zone[Zone.__symbol__('BLACK_LISTED_EVENTS')] = ['scroll'];
// black list scroll event handler for onscroll
const targets = [window, Document.prototype, HTMLBodyElement.prototype, HTMLElement.prototype];
 __Zone_ignore_on_properties = [];
targets.forEach(function(target) {
  __Zone_ignore_on_properties.push({
    target: target,
    ignoreProperties: ['scroll']
  });
});
</script>

HttpClientModule вместо HttpModule

Улучшения:

1. Больше не нужно вручную извлекать json

Раньше приходилось делать так:

list(): Observable<UserModel> {
  return this.http.get('api/users')
  .map(response => response.json())
}

Теперь:

list(): Observable<UserModel> {
  return this.http.get('api/users')
}

2. Можно использовать HttpClientTestingModule, который делает тестирование проще (на первый взгляд — как минимум меньше кода :)) )

Раньше тест мог выглядеть примерно так:

describe('UserService', () => {

  beforeEach(() => TestBed.configureTestingModule({
    imports: [HttpModule],
    providers: [
      MockBackend,
      BaseRequestOptions,
      {
        provide: Http,
        useFactory: (backend, defaultOptions) => new Http(backend, defaultOptions),
        deps: [MockBackend, BaseRequestOptions]
      },
      UserService
    ]
  }));

  it('should list the users', async(() => {
    const userService = TestBed.get(UserService);
    const mockBackend = TestBed.get(MockBackend);
    // fake response
    const expectedUsers = [{ name: 'Cédric' }];
    const response = new Response(new ResponseOptions({ body: expectedUsers }));
    // return the response if we have a connection to the MockBackend
    mockBackend.connections.subscribe((connection: MockConnection) => {
      expect(connection.request.url).toBe('/api/users');
      expect(connection.request.method).toBe(RequestMethod.Get);
      connection.mockRespond(response);
    });

    userService.list().subscribe((users: Array<UserModel>) => {
      expect(users).toEqual(expectedUsers);
    });
  }));
});


Теперь же:

describe('UserService', () => {

  beforeEach(() => TestBed.configureTestingModule({
    imports: [HttpClientTestingModule],
    providers: [UserService]
  }));

  it('should list the users', () => {
    const userService = TestBed.get(UserService);
    const http = TestBed.get(HttpTestingController);
    // fake response
    const expectedUsers = [{ name: 'Cédric' }];

    let actualUsers = [];
    userService.list().subscribe((users: Array<UserModel>) => {
      actualUsers = users;
    });

    http.expectOne('/api/users').flush(expectedUsers);

    expect(actualUsers).toEqual(expectedUsers);
  });
});

3. Можно использовать перехватчики (interceptors)

Эти перехватчики вызываются для каждого запроса и ответа. Они позволяют легко управлять такими задачами как, например, добавление заголовка ко всем запросам.

Пример:

@Injectable()
export class GithubAPIInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    // if it is a Github API request
    if (req.url.includes('api.github.com')) {
      // we need to add an OAUTH token as a header to access the Github API
      const clone = req.clone({ setHeaders: { 'Authorization': `token ${OAUTH_TOKEN}` } });
      return next.handle(clone);
    }
    // if it's not a Github API request, we just handle it to the next handler
    return next.handle(req);
  }

}

Больше используемых материалов

Angular — How to use HttpClientModule?
Презентация Alex

P.S. Второй доклад был тоже очень интересным — «Масштабирование приложения, как профи!», но пока не решила стоит ли и его перевести.

Хотите быть в курсе новых статей?