Building a Realtime App with Firebase and Ember M3
This is a modal window.
The media could not be loaded, either because the server or network failed or because the format is not supported.
Formal Metadata
Title |
| |
Title of Series | ||
Number of Parts | 23 | |
Author | ||
License | CC Attribution 3.0 Unported: You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal purpose as long as the work is attributed to the author in the manner specified by the author or licensor. | |
Identifiers | 10.5446/62164 (DOI) | |
Publisher | ||
Release Date | ||
Language |
Content Metadata
Subject Area | ||
Genre | ||
Abstract |
|
EmberConf 20211 / 23
2
3
5
9
10
12
13
14
15
16
17
18
21
22
00:00
CodeModel theorySynchronizationLinked dataClient (computing)Code refactoringKolmogorov complexityData modelComplex (psychology)Software maintenanceFront and back endsMobile appComputer wormProjective planeCombinational logicMereologyMatching (graph theory)Goodness of fitWordBitShared memoryCore dumpRight angleModel theoryDynamical systemSingle-precision floating-point formatObject (grammar)Type theoryShape (magazine)Boolean algebraCodeSocial classDebuggerWeb 2.0Different (Kate Ryan album)10 (number)Axiom of choiceMathematical modelLogicOrder (biology)Software maintenanceSynchronizationMobile appEndliche ModelltheorieClient (computing)Complex (psychology)Instance (computer science)Connectivity (graph theory)QuicksortComputer wormDatabaseCartesian coordinate systemFront and back endsMultiplication signRoutingWeb-DesignerService (economics)TrailScaling (geometry)Software frameworkGroup actionString (computer science)Code refactoringDependent and independent variablesComputer animationJSON
08:48
Complex (psychology)Representation (politics)Query languageModel theoryDatabaseSimilarity (geometry)Table (information)Object (grammar)Total S.A.NumberHTTP cookieMaizeModemMetreStatisticsDampingModule (mathematics)Hardware-in-the-loop simulationEmbedded systemGamma functionDiscrete element methodElectronic meeting systemThermische ZustandsgleichungMountain passEmailLaceTablet computerComa BerenicesDecision tree learningMassChi-squared distributionLie groupRoutingSubject indexingData storage deviceSocial classInterface (computing)HookingHome pageCartesian coordinate systemContent (media)Default (computer science)Limit (category theory)PlanningBoilerplate (text)NumberSimilarity (geometry)Object (grammar)Row (database)Instance (computer science)Total S.A.Category of beingElectronic mailing listDynamical systemString (computer science)Server (computing)Food energyWordDemo (music)Multiplication signHTTP cookieMereologyPower (physics)CountingSoftware developerMobile appSystem callComputer fontModel theoryDatabaseEndliche ModelltheorieSet (mathematics)BitDifferent (Kate Ryan album)CodeArray data structureComputer fileTrailReal numberRevision controlProxy serverTable (information)Representation (politics)Query languageFlow separation
17:15
Game theoryGraphic designAdvanced Boolean Expression LanguageGame controllerHTTP cookieGamma functionOrdinary differential equationGEDCOMDecision tree learningBit error rateRoyal NavyEmailBitEmbedded systemLie groupLattice (order)Core dumpSubsetSchmelze <Betrieb>Dew pointMenu (computing)outputMIDIEstimationScalar fieldLogic gatePlanningConnectivity (graph theory)Template (C++)Default (computer science)Loop (music)Subject indexingData storage deviceLogicoutputGame controllerSet (mathematics)Link (knot theory)Event horizonBitRow (database)Endliche ModelltheorieMessage passingRoutingNumberGroup actionDynamical systemFunctional (mathematics)Generic programmingHash functionText editorParameter (computer programming)EmailHookingQuery languageEqualiser (mathematics)Right anglePerfect groupComputer animationMeeting/Interview
25:43
Gamma functionPolygon meshGame theoryComputer configurationHTTP cookieGEDCOMDemosceneWorld Wide Web ConsortiumLattice (order)Inclusion mapSchmelze <Betrieb>outputSet (mathematics)Bit error rateEmbedded systemIcosahedronSystem callCore dumpCarry (arithmetic)Ring (mathematics)DampingMaxima and minimaScherbeanspruchungBitExecution unitFood energyEvent horizonLogicTemplate (C++)Object (grammar)Connectivity (graph theory)State of matterServer (computing)NumberHookingMusical ensembleParameter (computer programming)Computer configurationFigurate numberFunctional (mathematics)MathematicsMereologyMobile appCategory of beingoutputHTTP cookiePlanningEndliche ModelltheorieTerm (mathematics)Object-oriented programmingType theoryBitTotal S.A.Proxy serverPerfect groupDot productEqualiser (mathematics)Electronic visual displayNormal (geometry)Computer animationMeeting/Interview
34:10
HTTP cookieTotal S.A.NumberBookmark (World Wide Web)State of matterDemo (music)Real-time operating systemBitData structureHTTP cookiePlanningComputer animation
35:08
Linker (computing)Inclusion mapJSON
Transcript: English(auto-generated)
00:22
Hello, EmberConf. How's it going? My name is Chris, but you might know me better by my online handle, Zuroc. And I work at LinkedIn, and I'm a member of the Ember Framework Core Team. Normally, I work on the rendering layer. I work on GlimmerVM and components and auto-tracking, that sort of thing.
00:43
But today, I wanted to talk about something a little bit different. I wanted to talk about a data layer, specifically Ember M3. M3 is a data layer that we developed at LinkedIn to handle some of the problems we were dealing with at scale with vanilla Ember data. And you might be thinking, if Chris is getting up here to talk about this M3 thing,
01:05
is this the future of Ember data? Should I switch over to it immediately? The answer is no, actually. Ember data and M3 are built on the same common core and share most of the same infrastructure.
01:21
M3 is really just a different take on the modeling layer in Ember data, and they can actually be used side-by-side. If you wanted to use M3, it actually has trade-offs, and there are situations where it makes sense, but there are also situations where it doesn't. So we'll dig into that a little bit more as we go on and see if it really would be the right solution for you.
01:46
I personally started using M3 in some side projects alongside Firebase, and part of the reason I wanted to do this talk was just, as I was using it, I found out that it was one of the most fluid, flexible, and
02:03
fun data experiences that I've had in my 10 years as a front-end web developer. Huge thanks to Chris Thoburn, RunSpired, for showing me this combination and showing me just how powerful it could be. So, yeah. Today we'll be talking about what M3 is,
02:22
and what problems it was built to solve, how it works, what Firebase is, and why it's a good match for M3, and then we'll actually see the combination in action, so you don't have to take my word for it. First off, what is M3? Well, I think it's best to understand M3 by understanding the problem that it was trying to solve.
02:44
At LinkedIn, we have a lot of data. We have thousands of models, and those models are very complicated with nested fragments within them, leading to tens of thousands of different models and fragments all over. That is very difficult to keep in sync, in general, with our ever-changing API layer.
03:04
So our first solution was actually to generate the models. Our API layer is highly conventional, so we were able to build tooling that did just that. This helped with the issue of keeping things in sync, but we still had this problem of shipping tens of thousands of generated models and fragments to our clients, and that added up.
03:26
That was hundreds of kilobytes of code that was having an impact on our customers. So, enter M3. The idea with M3 was rather than have a class for every single model and fragment,
03:41
we would have one class that could represent all of them, and it would do that dynamically. The way M3 does this is it really just understands a model to be a blob of JSON. It can have JSON values of any type. For instance, here we have a
04:01
name, an object that has a name, which is a string, and it has a chapters array, which is an array of objects. These could be updated to be Booleans or any kind of value. You don't have to specify what the shape of your model is ahead of time.
04:22
Now, this is still better than using fetch, because M3 does understand that each model has an ID and a type, and so it'll de-duplicate them and make sure you're using the same reference to the model everywhere that you're using it. And this is also very important for M3's other major feature. It also understands
04:44
relationships within that blob of JSON. It understands them based on conventions. So, in this API response, for instance, we are representing a reference to another object as an object with a type and ID. M3 will see this, and based on that convention, it will load those models and
05:05
replace them in the actual model itself, so that when we access them, we're accessing the instance of the relationship, rather than the type ID thing. So we don't have to figure that all out ourselves.
05:23
This can be configured with the M3 schema. So you can make this work with any kind of convention for your relationships and with any conventional API. It can work with JSON API or RESTly, which is our LinkedIn spec, or GraphQL,
05:41
so on. Anything that has strong conventions for relationships. This allowed us to go from tens of thousands of models being shipped to the client to just one. In our single largest refactor, we removed over 64 kilobytes of generated code after min and gzip, which is absolutely enormous.
06:02
So it's had a huge impact on our code. Now, is M3 the right choice for you? As I said before, there are trade-offs. On the pros side of things, we have the fact that it handles these complex nested models of JSON very fluidly.
06:22
This also works really well with strong and conventional APIs, and it requires less maintenance to keep things in sync between your front end and back end. Finally, for very large apps with hundreds or thousands of models, it definitely can help to improve performance.
06:40
The cons are your front end no longer has your model definitions. So you really have to have like good API documentation in order to make sure that your code doesn't get out of sync and that everybody understands what the models should look like, what the API should look like. It also really requires a conventional API. It can't work with one-off or bespoke APIs,
07:03
although you could use vanilla Ember data alongside it for that. Finally, M3 really expects to load relationships synchronously when you access them. What this means is that there's no promise API for loading a belongs to or has many
07:22
only when you need it. If you want to do that, you have to make sure that those relationships are loaded ahead of time yourself. So you either have to include that logic in your routes, or you have to absorb it somewhere else yourself. And that can be a little bit of complexity to add to your data layer.
07:46
So M3 is really great for large apps that have conventional APIs and will definitely benefit from the decreased maintenance and payload size. But it's not as great for small to medium-sized apps that won't benefit as much from that decreased size and
08:02
may have more trouble with the dynamic models and the sync relationships and so on. That said, I can guarantee you that the personal projects that I've been working on aren't the size of LinkedIn, and the whole premise of this talk is that Firebase plus M3 is
08:21
absolutely an amazing combination. So why is that exactly? Well, to understand that, I think we need to dig into Firebase a little bit. What is Firebase? Firebase is a back-end as a service, which basically means it provides everything you need to write an application and just focus on the front-end. Firebase provides a database and it provides things like push
08:45
notifications and so on. And this can be really helpful if you're like me and you only have a few hours on the weekends to work on some idea, and you just want to get it out there really quickly. Firebase's database is Firestore, and
09:01
the difference between this database and other databases is that it's a document-oriented database. And what that means is that rather than representing data as tables, it represents it as objects, and specifically as JSON. So you can write any record into your database as any arbitrary
09:26
JSON value. And this has a number of advantages. It means you can represent really complex models very easily. There's no need to have 10 tables and 20 joins to represent something that's conceptually simple, like
09:43
an object that has an array with a couple of nested objects in it. It also really means you can think about your database and application in the same way as you're developing them. And that can be really helpful for making application development more fluid.
10:00
The cons are it has very limited query capabilities. It's hard to query arbitrary JSON objects in general. And because of that, it also has no concept of relationships. Relationships really have to be managed manually. So it means that there's a lot more boilerplate there.
10:21
You have to do the work yourself to figure out what related values are and to load them. So right off the bat, we can see the similarities between Firestore and M3. Both of them think about models or records or documents, whatever you want to call them, as JSON values. They're just data that has
10:43
any kind of JSON object within it or array or any other value. The key thing that M3 adds here is those relationships once again. M3 is able to understand what the relationship looks like when it reads that JSON
11:02
value and replace it with an instance of that class, of that model. So you don't have to do all of that boilerplate to figure out what the relationship is yourself and load it yourself from Firestore. M3 handles that for you. This means you get all the advantages of a document store database.
11:23
But you have conventions around your relationships and that is what makes it such an amazing development experience in my opinion. But you don't have to take my word for it. Let's dig right in. It's demo time.
11:41
Okay, so for today's demo we'll be working on a simple meal planning app called Ember Meal Plans and the concept is pretty straightforward. You'll be able to add recipes and then use those recipes to create meal plans. I've already actually created the recipe portion of the application as we can see here. We can add a new recipe.
12:05
We'll call it cookies. It'll serve 12. It'll create 12 cookies and I'll say 200 calories a cookie. The most important part bake it with love. The most important ingredient when baking cookies.
12:24
And as we can see if we reload that all persists and everything so it's already hooked up to Firestore. What we're gonna do is create the meal planning portion of the app which so far all I have here is an outline made with HTML, but no actual JavaScript hooking it up to Firestore.
12:41
So we can see we have our index page that shows the total list of meal plans and then if we go into our meal plan it has a name, it has a number of days, and each day has a number of meals which we can add to and do things like track our total calorie counts and so on and so forth. So let's get started.
13:03
First up I just wanted to have a real quick note. I'm actually using Canary versions of Ember Data and Ember M3 because I wanted to show a Canary feature that is coming soon, which is native proxy support. And that allows us to not use Ember get and Ember set and rather
13:24
instead use native access and native methods like push instead of push object on arrays and such. This is a sorely needed feature because otherwise M3 definitely still feels like it's a little bit pre-octane
13:42
but I'm really looking forward to it and I wanted to show you what the development experience is with that enabled. Because as I said, it should be here sooner rather than later. So first off I want us to start by thinking about the model itself.
14:01
So I like to document my models in my code base and I use TypeScript interfaces for that. We don't actually have to create any files or interfaces or any TypeScript to work with M3 models because they're dynamic. But I find it really helpful if there's some clear concise documentation of what we actually expect them to look like.
14:26
So I already created one for recipes and we can see here that recipes have a name, which is a string, instructions, another string, a number of servings, and total calories. So let's go ahead and create an interface for meal plants, interface meal plan.
14:45
Export default meal plan. It's a quirk of TypeScript. And first off our meal plans definitely have a name as we can see here, and then they have a number of days and
15:02
those days are actually going to have a lot of their own properties. So it's a list of objects, an array of objects. And we'll actually go ahead and create another interface so we can document that that object and see what it looks like.
15:20
Within that we can see days have a number, but that's honestly just the index in the array where the day is if it's the first one, second one, and so on. So we don't need to store that. And they also have a total calorie count, but that's also derived. So the only thing it really has per day is a list of meals.
15:41
And meals are also objects because each meal has a number of properties. So we'll go ahead and create another interface for meals. And meals have a name as well. Name string. And here's where we start to see some of the power of M3. Meals have a recipe which we can just
16:05
import the interface from that file and use it here. And this is really where we see some of the power of M3 because we can just place this relationship on this nested object within this model. We don't need to
16:23
create any other model classes or intermediate steps like we would with Ember Data. This whole model here is pretty complicated. It's got several layers of nesting and in Ember Data that would have to be a few has many relationships and belongs to relationships and so on.
16:44
But with M3 it can just be a JavaScript value, a JSON value with the nested relationship in it. So that really helps to clarify everything and I feel like this interface helps us to kind of see what we expect our data to be and of course to document that.
17:03
Okay, so now moving on let's actually hook up the index route here. So I've got my index route set up and I've already injected the store into it and I'm going to do model and for the model I'm just going to return this dot store dot find all meal plan.
17:23
And that is basically the same as Ember Data there. We can just find all the meal plans and use them. And we'll actually use them in our template here. H model as plan H.
17:46
We got this link to here. We're going to have to pass the model to it as well. Equal plan dot ID so that it triggers the logic within our show route which we'll see in a minute.
18:01
And we'll use plan dot name for the title of that link. All right, so now we don't have anything because we haven't created a plan yet. So next up we're going to add an event listener to our template on the button for new plans create plan.
18:24
And then in our controller we are going to add an event listener here for creating that new plan. And we're going to make that an async function. And I've already injected store here as well. It's kind of like a plan this out or something.
18:41
And then we're going to create a new plan. Let plan equal this dot store dot create record meal plan. And we're going to pass in a name to that and just a default kind of generic name. Name new plan.
19:01
And we're also going to pass in an array for days. And it's just going to be an empty array for now. So this is again basically the same as what you would do in Ember Data. Just store dot create record and then we can do await plan dot safe.
19:21
And then we'll do we'll transition to the plans dot show route. Using the plan id. Perfect. And if we press the button we see the plan. We don't actually transition to it though.
19:40
And I think I know what that is. I think we need to figure out what to do with. First off we need to add the dynamic segment to our link to. So plan id. And then we need to actually add some logic to our show route to handle.
20:05
Loading the data. Loading the model. Otherwise there's some default logic that happens and that's not what we want. So let's do that real quick.
20:23
So we have our model function and like before we have our store already injected. But we also get plan id now. And I'm also going to import hash from rsvp because I want to return a couple of promises here. So the first one I want to return is the plan.
20:42
We're going to do this dot store dot find record. And that's going to be meal plan. And we're going to pass in plan id. So the meal plan that we actually want to load. But then we're going to load all of the recipes that go along that exist in our store.
21:06
Because we want to be able to select from our recipes within our meal plan editor. You could do this dynamically in one of your components. But for now just to keep things simpler I'm doing it here. And I'm going to do this dot store dot query recipe.
21:23
Because I also want to pass in an extra parameter here. I want to pass in subscribe which is going to subscribe us to updates for those recipes. This will help to demonstrate some of the reactive features later on. And this is something that is special to unique to Firestore in particular.
21:43
It's not an M3 feature in general. All right cool. So now if we click on that okay our route is working again. But nothing is hooked up of course yet. So let's go to our template and start doing that. We'll do at model dot plan dot name.
22:04
But that's not actually going to update anything. That header is just there for accessibility and purposes. The value that we actually need to hook up here is this input. And I have an add-on that I'm using called x input that we're going to use for that.
22:22
So we're going to pass in value. This is going to be at model dot plan dot name. And then on input we're going to set at model dot plan name. So this is using emberset helper.
22:40
This allows us to update the value without actually having to define a JavaScript handler. Which can be really nice when you just want to update a value. So we'll do that and look at that. That works. It updates here correctly. But if we reload it doesn't persist because we're not actually saving it.
23:02
So next up we are going to... I actually don't think I need store in this one. We're going to add save to our controller. And this is going to take our model this dot model dot plan and call save on it. So then we can use that in our template.
23:21
I'm going to use ember composable helpers here to queue up a few different actions to happen together. So we're going to do set model plan dot name. And then we're going to call this dot save immediately after that. So now if we update this to be my plan and we reload.
23:44
That did not work. Oh got to save. My plan and everything persists. Awesome. Okay next up I'm going to actually extract out a component here because the rest of this
24:00
template is a little bit complicated for my taste. I don't think we should do this all in one spot. So I'm going to generate a component. Then I'm going to copy that code over into this component. And then what I'm going to do is we're going to actually loop over model dot plan dot days as day.
24:31
And we are going to invoke our component meal plan day. We're going to pass it day which will be the day.
24:41
We're also going to pass it day number equals and we need our index here from our loop. And we're going to increment it with Ember composable helpers. Ink index one. And for now that's all we're going to pass it. So internally in our component we can do at day number.
25:06
Okay cool. We go back of course there aren't any days yet because our meal plan started off with no days. So let's also add the logic for adding a day. We on click this dot add day and then add day equals
25:28
this dot model dot plan dot days dot push. We can just push into the days array like it's a normal array. And we're going to push a new day. It's going to have a meals array and that's all it has.
25:42
And I also think we can add a default model here. We'll have name new meal recipe null and servings one. Because I think honestly if you're creating a new day you probably want to have a meal in it.
26:03
Just for starters. So then we'll do this dot model dot plan dot save. And now in our app we can add a day. And if we reload that day is still there. So next up we are going to hook up the meals form within our day.
26:21
We're going to add an each loop because we're going to each over the day dot meals array as meal each and then within that within that we're going to use our x input again.
26:49
I'm just going to copy that over from over here put that in. So the first one. Also updating a name but of the meal instead of model.
27:20
Oh I have to put it inside the li up here as well.
27:24
Okay cool perfect. So new meal is updated. Foo cool. That won't persist yet though because we need to also queue in save. And save is not this dot save. We have to actually pass it in as an argument here.
27:41
So let's pass that in at save equals this dot save. Perfect. Now that should update and if we reload. Awesome. We're still having cookies for breakfast. That's the way I like to start my day. All right next up we're going to do the select here.
28:00
I'm using x select which is an add-on for selects and I'm going to do value equal meal dot recipe and here's where we update and change our relationship. So we can just pass in the recipe set meal recipe and add save similar to the input.
28:27
I'm just going to quickly copy over some template for the options themselves. As we can see here we actually pass in the recipes themselves into each option and then
28:46
set them directly on that on the meal. We don't have to do anything special. We can just add the relationship to the JavaScript value and m3 figures out all the details for us and of course we have to pass in the recipes to our meal plan day
29:02
component so we'll go to model dot recipes here and if we go back now we can select a recipe. Still doing cookies. All right finally we'll do the servings input there. Just copy our logic here type equals number because it's a number input and set meal dot
29:26
servings and I want two cookies for breakfast. Oops added another day there. Okay cool so everything appears to be working. Last thing we need to do is add this the logic for this add meal button.
29:41
So let's do that real quick. We're actually going to need a event handler so we'll do that and it's going to need to receive the day that we're adding the meal to as an object because otherwise we wouldn't really know. That's going to be day dot meals dot push.
30:04
We'll just copy the template here for a empty meal and then this dot model dot plan dot save. Perfect and then now we can use that within our template. Add meal equals this dot add meal and in our component we can now add it to this button.
30:27
On click this dot but it's not this sorry and we actually need to use fm because we need to pass in the the day as well so now we should be able to do yep and we can add meals.
30:45
I'm going to add lunch and we're going to go with french toast because you know I really am enjoying sweets today. Cool finally let's do these calories counter things here.
31:01
We want to actually be able to show some derived state. So I'm going to generate EmberG some helpers to help us out with that. Meal calories then EmberG helper day calories all right and then in day calories we're going
31:30
to receive our day actually I'll start with meal calories meal calories we're going to receive our meal and I'm going to have to reuse this logic in day calories.
31:43
So I'm going to export this function and then return let's see we're going to want to do let
32:04
recipe and servings from our meal equals meal and then if we have a recipe it could be null. So if so we're going to just return zero but if we do have a recipe we're going to want to do
32:21
math dot floor of recipe dot total calories divided by recipe dot num servings got to find out how much an individual serving is and then we'll multiply that by the number of servings that we're having and we do math dot floor just to make it a little bit nicer in terms of
32:41
displaying. So we can use that in our component here we'll go meal calories meal and that looks about correct me we add another cookie that increases the overall calories
33:01
and then for day calories I'm actually going to just copy paste that over here real quick but you can see the gist of it we import meal calories and then we have our total and we add the meal calories for each meal to our total for the day and then I'm going to use that in our template real quick.
33:23
The important part about these helpers day calories dot day is that they're all just plain JavaScript we're just using a meal with properties that have recipe servings the recipe relationship is just treated like a normal object even though it is a relationship
33:45
and for the meals themselves we can for each over it just like it's a normal JavaScript array so all of our derived state as well especially with this new proxy feature that I mentioned earlier is really intuitive and easy to use overall in my experience and yeah now we have a
34:06
meal planner that can show us the calories that we're eating on our every day and you know I'm going to just go ahead and show off a little bit more here let's say that you wanted to you know edit those recipes maybe we decided you know what our cookies don't need that much sugar
34:30
we can just go ahead and do that and we can see the impact that it has in real time on our meal plan because our meal plan is represented in this very straightforward data
34:44
structure that allows us to create all of this derived state based on it and we also have all the real-time capabilities of Firebase so that to me is something that is pretty amazing and one of my favorite features about Firestore overall
35:03
all right so that concludes our demo thank you so much