Advanced Angular Concepts – Alex Rickabaugh – AngularConnect 2017

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

Very useful talk if you are a library dev/maintainter

πŸ‘οΈŽ︎ 1 πŸ‘€οΈŽ︎ u/i_spot_ads πŸ“…οΈŽ︎ Dec 18 2017 πŸ—«︎ replies
Captions
ALEX: How is it going, AngularConnect! How are you guys doing? It has been an awesome day. Alright, well as Ed said, I'm Alex. This talk is called Advanced Angular Concepts. So, I'm a software engineer at Google, and, for the last three years, I've been a member of the Angular core team. On the team most recently, I worked on our HTTP client implementation, the Angular API which is stable in Angular 5. It supports a number of advanced features line interceptors which we are going to see today. With 5, I launched our service worker implementation which is something I've been working on for a long while making Angular more reliable across flaky network connections. Service workers are part of our progressive web app story which I've been passionate about and talked at other conferences, including last year at Connect. But today, we're going to do something that is completely and totally different. Occasionally, I go to these events and I talk to people, and I'm active on our community chat on Gitter which is a good place to meet other people in the Angular community. People pose questions there like, "How do I design this thing with Angular?" . Some of the more elegant answers use the lesser known elements of the APIs. Not necessarily more complex but things that your average developer doesn't encounter on a day-to-day basis. That's what we will do today: dive into a few cases and learn how to use Angular APIs to build better services and components that we share and create with others and for ourselves. The first is to learn how to configure services and libraries and learn interesting ways of communicating between components that go beyond simple inputs and outputs. Rather than explore all of these topics abstractly, we are going to consider them in the context of two design challenges that I've seen come up in the past. The first is to build a retry interceptor for the HTTP client which takes its configuration from the consumer. It's going to allow a configurable number of retries as well as specific retry limits for individual domains because not every service that we depend on is as reliable. The second thing we're going to build is a wizard component. That's going to be our example of intercomponent communications. A wizard is an orchestration component that displays the sequence of steps to the user and moves them through them, and those stems we will try to decouple of as much as we can. We have two topics: dependency injection and queries. We will talk about them individually and show an awesome way to combine the two. Let's go ahead and jump straight in: so dependency injection: it is not that new of a concept in computer science. It is very popular in a lot of backend frameworks - Spring for Java, and Google has that juice implementation. Angular.js was one of the first to implement the I in the browser and Angular improved it by coupling it with TypeScript. The main goal of any DI framework is to separate the concept of depending on something from the knowledge of how to construct it. So, with DI, we don't have to know how our services or other depend ease are instantiated. We simple ask the framework for an instance that someone else has considered for us. That decoupling of dependency is one of the i's many benefits. We can depend on the aim of something without necessarily knowing what implementation we will get at runtime. DI is an excellent mechanism for packing up a service or set of services and publishing them in in a usable fashion in how many have used Firebase? Awesome. Keep your hand up if you used Angular Fire. It is a fairly complicated library, internally, but for the user, it is relatively simple to use because it comes with a couple of modules that you drop into your application and configure, and that's thanks to DI. DI is very important in testing. It's what allows us to test components in isolation by marking out their dependencies so we don't have to bring up entire applications to test one small part. The first thing we are going to do is implement an HTTP interceptor and retry request. That in itself is easy to do. We're not concerned with how this interceptor works internally, we care about how the user sees it, can configure it and make it behave they want it to. It should be reusable, that means having a module that you can you can install to use it and allows the customer to customise the retry limits. Finally, it should be safe to use. If you're a user and you drop this thing in your application and you don't set it up correctly, it should tell you at compile time when your application starts. It should not silently pale to work. This is going to be our interceptor. We're not too concerned with the implementation. Our RxJS makes this a one-liner so it is not that interesting. We care more about how it's configured. This is our module. Right now, this is a template. We will answer the questions of what should it provide and what should its implementation be? We're going to put some code inside the module class which is not something you often see in Angular. We're going to fill these in as we explore the capabilities of DI. I'm going to do that through four topics: we're going to talk about the tokens, which are the building blocks; then the providers that configure the tokens; the modules that contain and organise the providers; and finally, the injectors that we are providing from those modules. Tokens are building blocks. If you think of injecting dependencies is looking up values on a map, then tokens are the keys. Because they actually exist in a map at run time, they have to have runtime values. You can't use any type-like string as a token. Many tokens we're used to dealing with are class types - classes we can declare and they can even be abstract. But there are a lot of things that you didn't use directly with DI. You can't provide an interface, or a primitive type like a string because those don't have runtime values and there are a lot of uses where you might want to do that. If you're taking configuration, your configuration is to set primitive values or interface to expect some objects you're going to get. You need to mesh them with the DI. In Angular, that comes in the form of injection token. Injection token is the key that allows us to reference and inject an arbitrary type. So, in this case, we have an example where we are injecting a token, that represents an - url value - and then inject it into the service. When we are injecting this thing, we can't just say "inject a string" because Angular has no idea what string we're talking about. The TypeScript type isn't enough. We add this inject metadata and that tells Angular which token we want to use to satisfy this injection. We can now get started working on this interceptor. We need to know the number of times we want a retry request so we will try that with the DI token we creatively call retries. You get a hold of this value and use it when it is handling requests. But to see how to configure this, we need to talk a little bit about providers. So, we are going to go through the different kind of providers and apply what we learn to start filling out our module a bit. So a provider in Angular is a declaration of where the value of a token should come from. In most cases, we ask Angular to provide us the token by instantiating a class. You can also provide a literal value or ask Angular to alias the token to another type. Finally, you can take over completely and give Angular a factory function that it will use to take over the token. Let's go through a quick example of each of these. The most common way to provide something is with use Class. You're asking Angular to instantiate it for you by looking at the instructor. A lot of the times the class that needs to be instantiated is the class that you're using for the token. You're just providing a service. So Angular has some syntactic sugar for expressing that. The two that are listed here are actually equivalent. Both specify that retry logging service should be provided by instantiating itself. The second way you can provide a value for a token is to list it literally. So, in this example, we are providing the retries token we created earlier using the constant value of three. Right, that's relatively straightforward. Another way to configure a provider that I actually don't see used that often is to specify that it should have the same value as another existing token, right. So, to do that, you say provide something, use existing, and there's something you should actually be careful about with this used class, because the two can look very similar, but they have a major difference in behaviour. So take a look at this example. Right, this is using use Class. The first provider provides it with itself. Angular's going to create an instance of it. The second provider says to provide a different token, retry service, using a class of time-out retry service. So we get a second instance of the service. That can be a problem because, depending on which token you use to inject it, you get different instances of the same service. If the service happens to be stateful, you can have different versions in different places and they don't work together, right? You can have really subtle bugs in applications happen this way. I've seen it. UseExisting doesn't have this problem because you're creating an alias, creating the same value for the new one. We have one service here that you can inject in each token which is a lot safer to do. The last way we have of declaring a provider is to tell Angular how it should be constructed directly by providing a factory function. If you use factories, it's not where you have the export function and the - we are actually using a lambda here. In Angular 5, this works. It took forever! If you want your factory to have arguments, and those arguments to be injected, it's unfortunately not enough just to specify types in TypeScript, you have to literally declare out what types of arguments are in this deps array, and that can be annoying, and you can trip up on this. We tried services using an off service to get a token from it before it creates it in the factory. So, ordinarily, if you provide the same token twice, the one that you choose will override the one that happened before. Sometimes, that's not the behaviour that you want. You really want to get all the values that people have provided for one token. For an example of this, think of the router. Right? With the router, we are able to, in different parts of our application, import router module for child, specify a where did you go of routes, and the router doesn't pick one of them to go with, it actually collects all of them and builds the full routing configuration from all the individual pieces. That's called a multi-provider. They're relatively easy to use. You have to specify multi- to true when you're providing values to tokens and Angular are take care of injecting them all. Here's a pretty example of that. We have a token cookie name. We are providing two different values for it, both with use values, so this year literals. When we inject cookie name, we don't get the last value but an array of all the different values. So now we can begin building out our retry module. The first thing we probably want to do is configure our interceptor. As it turns out, HTP client uses a multi-provider to get the list of interceptors. All we have to do is provide this token, HTP interceptor, we name our class that we want Angular to instantiate for us and we say that multi is true because there can be more than one interceptor in this. Angular are build an array and inject it into the APC client. That's pretty cool. Next, we want to specify a default value for the retry token. If a user installs the module, they will get sensible behaviour. Three is a pretty good choice. Now, anyone who wants to use our interceptor can just import retry module. They're probably importing the HTP client - because that's what they are using for the make request - and the interceptor will just start working. That's a fairly simple API. They can also use it to override the behaviour, if you like. If we expose the retries token to them, they can specify their own provider for it. Since they don't use a multi-provider, it will override the retry module because the current module providers overwrite those from the imports. The next goal we had was to set specific retry limits for specific domains. We can do that by requiring the user to provide a token that represents a map, and the map will associate a domain with a specific limit for it, and the user would declare all of that in their application module. But it would be nicer if users could move the configuration for a specific domain right beside where they can configure their service that accesses that domain. They have a module for each API that they access. Like Angular does HPC interceptors or routes, we can use our own multi-provider to do this. So the first thing we need is a type-two inject. In this case, it is going to be an interface called domain retry limit that specifies the domain and the limit. We need an injection token for that interface because we can't unions the interface directly in the DI. Nothing about this token suggests that we are using multi-injection with it. It is completely up to the consumer to decide how to provide it. So with this token, a consumer can specify a value for a particular API domain directly from the module where they configure their API for that domain. Here they have an API module, providing some service for it, and they're also saying for this domain, we want to do five retries instead of three. That keeps all of the configuration in the user's application for the API in a single module. That's a really nice property to have. So, on the interceptor side, all we have to do is inject this token and tell TypeScript that we are expecting an array, and inside it will probably build up the mapping we talked about earlier and use that whenever a request comes through. But what happens if nobody specifies this token, right? What happens if the user doesn't want to do any customisation? Angular doesn't know about the token, so if we just ask to inject it, we will get an error, right? Nothing actually provided it. But, fortunately, we have a way of telling Angular this dependency is actually optional. We can use this optional metadata marker and Angular won't care, it will simply inject null. So we've already talked about modules a little bit as we build out the retry module but we are going to learn a few more things about them. So we've seen how modules package code for reuse, and also a little bit about how they affect the ordering of providers. We will take a look at their role in providing a nice API for user configuration better than what we've come up with so far. So, firstly, wrapping our retry interceptor into a module does make reusing it very nice. All someone has to do is import it into the at module. We've seen - the appmodule. The provider is inside the app module, overriding the ones that are importing. Provider importing is not only important in the application itself but it is important in testing. It's what allows us to have testing modules - for example, a hypothetical mock retry module - that overrides the providers from the application. So, inside of a test, for example, we could disable retry because we know the server isn't going to succeed if the test is designed to fail, so the test will run a little faster. However, I'm sure you will agree with me that this API is not really even the nicest that you might imagine. For example, the user has to know that this retries token exists, right? They have to find that in the documentation somewhere. The token is configured in a different section of the app module and there is nothing here that indicates that the retries token and module have anything to do with each other. As it turns out, Angular gives us some tools to do a little bit better here. This is what it possible. We can eliminate the need for the user to specify providers directly. And hide it behind a static function they call on the module from their import section. In this case, we've named it "with retries". This is a much cleaner API. It is clear we are configuring the module, discoverable through code completion and we get much better type safety. So how do we get this? What can we do in our module to implement this? The answer is a feature in Angular called a "module With Providers function". They can take parameters. But they're not functions in the sense that they will be evaluated at run time, they're instructions to the compiler so they've special rules associated with them. They are the same rules that you see inside of metadata. We can't have logic, we can't do operations and code, we can only return an object. This object specifies a module, and a bunch of providers to layer on top. So, here's what this looks like. Implemented in retry module. We declare a static function called With Retries. It has a parameter, and that is used to provide the retries token with use value and the object that we return. So the interceptor hasn't noticed anything has changed. It is still getting its configuration through DI but now the complexity of setting up this provider, and this example is simple but you could imagine something like Angular Fire has five or six parameters that you have to define been the complexity of providing all of those is abstracted away inside the module itself. The consumer only has to call the function, which has really nice type signatures, to import the right configuration. That's a much better experience if you ask me. We can do the exact same thing with our per-domain configuration. Providing the user to code out this environment is riskier - there's a lot more they can get wrong, especially if they forget to say multi-true and forget everything that came before it. This can also be wrapped in one of those modules with providers' functions. Let's call it retry module.customise domain. It will take two arguments and it request be imported from the API module like before. There's no harm in importing a module in different places. They will be deduped at runtime. The installation is similar. Instead of one parameter, we take two. We bind them in DI as before, except the user doesn't have to know about the custom interface we are dealing with, either. We can construct the object that gets bound instead requiring the user to do that, so even cleaner. That brings us to our last DI topic of the day which is injectors. There are a lot of those running around. They are the runtime interface to DI. You can think of them as a map of tokens to the values for those tokens that were determined by the providers. A really thing about injectors is they exist in a hierarchy in Angular. They have a parent. If an injector doesn't have a parent when you inject it, it will delegate up the tree. It's this hierarchy I want to talk to you about. It starts in Angular with the platform injector, the top-level injector on any page. It has in it single instances of all of the services that interact with DOM APIs, location bar, et cetera. Everything that there is only one of. No matter how many applications you have running on the page, they're all sharing the same location bar, right? You can't use the router for three applications on the same page without them trying to trip over each other, updating the current location. It represents the single ton state. The next down on the application injector. That's a child of the platform. This is the one that has all the providers from your application module. This is the one that gets created when you call bootstrap module or bootstrap module factory. There can be many such applications on a single page, each created from an application module, all of them children of the same platform. So, in the hierarchy, it looks like this: simple so far with platform at the top, application down below. As it turns out, it gets more complicated. When you start lazy-loading things, which everyone should be doing in Angular, it gets its own injector, right? When you lazy-load a module, that module has its own injector - that makes sense. A separate piece of code injected in. It would be strange if we stuck them in the application that was already running. Maybe we have to recreate a few services because their provider changed, and that can get complicated really quickly. It makes sense when we load new code in, we get a new level on the injector hierarchy. If the use the router, for example, to display a lazy-loader component, that component class actually gets injected from the lazy injector, not the application. It will delegate up the application, and so on. You can do really cool things with that but it creates a real problem known as shadowing. So shadowing is what happens when you have multiple providers for the same token in different injectors. We saw a taste of use Class and useExisting. Especially if it is broadcasting events that your application wants to listen to, you can get weird behaviour if you have different components injecting different components of the service. This is what it looks like in the hierarchy. An application component coming from the app injector. That gets instance A. We also have a lazy route which is injecting the same service, same token, but it gets its one from the lazy module injector, and the lazy module injector has instance B this. That can create really subtle problems. This is really hard to debug, actually. Fortunately, the community has a solution for this, in the form of something called a four-route function. You may have seen these running around and wondering how they work internally: it is just a convention been what it says this is a module with providers that should only include providers that go in the application module. Nobody should ever export one of these things in a lazy-loaded context. Here is what our forRoot would look like. We have things at the top which don't matter - things that are stateless - but the critical providers would go in the section of the forRoot. This would work great if everyone followed the right rooms and only never wrote code like on the right which is a safe assumption because developers use APIs 100 per cent of the time the way they are meant to be used. Everyone reads the documentation. What could possibly happen here! So, instead of relying on convention, I suggest you direct your modules to actually check for the conditions that cause problems. For example, you can actually ask Angular if the parent injector and the current injector have two different versions of the same service. Right, if they do, you can complain about it loudly instead of getting your user into a situation where they end up with subtle bugs. So the problem with our retry interceptor is that HTP client has to be provided in the same configure with - our users get a nice error instead of wondering why their interceptor isn't working. So it's actually relatively he's a to do this. Modules are allowed to have constructors. Those constructors do get called at runtime. As long as we inject HTP client, Angular will make sure that it is provided. If a user drops in retry module and isn't using HTP client, they will get an error. We can tag this injection with metadata @self. It says get it from the current injector, don't go up the hierarchy. That is much better than failing silently if the user puts the module accidentally somewhere that the HTP client isn't configured. So, with this, we've accomplished all the goals we set out to design retry interceptor. It is easy to configure and it is safe to use. We still have a little bit more about injectors to cover but we will move on to the next design challenge which is building a wizard component. Like I said before, a wizard is an orchestrator. It takes a sequence of steps and walks the user through them in order and prompting them how to complete each step. Once again, we're not going to care about the implementation of the wizard, we care more about the architecture, right? How do we model a wizard and its steps and how do they communicate with each other. In principle been we want this implementation to be as decoupled as possible. Ideally, the wizard should know nothing about the step components except the API that they use, and the steps should have no knowledge of the wizard that they're going to be used in. Here's the API we would like our wizard to have. It's a component that you can use on a template, and you content-project a bunch of steps on to it. That's it. It is clean and efficient, but sadly impossible. We can almost get there, though. So our wizard implementation is going to start off rather simply. It is a component with a template which just projects everything passed into it. So let's take a look how we can architect this wizard and its steps and learn more about the injectors in the process. As it turns out, beyond the platform, the injection, any application and lazy-loaded modules that you have, there are injectors in the view. So many times, elements in the view, especially when they have a component attached, will have their own injector, and the hierarchy of those view injectors roughly follows the DOM hierarchy up to the component that loaded that in the first place. Understand that a little more and how it affects component design we can take a look at how template-driven forms work. This is a simple DOM hierarchy with a application for a form. We have a root component, the app component, the form inside of that, and one input with an ngModel directive opinion when you're using template-driven forms, the form element has a directive applied to it called ngForm. That directive is available for injection by anything inside the form such as ngModel. So here's what this looks like in the chart, right? Geneva the application module injector, and form has an injector. The injector for form has ngForm provided. Because input is the child of the form injector, the ngModel and input it- inject the ngForm. That's how they communicate, right? NgModel doesn't get the current form pass in, it injects did. This is simplifying it a little bit. But this kind of implicit component communication that uses DI in the view hierarchy, powerful. Let's apply that to design our wizard. So since the step components are project he could into the wizard into the -- projected into the wizard in the view, we can inject the wizard into the step. Because we want the wizard to talk to the steps, the steps have to register themselves with the wizard when they have created. They parse themselves. All the wizard has to do is implement the API. That's easy. Well, not quite, because this doesn't solve the design challenge that we set out to do, right? This has strong coupling between the wizard and its steps. Not only does each step know what wizard is going to be used in - the exact type of the component - but they can't actually work without one. They are required to inject it. Instead, let's move on from talking about DI to talking about queries. Use them to decouple the wizard in its steps. Queries are a mechanism in Angular for reading state out of the view. They work using these four annotations. You place these annotations on component properties. View child, view children, content child, and content children. They don't use DOM APIs to go query and find elements. They result in dominance in compile time generation, set up ahead of time, and that means they are very efficient. To study queries, we will simplify our wizard a little bit. Instead of content-projecting, we will assume it's the one step hard-coded into the view. It's in the wizard template, not projected in. Since we know the wizard is going to have this child step component, we can create a property step of the right type and add this view child annotation to it. The annotation tells Angular we would like to keep this property updated with a reference to the step component that gets created in the view. Unfortunately, that reference isn't going to exist right away. It will be undefined. There's a because when the object is being constructed, which is what this wizard is, the - that's a product that happens asynchronously. We need a way for Angular to tell us the view is ready, and we can do with AfterViewinIt. It's inside this method that Angular is letting us know the view is ready and any view queries you have are good to go. So now we can console-log out our reference. It's important to be careful with this view event because it is change at the time ex-which starts creating the view in the first place, so the view is initialised after the first round of change detection has completed. As you know, Angular has unidirectional data flow, so we can't change component state after change detection has occurred in the middle of this micro task. We have to actually trigger a set time out and a whole new round of change detection. So just something to be careful about. As I mentioned before, this view child reference is actually live. We're not only getting the instance when it is first created, it updates if the component changes or goes away. So, for example, if this step is behind an ngIf and when the "if" condition becomes false, the step reference can go to null. It is also possible to ask Angular to query for an element that doesn't have a component or directive on it. This is done using the number tag symbol to apply a hashtag like name to the element, in this case called "step", and that's called a reference. We pass that string to view child instead of component type. What we are getting back is not - but actually an element reference which is Angular's way of talking about an element that doesn't have any other type associated with it. We can use the element raft to interact natively with the element on the page. But what happens if you have more than one step? For example, if our step component is repeated with an ngFor, what comes out of this query? Does it come back as an array? Unfortunately, no, it only gives us back the first step we created. If we want all of them, we have to use an annotation called View Children, and that corresponds to all of the matches instead of a single one. It returns a thing called a query list which is like an array but has a few extra options on it. So it has all of the array operators, right? You can go four each, reduce, access something like index. If you need a true array, you can call to array on it. It has changes to property. Changes gives you an observable that admits whenever the view changes. If someone changes the array you're its demonstrating over and you get new steps created, changes will emit and we can react to it. The wizard component can watch the contents of this query in real time and react when new steps are created. Maybe it needs to reduce some internal state. The only probably is that these step we have are configured inside the wizard's view. Can we query for them if they're not in the view but are instead projected in as we designed earlier? It turns out you can do this with content child and content children which work similarly to the view queries but work on projected content and not templates. They have their own event which specifies whenever they're ready to use. We can convert our wizard to the conventional design, injecting content in for all of its steps. The users will use it like this, right? The only thing we have to change is every step component has to be the same one because that's what we are using the query for them. So, with that, the wizard can get a list of all the projected steps and interact with the API on each step. It can tell every step that the user is currently on wants to hide itself, so can now the title of each step so know the right bread crumb trail at the top. This design is getting closer and closer to what we want it to be. It is still annoying in one way. Every step component has to be the same, and the wizard those what they are. But we wanted a design where users could bring their own component and use it as a wizard step as long as it uses it as the API. Let's fix that. One way to do is querying for a directive instead of a component. So, for every step, we can apply a directive to it. It's called "wizard step". This is more verbose than listing the steps without doing anything, but I would argue that it is actually useful. We are making it clear we intend these to be treated as steps and opening the door for the wizard to project other content besides just the steps that it wants to process. This directive doesn't have to do anything at all. Its own job is to exist so that we can query for it. That's what we will do. We can query for the directive instead of querying for the step component, and here we are using content children to get instances of it. Who thinks this is going to work? Wow. Nobody! We will actually get out a list of all the directive instances. But the directive itself is going to be useless. Those are empty objects. We have no way of interacting with the step components. We can't even talk to the DOM directly because we can't get that from the directive. What we really want is a way to query based on the directive but get something else back in our list. We can take advantage of a feature of queries to do just that, called Reading. We pass in a read instruction to view children or content children, and that allows us to specify that the query should select elements with the wizard step directive but we really are interested in the element references. We can't read out the component type because the component is different, we don't know what it is. This is a little more useful. We can manipulate the style of the native element to show our hide steps, but it is still not as good as being able to talk to the API of those components directly. To do that, we have to do something rather unusual. As it turns out, we can use read with queries to get access to dependency injection from the injector of the element that you're querying for. We are going to use this fact to finish our wizard. So our plan is to have each wizard step implement an interface, right? This is a common API that our wizard is going to depend on. Using a DI query, the wizard it then read the service from every step, without necessarily knowing what type of step it is in the first place. Let's start by defining our service. It is going to be an abstract class, because we're going to use it as a DI token, and it has to have the methods that we are going to require each step to implement. In this case, we are using show and hide but in each case the wizard may have interests about dependencies between steps or titles, or stuff like that. Any time we write a step, we are going to have to implement this wizard step interface. That's relatively straightforward. Here's what an our wizard is going to do: we're going to query for the step as before but this time, ask for the injected value of wizard step service instead of the element and that is convenient. That will give us a query list to interact with the API for each step. Injecting is only half the API story, we have to provide it. We need to set up a provider in the step that binds the wizard step-service token, and an easy way to do that is, you say, to use existing and point it to the component instances already in the injector. We are going from a component that the wizard knows nothing about to an interface that it does, and every component, every step component provides that interface with its own type. That almost works. Except this is actually a TypeScript error, right? We are using step 1 as a value inside the metadata. This is one of the few cases where the order of declaration actually matters. So Angular has a way around this, and it's called forwardref which captures the early access inside of a enclosure, and evaluates that closure after the class has been defined, and that solves the problem. With that, we have our finished wizard and step API. So we've explored dependency injection, built a retry module that is safe to use and convenient to configure, and we use queries to develop a wizard that would use injection to communicate between the wizard and the steps, and finally, we combined queries in dependency injection to decouple the two entirely and run all communication through them on a communication service we declared right in the view. So, thank you very much. That's all I have for you today. If you follow me on Twitter, I will tweet out ... [Applause]. Thank you. I have a link to my slides, and also a stack for each example that has the whole thing implemented. I will tweet those out later today. Thank you very much. [Applause]. >> Just a couple of quick things. The lightning talk, you can sign up for those now. There's a board outside the Saturn Room. We are also looking for a couple of people who will be joining Shai's gameshow, so looking for Alex Peshev and Darrel Brown. If you were in the room, come to the front, please.
Info
Channel: AngularConnect
Views: 50,499
Rating: 4.9502072 out of 5
Keywords: angular, angularconnect
Id: EoSn8qASqQA
Channel Id: undefined
Length: 42min 28sec (2548 seconds)
Published: Mon Dec 11 2017
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.